77 lines
4.3 KiB
PHP
77 lines
4.3 KiB
PHP
<?php
|
|
|
|
use Illuminate\Support\Facades\DB;
|
|
use Symfony\Component\Process\Process;
|
|
|
|
function orderCancellationRaces(array $environment, array $config, string $database): array
|
|
{
|
|
config(['database.connections.cancel_race_read' => [...$config, 'database' => $database, 'url' => null]]);
|
|
$reader = DB::connection('cancel_race_read');
|
|
$results = [];
|
|
foreach (['request_receipt', 'receipt_request', 'double_approval'] as $scenario) {
|
|
$worker = fn (array $args) => new Process([PHP_BINARY, 'tests/Support/order-cancellation-worker.php', ...$args], base_path(), $environment, timeout: 40);
|
|
$seed = $worker(['seed']);
|
|
$seed->mustRun();
|
|
$data = json_decode($seed->getOutput(), true, 64, JSON_THROW_ON_ERROR);
|
|
$data['barrier'] = storage_path('framework/testing/cancellation-race-'.bin2hex(random_bytes(8)));
|
|
if (! is_dir(dirname($data['barrier'])) && ! mkdir(dirname($data['barrier']), 0700, true)) {
|
|
throw new RuntimeException('Testing barrier directory unavailable.');
|
|
}
|
|
if ($scenario === 'double_approval') {
|
|
$worker(['run', json_encode($data, JSON_THROW_ON_ERROR), 'request'])->mustRun();
|
|
}
|
|
$first = $worker(['hold', json_encode($data, JSON_THROW_ON_ERROR), $scenario === 'receipt_request' ? 'receipt' : ($scenario === 'double_approval' ? 'approve' : 'request')]);
|
|
$second = $worker(['run', json_encode($data, JSON_THROW_ON_ERROR), $scenario === 'receipt_request' ? 'request' : ($scenario === 'double_approval' ? 'approve' : 'receipt')]);
|
|
try {
|
|
$first->start();
|
|
$deadline = microtime(true) + 15;
|
|
while ($first->isRunning() && ! str_contains($first->getOutput(), 'LOCKED') && microtime(true) < $deadline) {
|
|
usleep(20000);
|
|
}
|
|
if (! str_contains($first->getOutput(), 'LOCKED')) {
|
|
throw new RuntimeException('Company lock not acquired by first worker.');
|
|
}
|
|
$second->start();
|
|
$waited = false;
|
|
$deadline = microtime(true) + 15;
|
|
while ($second->isRunning() && microtime(true) < $deadline) {
|
|
$waited = $reader->selectOne("select count(*) as total from pg_stat_activity where datname = ? and application_name = 'cancel-race-run' and wait_event_type = 'Lock'", [$database])->total > 0;
|
|
if ($waited) {
|
|
break;
|
|
} usleep(20000);
|
|
}
|
|
if (! $waited) {
|
|
throw new RuntimeException('Company row lock wait not observed.');
|
|
}
|
|
file_put_contents($data['barrier'], 'release');
|
|
$first->wait();
|
|
$second->wait();
|
|
if (! $first->isSuccessful() || ! $second->isSuccessful() || ! str_contains($first->getOutput(), 'OK')
|
|
|| ! str_contains($second->getOutput(), $scenario === 'double_approval' ? 'OK' : 'BLOCKED')) {
|
|
throw new RuntimeException('Concurrent outcome not expected.');
|
|
}
|
|
$receipts = $reader->table('goods_receipts')->where('purchase_order_id', $data['order'])->count();
|
|
$cancellations = $reader->table('order_cancellations')->where('purchase_order_id', $data['order'])->get();
|
|
$reversals = $reader->table('budget_entries')->where('kind', 'order-cancel-'.$data['order'])->count();
|
|
if ($receipts !== ($scenario === 'receipt_request' ? 1 : 0) || $cancellations->count() !== ($scenario === 'receipt_request' ? 0 : 1)
|
|
|| $reversals !== ($scenario === 'double_approval' ? 1 : 0)
|
|
|| ($scenario === 'double_approval' && $cancellations->first()->status !== 'approved')) {
|
|
throw new RuntimeException('Concurrent effects not unique.');
|
|
}
|
|
$results[] = ['scenario' => $scenario, 'company_lock_wait_observed' => true, 'receipts' => $receipts, 'cancellations' => $cancellations->count(), 'reversal_entries' => $reversals, 'result' => 'passed'];
|
|
} finally {
|
|
if ($first->isRunning()) {
|
|
$first->stop();
|
|
} if ($second->isRunning()) {
|
|
$second->stop();
|
|
}
|
|
if (is_file($data['barrier'])) {
|
|
unlink($data['barrier']);
|
|
}
|
|
}
|
|
}
|
|
DB::purge('cancel_race_read');
|
|
|
|
return $results;
|
|
}
|