Kimura A
a.kim****@live*****
2013年 9月 20日 (金) 19:46:17 JST
木村です。
より読みにくそうなInsertシェル側のみ、引数を廃止したりクエリをベタ書きに近づけたりと簡略化してみました。
末尾にコピー&ペーストしておきますので、テストの際はこちらを使っていただければと思います。
Insertシェルを下掲の内容に差し替え後、念のためにcronによる「5分間Insert→1分間の停止中に二重Select」の実験を行ってみたところ、僕の環境ではレコード数が51500件になった段階でエラーが再現しました。
4回連続エラーになった時点でテストを終了し、うち3回は片方エラー、1回は両方エラーという結果でした。
以上、どうぞよろしくお願いしますm(_ _)m
<?php
App::uses('AppShell', 'Console/Command');
/**
* mroonga「WHERE IN 多数」クエリのテスト用データ生成シェル
*
* @property Model $Model
*/
class InsertShell extends AppShell {
/**
* メイン処理
*
* @param Model $Model
* @return void
*/
public function main() {
$limit = 100; // 1度にいくつのレコードをINSERTするか
$minutes = 5; // 実行を許可する最大分数。cronによる動作時の重複回避用
$headRoomSec = 10; // 実行許可分間のうち処理の重複回避のために何秒の余裕を残すか
// モデルを準備して接続先をtestデータベースに切り替え
$this->Model = ClassRegistry::init('Product');
$this->Model->setDataSource('webTest');
// ロックファイルがある場合
if (file_exists(TMP . 'mroonga_errored')) {
if ($this->Model->find('first', array('fields' => array('id')))) {
// productsテーブルが空でない場合:エラー再現条件をすでに満たしているとみなして終了
CakeLog::debug("mroonga_erroredファイルが存在するためINSERT処理を回避します");
return;
}
@unlink(TMP . 'mroonga_errored');
}
// 通常は開始ログのみ出力
CakeLog::debug("{$limit}件単位の一括INSERT処理({$minutes}分間)を開始します");
if (file_exists(TMP . 'mroonga_errored')) {
// エラー発生後はINSERT処理回避
CakeLog::debug("mroonga_erroredファイルが存在するためINSERT処理を回避します");
return;
}
$startTS = time(); // 処理開始TS
$timeLimitTS = $startTS + (60 * $minutes) - $headRoomSec; // 想定上のタイムリミットTS:$headRoomSec秒の余裕を確保
$loopSecMax = 0; // 1回のループに要した最大秒数を保持
for ($i = 0; $i < 10000; $i++) {
if ($i) {
// 初回ループ以外は1秒のインターバルを挟む
sleep(1);
}
// ループ処理開始時刻を保持
$loopStart = time();
// 一括INSERT実行
if (!$this->insert($limit)) {
// エラー時停止
return;
}
// 一括INSERTの所要秒数を算出
$loopSec = time() - $loopStart;
// 一括INSERTの最大所要秒数を特定
$loopSecMax = max(array($loopSecMax, $loopSec));
// 一括INSERTの最大所要秒数を基準に、トータル処理時間が次のループで所定の秒数を超過しないかチェック:後続のSelectシェル処理との重複回避
$loopEndTS = time();
if ($loopSecMax > ($timeLimitTS - $loopEndTS)) {
// 超過の可能性がある場合はログを出力して処理を停止
$loopCount = $i + 1;
$totalSec = $loopEndTS - $startTS;
$maxId = $this->getMaxId();
$dbNameData = $this->Model->query('SELECT database() as `db`;', false);
$dbName = $dbNameData[0][0]['db'];
CakeLog::debug("{$minutes}分間のタイムリミットが近いため処理を終了します。DB名:{$dbName}/最大products.id:{$maxId}/トータル動作秒数:{$totalSec}/ループ回数:{$loopCount}/ループ最大所要秒数:{$loopSecMax}/ヘッドルーム秒数:{$headRoomSec}");
break;
}
}
}
/*
* 現時点での最大products.id値を取得
*/
public function getMaxId() {
$record = $this->Model->find('first', array(
'fields' => array('id'),
'order' => 'Product.id DESC'
));
return empty($record['Product']['id']) ? 0 : $record['Product']['id'];
}
/*
* 一括INSERT実行
*/
public function insert($limit) {
// クエリの構成
$sql = "INSERT INTO test.products (id, code, description, vendor_id, tag_pool) VALUES ";
$id = $this->getMaxId();
$vendorId = 1;
for ($i = 0; $i < $limit; $i++) {
// 個別レコードを準備
$id++;
$sqlChild = "(null";
foreach (array('code', 'description', 'vendor_id', 'tag_pool') as $field) {
switch ($field) {
case 'code':
$sqlChild .= ", 'code{$id}'";
break;
case 'description':
case 'tag_pool':
$sqlChild .= ", '" . $this->getText(1000) . "'";
break;
case 'id':
break;
case 'vendor_id':
$sqlChild .= ", 1";
break;
case 'title':
$sqlChild .= ", 'タイトル{$id}'";
}
}
$sqlChild .= ")";
if ($i) {
$sql .= ', ';
}
$sql .= $sqlChild;
}
$sql .= ";";
// 一括INSERT実行
$this->Model->query($sql);
return true;
}
/*
* TEXTカラム用のランダム文字列を構成
*/
public function getText($len = 100) {
// 5文字程度で半角スペース区切りになった文字列データを生成するためスペースを混入させておく
$sCharList = "abcde fghij klmno pqrst uvwxy zABCD EFGHI JKLMN OPQRS TUVWX YZ012 34567 89_-, .";
mt_srand();
$sRes = '';
for($i = 0; $i < $len; $i++) {
$sRes .= $sCharList{mt_rand(0, strlen($sCharList) - 1)};
}
return $sRes;
}
}
?>