Files
SGF/scripts/a-concurrency-proof.php

40 lines
3.3 KiB
PHP

<?php
use Illuminate\Support\Facades\DB;
use Symfony\Component\Process\Process;
require __DIR__.'/../vendor/autoload.php';
$app = require __DIR__.'/../bootstrap/app.php';
$app->make(\Illuminate\Contracts\Console\Kernel::class)->bootstrap();
\App\Shared\Testing\TestingDatabaseGuard::assertSafe();
$fixture = json_decode(file_get_contents('C:/laragon/data/sgf-tempo/secrets/browser-fixture.json'), true, 32, JSON_THROW_ON_ERROR);
$base = ['actor' => $fixture['manager_id'], 'tenant' => $fixture['tenant'], 'company' => $fixture['company'], 'unit' => $fixture['unit']];
function pair(array $firstInput, array $secondInput): array {
$first = new Process([PHP_BINARY, 'scripts/a-concurrency-worker.php', '--env=testing'], base_path(), null, json_encode($firstInput + ['hold' => true]));
$second = new Process([PHP_BINARY, 'scripts/a-concurrency-worker.php', '--env=testing'], base_path(), null, json_encode($secondInput));
$first->start();
if (! $first->waitUntil(fn ($type, $buffer) => str_contains($buffer, 'LOCK_ACQUIRED'))) { throw new RuntimeException('Could not establish concurrent lock test.'); }
$second->start(); $first->wait(); $second->wait();
$results = [];
foreach ([$first, $second] as $process) {
if (! $process->isSuccessful()) { throw new RuntimeException('Synthetic concurrency process failed.'); }
$lines = array_values(array_filter(explode("\n", trim($process->getOutput())), fn ($line) => str_starts_with($line, '{')));
$results[] = json_decode(end($lines), true, 32, JSON_THROW_ON_ERROR);
}
return $results;
}
$beforeEvents = DB::table('a_events')->count();
$key = (string) \Illuminate\Support\Str::uuid();
$input = $base + ['data' => ['name' => 'Cliente sintético de concorrência'], 'key' => $key];
[$first, $second] = pair($input, $input);
if ($first['status'] !== 200 || $second['status'] !== 200 || $first['result']['id'] !== $second['result']['id'] || ! $second['result']['replayed'] || DB::table('a_events')->count() !== $beforeEvents + 1) { throw new RuntimeException('Concurrent repetition was not deduplicated.'); }
$id = $first['result']['id'];
[$edit, $conflict] = pair($base + ['id' => $id, 'data' => ['name' => 'Versão concorrente preservada', 'version' => 1], 'key' => (string) \Illuminate\Support\Str::uuid()],
$base + ['id' => $id, 'data' => ['name' => 'Escrita antiga recusada', 'version' => 1], 'key' => (string) \Illuminate\Support\Str::uuid()]);
if ($edit['status'] !== 200 || $conflict['status'] !== 409 || DB::table('a_events')->count() !== $beforeEvents + 2 || DB::table('a_clients')->where('id', $id)->value('name') !== 'Versão concorrente preservada') { throw new RuntimeException('Concurrent version conflict was not protected.'); }
$report = ['environment' => 'sgf_tempo_test only', 'parallel_php_processes' => true, 'company_lock_overlap' => true,
'identical_create' => ['same_record' => true, 'second_replayed' => true, 'events_created' => 1],
'concurrent_edit' => ['first_status' => 200, 'second_status' => 409, 'version_after' => 2, 'events_created' => 1]];
file_put_contents(base_path('docs/proofs/a-concurrency.json'), json_encode($report, JSON_PRETTY_PRINT|JSON_THROW_ON_ERROR));
echo "Concurrent PHP/PostgreSQL proof passed: one repeated creation, one version-conflicting edit, no duplicated events.\n";