[...$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; }