This commit is contained in:
2026-11-02 20:29:34 +02:00
parent 340f3bb5fa
commit fcf31e8d48
181 changed files with 33383 additions and 51 deletions
@@ -0,0 +1,87 @@
<?php
namespace Tests\Benchmarks;
use Illuminate\Support\Facades\DB;
use Tests\Support\IdentityFixture;
require_once __DIR__.'/ModuleAssociationConcurrencyTest.php';
final class ModuleAssociationHttpLockTest extends ModuleAssociationConcurrencyTest
{
public function test_synthetic_volume_and_query_plans(): void
{
$run = (string) getenv('SGF_ISOLATED_REGRESSION_RUN');
$this->assertMatchesRegularExpression('/^[a-f0-9]{12}$/D', $run);
$identity = DB::selectOne("SELECT current_database() AS database,current_user AS role,inet_server_port() AS port,current_setting('cluster_name') AS cluster");
$this->assertSame('sgf_tempo_test', $identity->database); $this->assertSame('sgf_test_runner', $identity->role);
$this->assertSame('sgf_modular_regression_'.$run, $identity->cluster); $this->assertSame(55439, (int) $identity->port);
$tag = bin2hex(random_bytes(6)); $this->directory = base_path('../private/association-http-lock-'.$tag); mkdir($this->directory, 0700, true);
$before = $this->baseline(); $scope = IdentityFixture::create('-http-lock-'.$tag);
foreach (['clients.view', 'clients.edit'] as $permission) IdentityFixture::grant($scope['manager']->id, $scope['tenant'], $scope['company'], $permission);
$cases = ['confirm_rollback', 'confirm_commit', 'preview_rollback']; $ids = [];
foreach ($cases as $name) $ids[$name] = DB::table('a_clients')->insertGetId(['tenant_id' => $scope['tenant'], 'company_id' => $scope['company'], 'unit_id' => null,
'name' => 'Synthetic HTTP lock '.$name, 'version' => 1, 'data' => '{}', 'created_at' => now(), 'updated_at' => now()]);
$this->actingAs($scope['manager'])->withSession(['auth_version' => $scope['manager']->auth_version]);
$context = $this->postJson('/api/context', ['tenant_id' => $scope['tenant'], 'company_id' => $scope['company'], 'unit_id' => $scope['unit'], 'module_id' => null])->assertOk()->json('context.token');
$this->withHeader('X-SGF-Context', $context); DB::commit();
file_put_contents($this->directory.'/manifest.json', json_encode(['tenant' => $scope['tenant'], 'company' => $scope['company'], 'clients' => $ids], JSON_THROW_ON_ERROR));
$report = ['identity' => $identity, 'run_tag' => $tag, 'source_sha256' => hash_file('sha256', base_path('app/Modules/Identity/Services/ModuleAssociationReview.php')),
'controller_sha256' => hash_file('sha256', base_path('app/Modules/Identity/Http/ModuleRecordsController.php')), 'cases' => []];
try {
foreach ($cases as $number => $name) {
$id = $ids[$name]; $input = ['area' => 'clients', 'id' => $id, 'version' => 1, 'module_id' => 'commercial', 'operation' => 'associate', 'reason' => 'Synthetic HTTP lock recovery'];
$first = $this->postJson('/api/module-records/preview', $input)->assertOk()->json('review_token');
$second = $this->postJson('/api/module-records/preview', $input)->assertOk()->json('review_token');
DB::beginTransaction();
try {
$this->postJson('/api/module-records', ['review_token' => $first])->assertOk();
$gate = $this->directory.'/go-'.$number;
$application = 'sgf_assoc_http_'.$tag.'_'.$number;
$parent = (int) DB::selectOne('SELECT pg_backend_pid() AS pid')->pid;
$job = $this->launch(['mode' => $name === 'preview_rollback' ? 'preview_http' : 'confirm_http', 'lock_timeout_ms' => 1000,
'actor' => $scope['manager']->id, 'tenant' => $scope['tenant'], 'company' => $scope['company'], 'unit' => $scope['unit'],
'context' => $context, 'review_token' => $second, 'preview_input' => $input, 'go' => $gate, 'application_name' => $application]);
$this->ready([$job]); file_put_contents($gate, 'go');
$deadline = microtime(true) + 3; $wait = null;
do {
DB::select('SELECT pg_stat_clear_snapshot()');
$wait = DB::selectOne("SELECT wait_event,to_json(pg_blocking_pids(pid)) AS blockers FROM pg_stat_activity WHERE application_name=? AND wait_event_type='Lock'", [$application]);
if (!$wait) usleep(10000);
} while (!$wait && microtime(true) < $deadline);
$this->assertNotNull($wait); $this->assertContains($parent, json_decode($wait->blockers, true));
$result = $this->collect($job)['confirmation'];
$this->assertSame(423, $result['http_status']); $this->assertSame('association_busy', $result['code']);
if ($name === 'confirm_commit') DB::commit(); else DB::rollBack();
$count = fn () => DB::table('identity_audit')->where('tenant_id', $scope['tenant'])->where('company_id', $scope['company'])->where('action', 'like', 'module.record.%')
->whereRaw("details::jsonb->>'area'='clients' AND details::jsonb->>'record_id'=?", [(string) $id])->count();
$this->assertSame($name === 'confirm_commit' ? 1 : 0, $count());
if ($name === 'confirm_commit') {
$this->postJson('/api/module-records', ['review_token' => $second])->assertConflict();
$this->postJson('/api/module-records/preview', $input)->assertOk();
$recovery = '409_then_new_preview';
} else {
$this->postJson('/api/module-records/preview', $input)->assertOk();
$event = $this->postJson('/api/module-records', ['review_token' => $second])->assertOk()->assertJsonPath('replayed', false)->json('event_id');
$this->postJson('/api/module-records', ['review_token' => $second])->assertOk()->assertJsonPath('replayed', true)->assertJsonPath('event_id', $event);
$recovery = '200_then_idempotent_replay';
}
$this->assertSame(1, $count());
$report['cases'][] = ['case' => $name, 'wait_event' => $wait->wait_event, 'blocking_parent_verified' => true, 'timeout' => $result,
'recovery' => $recovery, 'decision_events_after_retry' => $count(), 'duplicate_decision' => false];
fwrite(STDOUT, "\nHTTP LOCK $name status=423 recovery=$recovery events=1\n");
} finally { while (DB::transactionLevel() > 0) DB::rollBack(); }
}
$report['completed'] = true;
} finally {
foreach ($this->jobs as $job) if (is_resource($job['process'])) { proc_terminate($job['process']); proc_close($job['process']); }
while (DB::transactionLevel() > 0) DB::rollBack();
$report['previous_qa_rows_and_index_catalog_preserved'] = $this->baseline($before) === $before;
$this->assertTrue($report['previous_qa_rows_and_index_catalog_preserved']);
$output = getenv('SGF_ASSOCIATION_LOAD_OUTPUT');
if (!$output || realpath(dirname($output)) !== realpath(base_path('..'))) throw new \RuntimeException('Unexpected report directory');
file_put_contents($output, json_encode($report, JSON_PRETTY_PRINT | JSON_THROW_ON_ERROR).PHP_EOL);
DB::beginTransaction();
}
}
}