Files
compras/tests/Support/order-cancellation-pg-races.php
nala 63d20b10fc
tests / ci (push) Canceled after 0s
commit de armazens
2026-11-04 09:40:26 +02:00

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;
}