fix: complete logical clock wall-time isolation
This commit is contained in:
@@ -35,7 +35,12 @@ anchor에 고정하므로 표시 시각이 튀지 않습니다.
|
||||
|
||||
```bash
|
||||
php scripts/verify-game-clock-engine.php --apply --engine-calls=2
|
||||
php scripts/verify-game-clock-engine.php --apply --until-unification --max-months=2400
|
||||
```
|
||||
|
||||
이 검증기는 manual mode만 허용하고, 엔진 호출 전후 clock tick이 벽시계 때문에
|
||||
변하지 않았는지와 마지막 처리 tick이 현재 tick을 넘지 않았는지 검사합니다.
|
||||
`--until-unification`은 달력이나 DB 시각을 고쳐 쓰지 않고, 매월 manual clock만
|
||||
정확히 다음 turn tick으로 옮긴 뒤 실제 `TurnExecutionHelper`를 반복 호출합니다.
|
||||
각 호출에서 시계 고정과 처리 tick 상한을 재검사하고 `isunited=2|3`이 되지 않으면
|
||||
성공으로 취급하지 않습니다. 반드시 격리 복제 DB에서 실행하세요.
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
declare(strict_types=1);
|
||||
|
||||
use sammo\DB;
|
||||
use sammo\DTO\VoteInfo;
|
||||
use sammo\GameClock;
|
||||
use sammo\Json;
|
||||
use sammo\KVStorage;
|
||||
@@ -269,6 +270,30 @@ try {
|
||||
'last천도Trial',
|
||||
);
|
||||
}
|
||||
foreach ($db->query(
|
||||
'SELECT `key`, value FROM storage WHERE namespace = %s AND `key` LIKE %s AND `key` NOT LIKE %s',
|
||||
'vote',
|
||||
'vote\_%',
|
||||
'%\_wall\_backup',
|
||||
) as $row) {
|
||||
$rawVote = Json::decode((string)$row['value']);
|
||||
if (!is_array($rawVote)) {
|
||||
throw new RuntimeException("{$row['key']} vote 저장값이 객체가 아닙니다.");
|
||||
}
|
||||
$db->insertUpdate('storage', [
|
||||
'namespace' => 'vote',
|
||||
'key' => "{$row['key']}_wall_backup",
|
||||
'value' => Json::encode($rawVote),
|
||||
]);
|
||||
$db->update('storage', [
|
||||
'value' => Json::encode(VoteInfo::normalizeGameStorage($rawVote, $conversionClock)),
|
||||
], 'namespace = %s AND `key` = %s', 'vote', $row['key']);
|
||||
}
|
||||
|
||||
// Historical/display DATETIME columns remain dates, but inserts must always
|
||||
// receive a GameClock-projected value rather than silently reading MariaDB time.
|
||||
$db->query('ALTER TABLE ng_old_nations MODIFY `date` DATETIME NOT NULL');
|
||||
$db->query('ALTER TABLE ng_diplomacy MODIFY `date` DATETIME NOT NULL');
|
||||
|
||||
$db->query(
|
||||
'ALTER TABLE general '
|
||||
|
||||
@@ -19,16 +19,25 @@ $_SERVER['REQUEST_URI'] ??= '/cli/verify-game-clock-engine';
|
||||
|
||||
require dirname(__DIR__) . '/hwe/lib.php';
|
||||
require dirname(__DIR__) . '/hwe/func.php';
|
||||
while (ob_get_level() > 0) {
|
||||
ob_end_flush();
|
||||
}
|
||||
ob_implicit_flush(true);
|
||||
|
||||
$options = getopt('', ['apply', 'engine-calls:']);
|
||||
$options = getopt('', ['apply', 'engine-calls:', 'until-unification', 'max-months:']);
|
||||
if (!isset($options['apply'])) {
|
||||
fwrite(STDERR, "Usage: php scripts/verify-game-clock-engine.php --apply [--engine-calls=N]\n");
|
||||
fwrite(STDERR, "Usage: php scripts/verify-game-clock-engine.php --apply [--engine-calls=N] [--until-unification --max-months=N]\n");
|
||||
exit(2);
|
||||
}
|
||||
$engineCalls = filter_var($options['engine-calls'] ?? '1', FILTER_VALIDATE_INT);
|
||||
$untilUnification = isset($options['until-unification']);
|
||||
$engineCalls = filter_var($options['engine-calls'] ?? ($untilUnification ? '100' : '1'), FILTER_VALIDATE_INT);
|
||||
if ($engineCalls === false || $engineCalls < 1 || $engineCalls > 1000) {
|
||||
throw new InvalidArgumentException('--engine-calls는 1..1000이어야 합니다.');
|
||||
}
|
||||
$maxMonths = filter_var($options['max-months'] ?? '2400', FILTER_VALIDATE_INT);
|
||||
if ($maxMonths === false || $maxMonths < 1 || $maxMonths > 10000) {
|
||||
throw new InvalidArgumentException('--max-months는 1..10000이어야 합니다.');
|
||||
}
|
||||
|
||||
$db = DB::db();
|
||||
$gameStor = KVStorage::getStorage($db, 'game_env');
|
||||
@@ -39,6 +48,112 @@ if ($clock->getMode() !== GameClock::MODE_MANUAL) {
|
||||
|
||||
$fixedNowTick = $clock->nowTick();
|
||||
$before = $gameStor->getValues(['year', 'month', 'turntime']);
|
||||
|
||||
if ($untilUnification) {
|
||||
$startedAt = GameClock::readWallTime();
|
||||
$startProjection = $clock->formatTick($fixedNowTick, true);
|
||||
$advancedMonths = 0;
|
||||
$totalEngineCalls = 0;
|
||||
while ($advancedMonths < $maxMonths) {
|
||||
$gameStor->resetCache();
|
||||
$state = $gameStor->getValues(['year', 'month', 'turntime', 'turnterm', 'isunited']);
|
||||
if (in_array(Util::toInt($state['isunited']), [2, 3], true)) {
|
||||
break;
|
||||
}
|
||||
|
||||
$beforeYearMonth = Util::joinYearMonth(Util::toInt($state['year']), Util::toInt($state['month']));
|
||||
$clock = GameClock::fromStorage($gameStor);
|
||||
$nextMonthBoundary = $clock->addTurns(
|
||||
\sammo\cutTurn(Util::toInt($state['turntime']), Util::toInt($state['turnterm'])),
|
||||
1,
|
||||
);
|
||||
$nextMonthTick = GameClock::addTicks($nextMonthBoundary, 1);
|
||||
|
||||
if (!\sammo\tryLock()) {
|
||||
throw new RuntimeException('manual clock 전진을 위한 GAME lock을 획득하지 못했습니다.');
|
||||
}
|
||||
try {
|
||||
$clock->persistTick($gameStor, $nextMonthTick, GameClock::MODE_MANUAL);
|
||||
} finally {
|
||||
$gameStor->resetCache();
|
||||
\sammo\unlock();
|
||||
}
|
||||
|
||||
$monthAdvanced = false;
|
||||
for ($call = 0; $call < $engineCalls; $call++) {
|
||||
$executed = false;
|
||||
$locked = false;
|
||||
TurnExecutionHelper::executeAllCommand($executed, $locked);
|
||||
$totalEngineCalls++;
|
||||
$gameStor->resetCache();
|
||||
$clock = GameClock::fromStorage($gameStor);
|
||||
if ($clock->getMode() !== GameClock::MODE_MANUAL || $clock->nowTick() !== $nextMonthTick) {
|
||||
throw new RuntimeException('실제 턴 엔진 실행 중 manual clock 상태가 벽시계에 의해 바뀌었습니다.');
|
||||
}
|
||||
$afterCall = $gameStor->getValues(['year', 'month', 'turntime', 'isunited']);
|
||||
if (Util::toInt($afterCall['turntime']) > $nextMonthTick) {
|
||||
throw new RuntimeException('마지막 실행 tick이 현재 manual clock tick을 넘어갔습니다.');
|
||||
}
|
||||
if (in_array(Util::toInt($afterCall['isunited']), [2, 3], true)) {
|
||||
$monthAdvanced = true;
|
||||
break;
|
||||
}
|
||||
// turntime is the authoritative completed schedule boundary. Some
|
||||
// legacy monthly state is cached until the next storage read, so do
|
||||
// not spin merely because year/month from that same call is stale.
|
||||
if (Util::toInt($afterCall['turntime']) >= $nextMonthBoundary) {
|
||||
$monthAdvanced = true;
|
||||
break;
|
||||
}
|
||||
$afterYearMonth = Util::joinYearMonth(Util::toInt($afterCall['year']), Util::toInt($afterCall['month']));
|
||||
if ($afterYearMonth !== $beforeYearMonth) {
|
||||
$monthAdvanced = true;
|
||||
break;
|
||||
}
|
||||
if ($locked) {
|
||||
throw new RuntimeException('통일 전 실제 턴 엔진이 GAME lock 또는 동결 상태에 머물렀습니다.');
|
||||
}
|
||||
}
|
||||
if (!$monthAdvanced) {
|
||||
throw new RuntimeException("한 달을 {$engineCalls}회 엔진 호출 안에 완료하지 못했습니다.");
|
||||
}
|
||||
|
||||
$advancedMonths++;
|
||||
if ($advancedMonths % 12 === 0) {
|
||||
$gameStor->resetCache();
|
||||
printf(
|
||||
"progress months=%d game=%d-%02d clock_tick=%d nations=%d engine_calls=%d\n",
|
||||
$advancedMonths,
|
||||
Util::toInt($gameStor->year),
|
||||
Util::toInt($gameStor->month),
|
||||
GameClock::fromStorage($gameStor)->nowTick(),
|
||||
Util::toInt($db->queryFirstField('SELECT COUNT(*) FROM nation WHERE level > 0')),
|
||||
$totalEngineCalls,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
$gameStor->resetCache();
|
||||
$finalState = $gameStor->getValues(['year', 'month', 'turntime', 'isunited']);
|
||||
if (!in_array(Util::toInt($finalState['isunited']), [2, 3], true)) {
|
||||
throw new RuntimeException("{$maxMonths}개월 안에 천하통일에 도달하지 못했습니다.");
|
||||
}
|
||||
$finalClock = GameClock::fromStorage($gameStor);
|
||||
printf(
|
||||
"UNIFIED months=%d engine_calls=%d game=%d-%02d isunited=%d clock_tick=%d projected=%s start_projected=%s wall_elapsed=%.6f\n",
|
||||
$advancedMonths,
|
||||
$totalEngineCalls,
|
||||
Util::toInt($finalState['year']),
|
||||
Util::toInt($finalState['month']),
|
||||
Util::toInt($finalState['isunited']),
|
||||
$finalClock->nowTick(),
|
||||
$finalClock->formatNow(true),
|
||||
$startProjection,
|
||||
(float)GameClock::readWallTime()->format('U.u') - (float)$startedAt->format('U.u'),
|
||||
);
|
||||
exit(0);
|
||||
}
|
||||
|
||||
$executedCount = 0;
|
||||
for ($call = 0; $call < $engineCalls; $call++) {
|
||||
$executed = false;
|
||||
|
||||
Reference in New Issue
Block a user