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,74 @@
<?php
namespace Tests\Benchmarks;
use App\Modules\Identity\Http\ModuleRecordsController;
use Illuminate\Support\Facades\DB;
require_once __DIR__.'/ModuleAssociationCompareTest.php';
final class ModuleAssociationAccessParityTest extends ModuleAssociationLoadTest
{
public function test_synthetic_volume_and_query_plans(): void
{
$run = (string) getenv('SGF_ISOLATED_REGRESSION_RUN');
$this->assertMatchesRegularExpression('/^[a-f0-9]{12}$/D', $run);
$id = 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', $id->database); $this->assertSame('sgf_test_runner', $id->role);
$this->assertSame('sgf_modular_regression_'.$run, $id->cluster); $this->assertSame(55439, (int) $id->port);
require_once base_path('../baseline-review.php');
config(['app.debug' => false]);
$f = $this->fixture(1000); $actor = $f['identity']; $report = ['date' => '2026-11-02', 'variants' => []];
foreach ([false, true] as $indexed) {
DB::beginTransaction();
try {
if ($indexed) {
DB::statement("CREATE INDEX sgf_qa_assoc_links ON identity_audit (tenant_id,company_id,(details->>'area'),(details->>'record_id'),(details->>'module_id'),id DESC) WHERE action IN ('module.record.associated','module.record.disassociated')");
DB::statement("CREATE INDEX sgf_qa_assoc_revision ON identity_audit (tenant_id,company_id,(details->>'area'),(details->>'record_id'),id DESC) WHERE action IN ('module.record.associated','module.record.disassociated','module.record.reviewed')");
}
$checks = [];
foreach (['reader', 'limited'] as $profile) {
$this->selectActor($actor, $profile);
foreach (['baseline', 'batch'] as $mode) { $this->useController($mode); $this->getJson('/api/module-records')->assertForbidden(); }
$checks[$profile] = 403;
}
$token = $this->selectActor($actor, 'manager');
foreach (['baseline', 'batch'] as $mode) {
$this->useController($mode);
$this->getJson('/api/module-records?unit_id='.$actor['hidden'])->assertForbidden();
$this->getJson('/api/module-records?per_page=101')->assertUnprocessable();
$this->withHeader('X-SGF-Context', 'obsolete')->getJson('/api/module-records')->assertConflict();
$this->withHeader('X-SGF-Context', $token);
}
$checks += ['unselected_unit' => 403, 'invalid_page_size' => 422, 'old_generation' => 409];
DB::table('permission_grants')->where('user_id', $actor['manager']->id)->where('company_id', $actor['company'])->where('permission', 'services.view')->delete();
$hashes = [];
foreach (['baseline', 'batch'] as $mode) {
$this->useController($mode);
$r = $this->getJson('/api/module-records?area=billing_drafts&per_page=25')->assertOk();
foreach ($r->json('records.billing_drafts') as $row) { $this->assertTrue($row['dependencies_restricted']); $this->assertSame([], $row['dependencies']); }
$hashes[] = hash('sha256', $r->getContent());
}
$this->assertSame($hashes[0], $hashes[1]);
$report['variants'][] = ['experimental_indexes' => $indexed, 'refusals_equal' => $checks, 'revoked_dependency_response_identical' => true, 'response_sha256' => $hashes[0]];
} finally { DB::rollBack(); }
}
$this->assertSame(0, (int) DB::selectOne("SELECT count(*) AS n FROM pg_indexes WHERE schemaname='public' AND indexname LIKE 'sgf_qa_assoc_%'")->n);
$report['indexes_rolled_back'] = true;
$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);
}
private function selectActor(array $f, string $profile): string
{
$user = $f[$profile]; $this->actingAs($user)->withSession(['auth_version' => $user->auth_version]);
$token = $this->postJson('/api/context', ['tenant_id' => $f['tenant'], 'company_id' => $f['company'], 'unit_id' => $f['unit'], 'module_id' => null])->assertOk()->json('context.token');
$this->withHeader('X-SGF-Context', $token); return $token;
}
private function useController(string $mode): void
{
app()->bind(ModuleRecordsController::class, fn () => $mode === 'baseline' ? new BaselineAssociationController() : new ModuleRecordsController());
foreach (app('router')->getRoutes() as $route) if ($route->uri() === 'api/module-records') $route->flushController();
}
}
@@ -0,0 +1,160 @@
<?php
namespace Tests\Benchmarks;
use App\Modules\Identity\Http\ModuleRecordsController;
use App\Modules\Identity\Services\BaselineModuleAssociationReview;
use Illuminate\Database\Events\QueryExecuted;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\DB;
require_once __DIR__.'/ModuleAssociationLoadTest.php';
final class BaselineAssociationController
{
public function index(Request $r) { return response()->json(app(BaselineModuleAssociationReview::class)->listing($r))->header('Cache-Control', 'no-store'); }
}
final class ModuleAssociationCompareTest extends ModuleAssociationLoadTest
{
private ?array $metrics = null;
private array $sql = [];
private int $start = 0;
// Overrides the old test: only this paired experiment is executed.
public function test_synthetic_volume_and_query_plans(): void
{
$this->assertSame('8.4.27', PHP_VERSION);
$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);
require_once base_path('../baseline-review.php');
$this->withoutExceptionHandling();
DB::listen(function (QueryExecuted $q) {
if ($this->metrics === null) return;
if (!preg_match('/^\s*(select|with)\b/i', $q->sql)) throw new \RuntimeException('READ_WORKLOAD_WROTE_SQL');
$this->metrics['queries']++; $this->metrics['database_ms'] += $q->time;
$key = hash('sha256', $q->sql);
$entry = $this->sql[$key] ?? ['sql' => $q->sql, 'bindings' => $q->bindings, 'count' => 0, 'database_ms' => 0.0];
$entry['count']++; $entry['database_ms'] += $q->time; $this->sql[$key] = $entry;
if ((hrtime(true) - $this->start) / 1e6 > 15000 || $this->metrics['queries'] > 2500) throw new \RuntimeException('CASE_BUDGET_REACHED');
});
$report = ['date' => '2026-11-02', 'identity' => $identity, 'baseline_checkpoint' => '340f3bb', 'baseline_sha256' => getenv('SGF_ASSOCIATION_BASELINE_SHA'), 'optimized_sha256' => getenv('SGF_ASSOCIATION_OPTIMIZED_SHA'),
'method' => 'Same immutable fixture per scale; baseline/batch requests alternate order across three repetitions; indexed batch uses the identical fixture after index creation in a savepoint. Warm kernel/buffers, no concurrent capacity claim.',
'bounds' => ['request_ms' => 15000, 'statement_ms' => 3000, 'queries' => 2500], 'datasets' => []];
$initial = $this->fingerprints();
try {
foreach ([1000, 5000, 10000] as $n) {
DB::beginTransaction();
try {
$f = $this->fixture($n, true); $actor = $f['identity'];
$this->actingAs($actor['manager'])->withSession(['auth_version' => $actor['manager']->auth_version]);
$token = $this->postJson('/api/context', ['tenant_id' => $actor['tenant'], 'company_id' => $actor['company'], 'unit_id' => $actor['unit'], 'module_id' => null])->assertOk()->json('context.token');
$this->withHeader('X-SGF-Context', $token);
$dataset = ['records_per_area' => $n, 'events' => DB::table('identity_audit')->where('tenant_id', $actor['tenant'])->where('action', 'like', 'module.record.%')->count(), 'cases' => []];
$cases = [];
foreach (array_keys($f['ids']) as $area) foreach ([25, 100] as $size) $cases[] = [$area, 'all', $size];
foreach (['pending', 'associated', 'removed'] as $state) $cases[] = ['clients', $state, 25];
foreach ($cases as [$area, $state, $size]) {
$expected = array_values(array_filter($f['ids'][$area], fn ($id) => match ($state) { 'pending' => $id % 3 === 0, 'associated' => $id % 3 === 1, 'removed' => $id % 3 === 2, default => true }));
$case = ['area' => $area, 'state' => $state, 'page_size' => $size, 'variants' => ['baseline' => [], 'batch' => [], 'indexed_batch' => []]];
foreach (['baseline', 'batch'] as $mode) { $this->mode($mode); $this->getJson('/api/module-records?area='.$area.'&per_page=1')->assertOk(); }
for ($i = 0; $i < 3; $i++) foreach ($i % 2 ? ['batch', 'baseline'] : ['baseline', 'batch'] as $mode) {
$prior = $case['variants'][$mode];
if ($prior && end($prior)['status'] !== 'ok') continue;
$this->mode($mode); $case['variants'][$mode][] = $this->sample($area, $state, $size, $expected);
}
$dataset['cases'][] = $case;
fwrite(STDOUT, "\nPAIR $n $area/$state/$size baseline=".$this->brief($case['variants']['baseline']).' batch='.$this->brief($case['variants']['batch'])."\n");
}
DB::beginTransaction();
try {
$dataset['experimental_indexes'] = [
"CREATE INDEX sgf_qa_assoc_links ON identity_audit (tenant_id,company_id,(details->>'area'),(details->>'record_id'),(details->>'module_id'),id DESC) WHERE action IN ('module.record.associated','module.record.disassociated')",
"CREATE INDEX sgf_qa_assoc_revision ON identity_audit (tenant_id,company_id,(details->>'area'),(details->>'record_id'),id DESC) WHERE action IN ('module.record.associated','module.record.disassociated','module.record.reviewed')",
];
DB::statement("SET LOCAL statement_timeout = '30s'");
foreach ($dataset['experimental_indexes'] as $ddl) DB::statement($ddl);
DB::statement('ANALYZE identity_audit');
$this->mode('indexed_batch');
foreach ($dataset['cases'] as &$case) {
$area = $case['area']; $state = $case['state']; $size = $case['page_size'];
$expected = array_values(array_filter($f['ids'][$area], fn ($id) => match ($state) { 'pending' => $id % 3 === 0, 'associated' => $id % 3 === 1, 'removed' => $id % 3 === 2, default => true }));
$this->getJson('/api/module-records?area='.$area.'&per_page=1')->assertOk();
for ($i = 0; $i < 3; $i++) { $sample = $this->sample($area, $state, $size, $expected); $case['variants']['indexed_batch'][] = $sample; if ($sample['status'] !== 'ok') break; }
$hashes = [];
foreach ($case['variants'] as $samples) foreach ($samples as $sample) if ($sample['status'] === 'ok') $hashes[] = $sample['response_sha256'];
$this->assertCount(1, array_unique($hashes), 'Successful variants must return byte-identical JSON');
$case['successful_responses_identical'] = true;
foreach ($case['variants'] as $mode => $samples) $case['summary'][$mode] = $this->summarize($samples);
fwrite(STDOUT, "\nINDEX $n $area/$state/$size ".$this->brief($case['variants']['indexed_batch'])."\n");
}
unset($case);
} finally { $this->metrics = null; DB::rollBack(); }
$this->assertSame(0, (int) DB::selectOne("SELECT count(*) AS n FROM pg_indexes WHERE schemaname='public' AND indexname LIKE 'sgf_qa_assoc_%'")->n);
$dataset['experimental_indexes_rolled_back'] = true;
$report['datasets'][] = $dataset;
} finally { $this->metrics = null; $this->mode('batch'); DB::rollBack(); }
$this->assertSame($initial, $this->fingerprints(), 'Committed records must survive fixture rollback unchanged');
}
$report['committed_data_and_index_catalog_preserved'] = true; $report['completed'] = true;
} finally {
$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);
}
}
private function mode(string $mode): void
{
app()->bind(ModuleRecordsController::class, fn () => $mode === 'baseline' ? new BaselineAssociationController() : new ModuleRecordsController());
foreach (app('router')->getRoutes() as $route) if ($route->uri() === 'api/module-records') $route->flushController();
}
private function sample(string $area, string $state, int $size, array $expected): array
{
DB::beginTransaction(); DB::statement("SET LOCAL statement_timeout = '3s'");
gc_collect_cycles();
$this->sql = []; $this->metrics = ['queries' => 0, 'database_ms' => 0.0];
memory_reset_peak_usage(); $base = memory_get_usage(false); $this->start = hrtime(true); $status = 'ok'; $extra = [];
try {
$r = $this->getJson('/api/module-records?'.http_build_query(['area' => $area, 'state' => $state, 'per_page' => $size]));
$elapsed = (hrtime(true) - $this->start) / 1e6; $stats = $this->metrics; $this->metrics = null;
$peak = memory_get_peak_usage(false); $allocated = memory_get_peak_usage(true);
$r->assertOk()->assertJsonPath('pagination.total', count($expected));
$rows = $r->json('records.'.$area); $this->assertSame(array_slice($expected, 0, $size), array_column($rows, 'id'));
foreach ($rows as $row) { $this->assertSame($row['id'] % 3 === 1 ? ['commercial'] : [], $row['modules']); $this->assertFalse($row['dependencies_restricted']); }
$extra = ['response_sha256' => hash('sha256', $r->getContent()), 'response_bytes' => strlen($r->getContent()), 'rows' => count($rows)];
} catch (\Illuminate\Database\QueryException $e) { $status = 'sql_timeout_or_error'; $extra = ['sqlstate' => $e->errorInfo[0] ?? $e->getCode()];
} catch (\RuntimeException $e) { if ($e->getMessage() !== 'CASE_BUDGET_REACHED') throw $e; $status = 'case_budget_reached';
} finally {
$stats ??= $this->metrics; $elapsed ??= (hrtime(true) - $this->start) / 1e6;
$peak ??= memory_get_peak_usage(false); $allocated ??= memory_get_peak_usage(true);
$this->metrics = null; DB::rollBack();
}
return ['status' => $status, 'elapsed_ms' => round($elapsed, 3), 'queries' => $stats['queries'], 'database_ms' => round($stats['database_ms'], 3),
'peak_php_used_bytes' => $peak, 'incremental_php_used_bytes' => max(0, $peak - $base), 'peak_php_allocated_bytes' => $allocated, ...$extra];
}
private function summarize(array $samples): array
{
$ok = array_values(array_filter($samples, fn ($s) => $s['status'] === 'ok')); $times = array_column($ok, 'elapsed_ms'); sort($times);
return ['completed_samples' => count($ok), 'median_ms' => count($ok) === 3 ? $times[1] : null, 'queries' => $ok[0]['queries'] ?? null,
'peak_php_used_bytes' => $ok ? max(array_column($ok, 'peak_php_used_bytes')) : null,
'incremental_php_used_bytes' => $ok ? max(array_column($ok, 'incremental_php_used_bytes')) : null,
'censored' => count($ok) !== 3, 'last_observed_ms' => end($samples)['elapsed_ms']];
}
private function brief(array $samples): string { $s = $this->summarize($samples); return $s['censored'] ? 'bounded@'.$s['last_observed_ms'] : $s['median_ms'].'ms'; }
private function fingerprints(): array
{
$out = [];
foreach (['a_clients', 'a_services', 'a_service_drafts', 'a_billing_drafts', 'identity_audit', 'users', 'permission_grants', 'company_memberships'] as $table) {
$out[$table] = (array) DB::selectOne("SELECT count(*) AS rows,md5(coalesce(string_agg(row_to_json(t)::text,E'\\n' ORDER BY row_to_json(t)::text),'')) AS hash FROM {$table} t");
}
$out['indexes'] = json_encode(DB::select("SELECT indexname,indexdef FROM pg_indexes WHERE schemaname='public' ORDER BY indexname"), JSON_THROW_ON_ERROR);
return $out;
}
}
@@ -0,0 +1,177 @@
<?php
namespace Tests\Benchmarks;
use App\Models\User;
use Illuminate\Support\Facades\DB;
use Tests\Support\IdentityFixture;
require_once __DIR__.'/ModuleAssociationLoadTest.php';
class ModuleAssociationConcurrencyTest extends ModuleAssociationLoadTest
{
protected array $jobs = [];
protected string $directory;
protected int $serial = 0;
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)); $index = 'sgf_qa_read_revision_'.$tag;
$this->directory = base_path('../private/association-concurrency-'.$tag); mkdir($this->directory, 0700, true);
$before = $this->baseline();
$this->assertSame(0, (int) DB::selectOne("SELECT count(*) AS n FROM pg_indexes WHERE schemaname='public' AND tablename='identity_audit' AND indexname <> 'identity_audit_pkey'")->n, 'Unindexed baseline must be explicit');
$report = ['date' => '2026-11-02', 'run_tag' => $tag, 'identity' => $identity, 'optimized_sha256' => hash_file('sha256', base_path('app/Modules/Identity/Services/ModuleAssociationReview.php')),
'method' => 'Independent PHP processes and synthetic identities, pre-authenticated in-process HTTP kernels, synchronized starts, closed loop of five reads per worker, three waves; no external HTTP/login/browser capacity claim.',
'fixture_policy' => 'New synthetic scope committed only in regression DB for subprocess visibility; retained with private manifest. Existing rows preserved, not reset or deleted.', 'phases' => []];
$f = $this->fixture(3000, true); $scope = $f['identity'];
$fourth = User::create(['name' => 'Consulta carga sintética 4', 'email' => 'assoc-load-'.$tag.'@example.test', 'password' => bin2hex(random_bytes(24))]);
DB::table('company_memberships')->insert(['user_id' => $fourth->id, 'tenant_id' => $scope['tenant'], 'company_id' => $scope['company']]);
$actors = [$scope['manager']->id, $scope['reader']->id, $scope['limited']->id, $fourth->id];
foreach ($actors as $actor) {
foreach (['company.view', 'access.manage'] as $permission) IdentityFixture::grant($actor, $scope['tenant'], $scope['company'], $permission);
IdentityFixture::grant($actor, $scope['tenant'], $scope['company'], 'unit.view', $scope['unit']);
foreach (['clients', 'services', 'service_drafts', 'billing_drafts'] as $area) IdentityFixture::grant($actor, $scope['tenant'], $scope['company'], "$area.view", str_ends_with($area, '_drafts') ? $scope['unit'] : null);
}
IdentityFixture::grant($actors[0], $scope['tenant'], $scope['company'], 'clients.edit');
for ($i = 0; $i < 25; $i++) {
$lines = []; foreach (array_slice($f['ids']['services'], $i * 100, 100) as $id) $lines[] = ['service_id' => $id, 'quantity' => '1', 'unit_price' => '10', 'description' => str_repeat('x', 1000)];
$data = ['currency' => 'MZN', 'tax_status' => 'pending', 'accounting_status' => 'not_processed', 'lines' => $lines];
DB::table('a_service_drafts')->where('id', $f['ids']['service_drafts'][$i])->update(['version' => 2, 'data' => json_encode($data)]);
$data['source_version'] = 2; DB::table('a_billing_drafts')->where('id', $f['ids']['billing_drafts'][$i])->update(['version' => 2, 'data' => json_encode($data)]);
}
foreach (['a_service_drafts', 'a_billing_drafts', 'identity_audit'] as $table) DB::statement('ANALYZE '.$table);
$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);
$workloads = [];
foreach (['clients', 'billing_drafts'] as $area) {
$url = '/api/module-records?area='.$area.'&per_page=25'; $r = $this->getJson($url)->assertOk()->assertJsonPath('pagination.total', 3000);
$workloads[$area] = ['url' => $url, 'total' => 3000, 'response_sha256' => hash('sha256', $r->getContent())];
}
// Separate processes cannot see uncommitted rows. Only this new QA scope is committed.
DB::commit();
file_put_contents($this->directory.'/manifest.json', json_encode(['synthetic_only' => true, 'tenant' => $scope['tenant'], 'company' => $scope['company'], 'unit' => $scope['unit'], 'actors' => $actors, 'workloads' => $workloads, 'index_name' => $index], JSON_PRETTY_PRINT | JSON_THROW_ON_ERROR));
$created = false;
try {
foreach (['none', 'revision'] as $mode) {
if ($mode === 'revision') {
$this->assertNull(DB::selectOne('SELECT to_regclass(?) AS value', [$index])->value);
DB::statement("CREATE INDEX $index ON identity_audit (tenant_id,company_id,(details->>'area'),(details->>'record_id'),id DESC) WHERE action IN ('module.record.associated','module.record.disassociated','module.record.reviewed')"); $created = true;
DB::statement('ANALYZE identity_audit');
}
$phase = ['index_mode' => $mode, 'waves' => []];
foreach ($workloads as $label => $workload) foreach ([1, 2, 4] as $count) for ($wave = 0; $wave < 3; $wave++) {
$phase['waves'][] = ['workload' => $label, 'configured_workers' => $count, ...$this->wave($scope, array_slice($actors, 0, $count), $workload, 5, $tag)];
fwrite(STDOUT, "\nCONCURRENT $mode $label workers=$count wave=".($wave + 1)." complete\n");
}
$report['phases'][] = $phase;
}
$candidate = array_values(array_filter($f['ids']['clients'], fn ($id) => $id % 3 === 0))[0];
$input = ['area' => 'clients', 'id' => $candidate, 'version' => 1, 'operation' => 'associate', 'module_id' => 'commercial', 'reason' => 'Synthetic concurrency rollback only'];
$first = $this->postJson('/api/module-records/preview', $input)->assertOk()->json('review_token');
$second = $this->postJson('/api/module-records/preview', $input)->assertOk()->json('review_token');
$events = DB::table('identity_audit')->where('tenant_id', $scope['tenant'])->where('company_id', $scope['company'])->where('action', 'like', 'module.record.%')->count();
DB::beginTransaction();
try {
$this->postJson('/api/module-records', ['review_token' => $first])->assertOk();
$report['mixed_read_wave'] = $this->wave($scope, $actors, $workloads['clients'], 5, $tag);
$gate = $this->directory.'/confirm-go';
$job = $this->launch(['mode' => 'confirm', 'actor' => $actors[0], 'tenant' => $scope['tenant'], 'company' => $scope['company'], 'unit' => $scope['unit'], 'context' => $context,
'review_token' => $second, 'go' => $gate, 'application_name' => 'sgf_assoc_confirm_'.$tag]);
$this->ready([$job]); file_put_contents($gate, 'go'); $observed = false; $deadline = microtime(true) + 3;
do { DB::select('SELECT pg_stat_clear_snapshot()'); $n = DB::selectOne("SELECT count(*) AS n FROM pg_stat_activity WHERE application_name=? AND wait_event_type='Lock'", ['sgf_assoc_confirm_'.$tag])->n; if ((int) $n > 0) { $observed = true; break; } usleep(25000); } while (microtime(true) < $deadline);
$result = $this->collect($job); $this->assertTrue($observed, 'Actual PostgreSQL lock wait must be observed');
$this->assertSame('55P03', $result['confirmation']['sqlstate'] ?? null);
$report['competing_confirmation'] = ['actual_lock_wait_observed' => $observed, ...$result['confirmation']];
} finally { DB::rollBack(); }
$this->assertSame($events, DB::table('identity_audit')->where('tenant_id', $scope['tenant'])->where('company_id', $scope['company'])->where('action', 'like', 'module.record.%')->count());
$report['synthetic_confirmation_rolled_back'] = 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();
if ($created) DB::statement('DROP INDEX '.$index);
$this->assertNull(DB::selectOne('SELECT to_regclass(?) AS value', [$index])->value);
$report['test_index_removed'] = true;
$report['previous_rows_unchanged'] = $this->baseline($before) === $before;
$this->assertTrue($report['previous_rows_unchanged']);
$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);
// Preserve the transaction-based TestCase lifecycle without undoing committed QA setup.
DB::beginTransaction();
}
}
private function wave(array $scope, array $actors, array $workload, int $requests, string $tag): array
{
$gate = $this->directory.'/go-'.(++$this->serial); $jobs = []; $startup = microtime(true);
foreach ($actors as $i => $actor) $jobs[] = $this->launch(['mode' => 'read', 'actor' => $actor, 'tenant' => $scope['tenant'], 'company' => $scope['company'], 'unit' => $scope['unit'],
...$workload, 'requests' => $requests, 'go' => $gate, 'application_name' => 'sgf_assoc_read_'.$tag.'_'.$i]);
$this->ready($jobs); $ready = microtime(true); file_put_contents($gate, 'go');
$samples = []; $finished = $ready;
foreach ($jobs as $job) { $result = $this->collect($job); $samples = [...$samples, ...$result['requests']]; $finished = max($finished, $result['finished_at']); }
$ok = array_values(array_filter($samples, fn ($s) => $s['status'] === 'ok')); $events = [];
foreach ($ok as $s) { $events[] = [$s['started_at'], 1]; $events[] = [$s['finished_at'], -1]; }
usort($events, fn ($a, $b) => ($a[0] <=> $b[0]) ?: ($a[1] <=> $b[1])); $active = 0; $max = 0;
foreach ($events as $event) { $active += $event[1]; $max = max($max, $active); }
$this->assertLessThanOrEqual(count($actors), $max); $this->assertGreaterThanOrEqual(1, $max);
return ['startup_to_ready_ms' => round(($ready - $startup) * 1000, 3), 'measurement_window_ms' => round(($finished - $ready) * 1000, 3), 'observed_max_inflight' => $max,
'complete_requests' => count($ok), 'limited_requests' => count($samples) - count($ok), 'samples' => $samples];
}
protected function launch(array $input): array
{
$stem = $this->directory.'/worker-'.(++$this->serial);
$input += ['ready' => $stem.'.ready', 'result' => $stem.'.json'];
file_put_contents($stem.'.input.json', json_encode($input, JSON_THROW_ON_ERROR));
$env = getenv(); $env['SGF_ASSOCIATION_WORKER_INPUT'] = $stem.'.input.json';
$pipes = []; $process = proc_open([PHP_BINARY, 'vendor/phpunit/phpunit/phpunit', 'scripts/benchmarks/ModuleAssociationReadWorkerTest.php', '--no-progress'],
[0 => ['pipe', 'r'], 1 => ['file', $stem.'.stdout', 'w'], 2 => ['file', $stem.'.stderr', 'w']], $pipes, base_path(), $env, ['bypass_shell' => true]);
$this->assertIsResource($process); fclose($pipes[0]);
$job = ['process' => $process, 'input' => $input, 'stdout' => $stem.'.stdout', 'stderr' => $stem.'.stderr']; $this->jobs[] = $job; return $job;
}
protected function ready(array $jobs): void
{
$deadline = microtime(true) + 30;
do {
$all = true;
foreach ($jobs as $job) if (!is_file($job['input']['ready'])) { $all = false; if (!proc_get_status($job['process'])['running']) throw new \RuntimeException('Worker failed before ready: '.file_get_contents($job['stdout']).file_get_contents($job['stderr'])); }
if ($all) return; usleep(10000);
} while (microtime(true) < $deadline);
throw new \RuntimeException('Workers did not reach barrier');
}
protected function collect(array $job): array
{
$deadline = microtime(true) + 60;
do { $status = proc_get_status($job['process']); if (!$status['running']) break; usleep(10000); } while (microtime(true) < $deadline);
if ($status['running']) throw new \RuntimeException('Worker deadline reached');
$exit = proc_close($job['process']); if ($exit === -1) $exit = $status['exitcode'];
$this->assertSame(0, $exit, file_get_contents($job['stdout']).file_get_contents($job['stderr']));
return json_decode(file_get_contents($job['input']['result']), true, 512, JSON_THROW_ON_ERROR);
}
protected function baseline(?array $old = null): array
{
$out = [];
foreach (['a_clients', 'a_services', 'a_service_drafts', 'a_billing_drafts', 'identity_audit', 'users', 'permission_grants', 'company_memberships'] as $table) {
$key = $table === 'company_memberships' ? 'user_id' : 'id';
$q = DB::table($table);
// IDs are captured integer primary keys, not user input. Avoid the
// PostgreSQL bind-parameter ceiling in accumulated QA fixtures.
if ($old !== null) $q->whereIntegerInRaw($key, $old[$table]['ids']);
$q->orderBy($key);
if ($table === 'company_memberships') $q->orderBy('tenant_id')->orderBy('company_id');
$rows = $q->get();
$out[$table] = ['ids' => $old[$table]['ids'] ?? $rows->pluck($key)->unique()->values()->all(), 'hash' => hash('sha256', $rows->toJson())];
}
$out['indexes'] = json_encode(DB::select("SELECT indexname,indexdef FROM pg_indexes WHERE schemaname='public' ORDER BY indexname"), JSON_THROW_ON_ERROR);
return $out;
}
}
@@ -0,0 +1,185 @@
<?php
namespace Tests\Benchmarks;
use Illuminate\Database\Events\QueryExecuted;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\DB;
use App\Modules\Identity\Http\ModuleRecordsController;
use App\Modules\Identity\Services\PreProjectionModuleAssociationReview;
require_once __DIR__.'/ModuleAssociationLoadTest.php';
final class PreProjectionAssociationController
{
public function index(Request $r) { return response()->json(app(PreProjectionModuleAssociationReview::class)->listing($r))->header('Cache-Control', 'no-store'); }
}
final class ModuleAssociationFanoutTest extends ModuleAssociationLoadTest
{
private ?array $metrics = null;
private ?array $plan = null;
private int $start = 0;
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);
$this->assertSame('8.4.27', PHP_VERSION);
require_once base_path('../pre-projection-review.php');
$this->withoutExceptionHandling();
$before = $this->fingerprints();
$report = ['date' => '2026-11-02', 'identity' => $identity, 'optimized_sha256' => hash_file('sha256', base_path('app/Modules/Identity/Services/ModuleAssociationReview.php')),
'pre_projection_sha256' => trim(file_get_contents(base_path('../pre-projection-sha256.txt'))),
'php_memory_limit' => ini_get('memory_limit'), 'records_per_area' => 10000, 'lines_per_draft' => 100,
'bounds' => ['request_ms' => 15000, 'statement_ms' => 3000, 'queries' => 250, 'php_used_bytes' => 201326592],
'method' => 'New synthetic fixtures, three sequential warm in-process HTTP samples; same profile across no-index/revision-only/pair phases; no product changes or deployment capacity claim.', 'profiles' => []];
DB::listen(function (QueryExecuted $q) {
if ($this->metrics === null) return;
if (!preg_match('/^\s*(select|with)\b/i', $q->sql)) throw new \RuntimeException('READ_WORKLOAD_WROTE_SQL');
$this->metrics['queries']++; $this->metrics['database_ms'] += $q->time;
if (str_contains($q->sql, 'identity_audit') && ($this->plan === null || str_contains($q->sql, 'from "a_clients"'))) $this->plan = ['sql' => $q->sql, 'bindings' => $q->bindings];
if ((hrtime(true) - $this->start) / 1e6 > 15000 || $this->metrics['queries'] > 250 || memory_get_usage(false) > 201326592) throw new \RuntimeException('DIAGNOSTIC_BUDGET_REACHED');
});
DB::beginTransaction();
try {
$f = $this->fixture(10000, true); $actor = $f['identity'];
$this->actingAs($actor['manager'])->withSession(['auth_version' => $actor['manager']->auth_version]);
$token = $this->postJson('/api/context', ['tenant_id' => $actor['tenant'], 'company_id' => $actor['company'], 'unit_id' => $actor['unit'], 'module_id' => null])->assertOk()->json('context.token');
$this->withHeader('X-SGF-Context', $token);
foreach ([128, 1000] as $descriptionBytes) {
$version = $this->expand($f, $descriptionBytes);
$profile = ['line_description_bytes' => $descriptionBytes, 'draft_version' => $version, 'variants' => []];
$cases = [];
foreach (['service_drafts', 'billing_drafts'] as $area) foreach ([25, 100] as $size) $cases[] = [$area, 'all', $size];
if ($descriptionBytes === 128) foreach (['pending', 'associated', 'removed'] as $state) $cases[] = ['clients', $state, 25];
$hashes = [];
foreach (['pre_projection', 'none', 'revision_only', 'pair'] as $mode) {
$this->controller($mode);
DB::beginTransaction();
try {
$ddl = [];
if (in_array($mode, ['revision_only', 'pair'], true)) $ddl[] = "CREATE INDEX sgf_qa_assoc_revision ON identity_audit (tenant_id,company_id,(details->>'area'),(details->>'record_id'),id DESC) WHERE action IN ('module.record.associated','module.record.disassociated','module.record.reviewed')";
if ($mode === 'pair') $ddl[] = "CREATE INDEX sgf_qa_assoc_links ON identity_audit (tenant_id,company_id,(details->>'area'),(details->>'record_id'),(details->>'module_id'),id DESC) WHERE action IN ('module.record.associated','module.record.disassociated')";
$start = hrtime(true);
foreach ($ddl as $sql) DB::statement($sql);
if ($ddl) DB::statement('ANALYZE identity_audit');
$variant = ['ddl' => $ddl, 'index_build_and_analyze_ms' => round((hrtime(true) - $start) / 1e6, 3), 'index_bytes' => [], 'cases' => []];
foreach (['sgf_qa_assoc_revision', 'sgf_qa_assoc_links'] as $name) {
$bytes = DB::selectOne('SELECT pg_relation_size(to_regclass(?)) AS bytes', [$name])->bytes;
if ($bytes !== null) $variant['index_bytes'][$name] = (int) $bytes;
}
foreach ($cases as [$area, $state, $size]) {
$expected = array_values(array_filter($f['ids'][$area], fn ($id) => match ($state) { 'pending' => $id % 3 === 0, 'associated' => $id % 3 === 1, 'removed' => $id % 3 === 2, default => true }));
$this->getJson('/api/module-records?area='.$area.'&per_page=1')->assertOk();
$case = ['area' => $area, 'state' => $state, 'page_size' => $size, 'samples' => []];
for ($i = 0; $i < 3; $i++) {
$sample = $this->sample($f, $area, $state, $size, $expected, $version);
$case['samples'][] = $sample;
if ($sample['status'] !== 'ok') break;
$hashes["$area/$state/$size"][] = $sample['response_sha256'];
}
$ok = array_values(array_filter($case['samples'], fn ($s) => $s['status'] === 'ok'));
$times = array_column($ok, 'elapsed_ms'); sort($times);
$case['median_ms'] = count($ok) === 3 ? $times[1] : null;
$case['complete'] = count($ok) === 3;
$case['planned_indexes'] = $this->plannedIndexes();
$variant['cases'][] = $case;
fwrite(STDOUT, "\nFANOUT $descriptionBytes $mode $area/$state/$size ".($case['median_ms'] ?? 'bounded')."ms\n");
}
$profile['variants'][$mode] = $variant;
} finally { $this->metrics = null; DB::rollBack(); }
$this->assertSame(0, (int) DB::selectOne("SELECT count(*) AS n FROM pg_indexes WHERE schemaname='public' AND indexname LIKE 'sgf_qa_assoc_%'")->n);
}
foreach ($hashes as $values) $this->assertCount(1, array_unique($values), 'All complete index variants must preserve the same response');
$profile['complete_responses_identical'] = true;
$report['profiles'][] = $profile;
}
} finally {
$this->metrics = null; DB::rollBack();
$report['original_test_data_and_indexes_preserved'] = $before === $this->fingerprints();
$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);
}
$this->assertTrue($report['original_test_data_and_indexes_preserved']);
}
private function controller(string $mode): void
{
app()->bind(ModuleRecordsController::class, fn () => $mode === 'pre_projection' ? new PreProjectionAssociationController() : new ModuleRecordsController());
foreach (app('router')->getRoutes() as $route) if ($route->uri() === 'api/module-records') $route->flushController();
}
private function expand(array $f, int $descriptionBytes): int
{
$versions = DB::table('a_service_drafts')->whereIn('id', array_slice($f['ids']['service_drafts'], 0, 100))->pluck('version', 'id');
$version = (int) $versions[$f['ids']['service_drafts'][0]] + 1;
for ($i = 0; $i < 100; $i++) {
$lines = [];
foreach (array_slice($f['ids']['services'], $i * 100, 100) as $id) $lines[] = ['service_id' => $id, 'quantity' => '1', 'unit_price' => '10', 'description' => str_pad('SYNTHETIC-WIDE-', $descriptionBytes, 'x')];
$data = ['currency' => 'MZN', 'tax_status' => 'pending', 'accounting_status' => 'not_processed', 'lines' => $lines];
DB::table('a_service_drafts')->where('id', $f['ids']['service_drafts'][$i])->update(['version' => $version, 'data' => json_encode($data, JSON_THROW_ON_ERROR), 'updated_at' => now()]);
$data['source_version'] = $version;
DB::table('a_billing_drafts')->where('id', $f['ids']['billing_drafts'][$i])->update(['version' => $version, 'data' => json_encode($data, JSON_THROW_ON_ERROR), 'updated_at' => now()]);
}
foreach (['a_service_drafts', 'a_billing_drafts'] as $table) DB::statement('ANALYZE '.$table);
return $version;
}
private function sample(array $f, string $area, string $state, int $size, array $expected, int $version): array
{
DB::beginTransaction(); DB::statement("SET LOCAL statement_timeout = '3s'"); gc_collect_cycles();
$this->plan = null; $this->metrics = ['queries' => 0, 'database_ms' => 0.0];
memory_reset_peak_usage(); $base = memory_get_usage(false); $this->start = hrtime(true); $extra = []; $status = 'ok';
try {
$r = $this->getJson('/api/module-records?'.http_build_query(['area' => $area, 'state' => $state, 'per_page' => $size]));
$elapsed = (hrtime(true) - $this->start) / 1e6; $stats = $this->metrics; $this->metrics = null;
$peak = memory_get_peak_usage(false); $allocated = memory_get_peak_usage(true);
$r->assertOk()->assertJsonPath('pagination.total', count($expected));
$rows = $r->json('records.'.$area); $this->assertSame(array_slice($expected, 0, $size), array_column($rows, 'id'));
$positions = array_flip(array_slice($f['ids'][$area], 0, 100));
foreach ($rows as $row) {
$this->assertFalse($row['dependencies_restricted']);
if ($area === 'clients') continue;
$this->assertSame($version, $row['version']);
$this->assertCount($area === 'billing_drafts' ? 102 : 101, $row['dependencies']);
$serviceIds = array_column(array_values(array_filter($row['dependencies'], fn ($d) => $d['area'] === 'services')), 'id');
$this->assertSame(array_slice($f['ids']['services'], $positions[$row['id']] * 100, 100), $serviceIds);
}
$this->assertStringNotContainsString('SYNTHETIC-WIDE-', $r->getContent(), 'Document contents must not leak into administrative metadata');
$extra = ['rows' => count($rows), 'response_bytes' => strlen($r->getContent()), 'response_sha256' => hash('sha256', $r->getContent())];
} catch (\Illuminate\Database\QueryException $e) { $status = 'sql_timeout_or_error'; $extra = ['sqlstate' => $e->errorInfo[0] ?? $e->getCode()];
} catch (\RuntimeException $e) { if ($e->getMessage() !== 'DIAGNOSTIC_BUDGET_REACHED') throw $e; $status = 'diagnostic_budget_reached';
} finally {
$elapsed ??= (hrtime(true) - $this->start) / 1e6; $stats ??= $this->metrics; $peak ??= memory_get_peak_usage(false); $allocated ??= memory_get_peak_usage(true);
$this->metrics = null; DB::rollBack();
}
return ['status' => $status, 'elapsed_ms' => round($elapsed, 3), 'queries' => $stats['queries'], 'database_ms' => round($stats['database_ms'], 3),
'incremental_php_used_bytes' => max(0, $peak - $base), 'peak_php_used_bytes' => $peak, 'peak_php_allocated_bytes' => $allocated, ...$extra];
}
private function plannedIndexes(): array
{
if (!$this->plan) return [];
DB::beginTransaction(); DB::statement("SET LOCAL statement_timeout = '3s'");
try {
$raw = DB::select('EXPLAIN (FORMAT JSON) '.$this->plan['sql'], $this->plan['bindings']);
$plan = json_decode($raw[0]->{'QUERY PLAN'}, true, 512, JSON_THROW_ON_ERROR)[0]['Plan']; $names = [];
$visit = function (array $node) use (&$visit, &$names): void { if (isset($node['Index Name'])) $names[] = $node['Index Name']; foreach ($node['Plans'] ?? [] as $child) $visit($child); };
$visit($plan); $visit = null;
return array_values(array_unique($names));
} finally { DB::rollBack(); }
}
private function fingerprints(): array
{
$out = [];
foreach (['a_clients', 'a_services', 'a_service_drafts', 'a_billing_drafts', 'identity_audit', 'users', 'permission_grants', 'company_memberships'] as $table) $out[$table] = (array) DB::selectOne("SELECT count(*) AS rows,md5(coalesce(string_agg(row_to_json(t)::text,E'\\n' ORDER BY row_to_json(t)::text),'')) AS hash FROM {$table} t");
$out['indexes'] = json_encode(DB::select("SELECT indexname,indexdef FROM pg_indexes WHERE schemaname='public' ORDER BY indexname"), JSON_THROW_ON_ERROR);
return $out;
}
}
@@ -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();
}
}
}
@@ -0,0 +1,62 @@
<?php
namespace Tests\Benchmarks;
use Illuminate\Database\Events\QueryExecuted;
use Illuminate\Support\Facades\DB;
require_once __DIR__.'/ModuleAssociationLoadTest.php';
final class ModuleAssociationIndexPlanTest extends ModuleAssociationLoadTest
{
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);
$f = $this->fixture(10000, true); $actor = $f['identity'];
$this->actingAs($actor['manager'])->withSession(['auth_version' => $actor['manager']->auth_version]);
$token = $this->postJson('/api/context', ['tenant_id' => $actor['tenant'], 'company_id' => $actor['company'], 'unit_id' => $actor['unit'], 'module_id' => null])->assertOk()->json('context.token');
$this->withHeader('X-SGF-Context', $token);
$capturing = false; $queries = [];
DB::listen(function (QueryExecuted $q) use (&$capturing, &$queries) { if ($capturing && str_contains($q->sql, 'identity_audit')) $queries[hash('sha256', $q->sql)] = ['sql' => $q->sql, 'bindings' => $q->bindings]; });
$report = ['date' => '2026-11-02', 'identity' => $identity, 'records_per_area' => 10000, 'variants' => []];
foreach (['batch', 'indexed_batch'] as $mode) {
DB::beginTransaction();
try {
if ($mode === 'indexed_batch') {
DB::statement("CREATE INDEX sgf_qa_assoc_links ON identity_audit (tenant_id,company_id,(details->>'area'),(details->>'record_id'),(details->>'module_id'),id DESC) WHERE action IN ('module.record.associated','module.record.disassociated')");
DB::statement("CREATE INDEX sgf_qa_assoc_revision ON identity_audit (tenant_id,company_id,(details->>'area'),(details->>'record_id'),id DESC) WHERE action IN ('module.record.associated','module.record.disassociated','module.record.reviewed')");
DB::statement('ANALYZE identity_audit');
}
$queries = []; $capturing = true;
$this->getJson('/api/module-records?area=billing_drafts&per_page=100')->assertOk();
$this->getJson('/api/module-records?area=clients&state=pending&per_page=25')->assertOk();
$capturing = false;
foreach ($queries as $signature => $q) {
DB::beginTransaction(); DB::statement("SET LOCAL statement_timeout = '3s'");
try {
$raw = DB::select('EXPLAIN (ANALYZE,BUFFERS,FORMAT JSON) '.$q['sql'], $q['bindings']);
$plan = json_decode($raw[0]->{'QUERY PLAN'}, true, 512, JSON_THROW_ON_ERROR)[0]; $nodes = [];
$this->nodesOnly($plan['Plan'], $nodes);
$report['variants'][$mode][] = ['signature' => $signature, 'shape' => $q['sql'], 'execution_ms' => $plan['Execution Time'], 'nodes' => $nodes];
} catch (\Illuminate\Database\QueryException $e) { $report['variants'][$mode][] = ['signature' => $signature, 'status' => 'plan_timeout', 'sqlstate' => $e->errorInfo[0] ?? $e->getCode()];
} finally { DB::rollBack(); }
}
} finally { $capturing = false; DB::rollBack(); }
}
$this->assertSame(0, (int) DB::selectOne("SELECT count(*) AS n FROM pg_indexes WHERE schemaname='public' AND indexname LIKE 'sgf_qa_assoc_%'")->n);
$report['indexes_rolled_back'] = true;
$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);
}
private function nodesOnly(array $node, array &$out): void
{
$out[] = array_intersect_key($node, array_flip(['Node Type', 'Relation Name', 'Index Name', 'Actual Rows', 'Actual Loops', 'Rows Removed by Filter', 'Shared Hit Blocks', 'Shared Read Blocks', 'Temp Read Blocks', 'Temp Written Blocks']));
foreach ($node['Plans'] ?? [] as $child) $this->nodesOnly($child, $out);
}
}
@@ -0,0 +1,192 @@
<?php
namespace Tests\Benchmarks;
use Illuminate\Database\Events\QueryExecuted;
use Illuminate\Foundation\Testing\DatabaseTransactions;
use Illuminate\Support\Facades\DB;
use Tests\Support\IdentityFixture;
use Tests\TestCase;
// Explicit invocation only: outside the default test directories. Never migrate.
class ModuleAssociationLoadTest extends TestCase
{
use DatabaseTransactions;
private ?array $measurement = null;
private array $queries = [];
private int $started = 0;
private array $report = [];
public function test_synthetic_volume_and_query_plans(): void
{
$this->assertSame('8.4.27', PHP_VERSION);
$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_modular_regression_'.$run, $identity->cluster);
$this->assertSame(55439, (int) $identity->port);
$this->withoutExceptionHandling();
DB::listen(function (QueryExecuted $q) {
if ($this->measurement === null) return;
$this->measurement['queries']++;
$this->measurement['database_ms'] += $q->time;
if (!preg_match('/^\s*(select|with)\b/i', $q->sql)) throw new \RuntimeException('READ_WORKLOAD_WROTE_SQL');
$key = hash('sha256', $q->sql);
$entry = $this->queries[$key] ?? ['sql' => $q->sql, 'bindings' => $q->bindings, 'count' => 0, 'total_ms' => 0.0];
$entry['count']++; $entry['total_ms'] += $q->time; $this->queries[$key] = $entry;
if ((hrtime(true) - $this->started) / 1e6 > 15000 || $this->measurement['queries'] > 2500) throw new \RuntimeException('CASE_BUDGET_REACHED');
});
$this->report = ['date' => '2026-11-02', 'checkpoint' => '340f3bb', 'php' => PHP_VERSION, 'identity' => $identity,
'method' => 'Sequential in-process HTTP kernel requests; three measured repetitions after warm-up; fixtures rolled back; no external HTTP/browser or concurrent capacity claim.',
'bounds' => ['sql_timeout_ms' => 3000, 'case_budget_ms' => 15000, 'query_budget' => 2500], 'datasets' => []];
try {
foreach ([1000, 5000, 10000] as $size) {
DB::beginTransaction();
try {
$fixture = $this->fixture($size);
$f = $fixture['identity'];
$this->actingAs($f['manager'])->withSession(['auth_version' => $f['manager']->auth_version]);
$token = $this->postJson('/api/context', ['tenant_id' => $f['tenant'], 'company_id' => $f['company'], 'unit_id' => $f['unit'], 'module_id' => null])->assertOk()->json('context.token');
$this->withHeader('X-SGF-Context', $token);
$this->getJson('/api/module-records?per_page=1')->assertOk();
$dataset = ['clients' => $size, 'services' => count($fixture['ids']['services']), 'drafts_per_area' => count($fixture['ids']['service_drafts']),
'module_events' => DB::table('identity_audit')->where('tenant_id', $f['tenant'])->where('company_id', $f['company'])->where('action', 'like', 'module.record.%')->count(),
'history_pairs_per_nonpending_record' => 4, 'statistics' => 'ANALYZE within disposable regression database; no index or schema changes', 'cases' => []];
$cases = [
['clients', 'all', 25, 1, ''], ['clients', 'all', 25, (int) ceil($size / 25), ''],
['clients', 'all', 100, 1, ''], ['clients', 'pending', 25, 1, ''],
['clients', 'associated', 25, 1, ''], ['clients', 'removed', 25, 1, ''],
['clients', 'all', 25, 1, 'LOAD-client-000017'],
['service_drafts', 'all', 25, 1, ''], ['billing_drafts', 'all', 25, 1, ''],
];
foreach ($cases as [$area, $state, $perPage, $page, $search]) {
$expected = array_values(array_filter($fixture['ids'][$area], fn ($id) => match ($state) {
'pending' => $id % 3 === 0, 'associated' => $id % 3 === 1, 'removed' => $id % 3 === 2, default => true,
}));
if ($search !== '') $expected = [$fixture['ids']['clients'][16]];
$case = $this->measure(compact('area', 'state', 'perPage', 'page', 'search'), $expected);
$dataset['cases'][] = $case;
fwrite(STDOUT, "\nLOAD ".$size.' '.$case['label'].' '.$case['status'].' '.($case['median_ms'] ?? '')."ms\n");
}
// A complete traversal with page size 100 validates ID coverage,
// not timing/capacity; it is capped to the smallest dataset.
if ($size === 1000) {
$seen = [];
for ($p = 1; $p <= 10; $p++) {
$rows = $this->getJson('/api/module-records?per_page=100&page='.$p)->assertOk()->json('records.clients');
$seen = [...$seen, ...array_column($rows, 'id')];
}
$this->assertSame($fixture['ids']['clients'], $seen);
$dataset['complete_traversal'] = ['pages' => 10, 'records' => count($seen), 'ordered_exact_coverage' => true];
}
$this->report['datasets'][] = $dataset;
} finally { $this->measurement = null; DB::rollBack(); }
}
$this->report['completed'] = true;
} finally {
$this->measurement = null;
$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($this->report, JSON_PRETTY_PRINT | JSON_THROW_ON_ERROR).PHP_EOL);
}
}
protected function fixture(int $size, bool $uniform = false): array
{
DB::statement("SET LOCAL statement_timeout = '30s'");
$f = IdentityFixture::create('-load-'.bin2hex(random_bytes(6)));
foreach (['clients', 'services', 'service_drafts', 'billing_drafts'] as $area) {
IdentityFixture::grant($f['manager']->id, $f['tenant'], $f['company'], "$area.view", str_ends_with($area, '_drafts') ? $f['unit'] : null);
}
$ids = [];
foreach (['clients' => $size, 'services' => $uniform ? $size : 100] as $area => $n) {
DB::insert("INSERT INTO a_{$area}(tenant_id,company_id,unit_id,name,version,data,created_at,updated_at)
SELECT ?,?,NULL,? || lpad(i::text,6,'0'),1,'{}'::jsonb,now(),now() FROM generate_series(1,?) i", [$f['tenant'], $f['company'], $area === 'clients' ? 'LOAD-client-' : 'LOAD-service-', $n]);
$ids[$area] = DB::table('a_'.$area)->where('tenant_id', $f['tenant'])->where('company_id', $f['company'])->orderBy('id')->pluck('id')->all();
}
$draftCount = $uniform ? $size : min(1000, (int) ($size / 10));
$draftRows = [];
for ($i = 0; $i < $draftCount; $i++) {
$draftRows[] = ['tenant_id' => $f['tenant'], 'company_id' => $f['company'], 'unit_id' => $f['unit'], 'name' => 'LOAD-draft-'.$i, 'version' => 1,
'client_id' => $ids['clients'][$i], 'data' => json_encode(['currency' => 'MZN', 'tax_status' => 'pending', 'accounting_status' => 'not_processed',
'lines' => [['service_id' => $ids['services'][$i % count($ids['services'])], 'quantity' => '1', 'unit_price' => '10']]]), 'created_at' => now(), 'updated_at' => now()];
}
foreach (array_chunk($draftRows, 500) as $chunk) DB::table('a_service_drafts')->insert($chunk);
$ids['service_drafts'] = DB::table('a_service_drafts')->where('tenant_id', $f['tenant'])->where('company_id', $f['company'])->orderBy('id')->pluck('id')->all();
foreach ($draftRows as $i => &$row) { $row['source_id'] = $ids['service_drafts'][$i]; $data = json_decode($row['data'], true); $data['source_version'] = 1; $row['data'] = json_encode($data); }
unset($row);
foreach (array_chunk($draftRows, 500) as $chunk) DB::table('a_billing_drafts')->insert($chunk);
$ids['billing_drafts'] = DB::table('a_billing_drafts')->where('tenant_id', $f['tenant'])->where('company_id', $f['company'])->orderBy('id')->pluck('id')->all();
foreach (array_keys($ids) as $area) {
DB::insert("INSERT INTO identity_audit(action,actor_id,tenant_id,company_id,unit_id,details,created_at)
SELECT CASE WHEN seq%2=1 THEN 'module.record.associated' ELSE 'module.record.disassociated' END,?,?,?,r.unit_id,
jsonb_build_object('area',?::text,'record_id',r.id,'module_id','commercial','version',1,'reason','Synthetic load history'),now()
FROM a_{$area} r CROSS JOIN generate_series(1,8) seq WHERE r.tenant_id=? AND r.company_id=? AND r.id%3<>0 ORDER BY seq,r.id",
[$f['manager']->id, $f['tenant'], $f['company'], $area, $f['tenant'], $f['company']]);
DB::insert("INSERT INTO identity_audit(action,actor_id,tenant_id,company_id,unit_id,details,created_at)
SELECT 'module.record.associated',?,?,?,r.unit_id,jsonb_build_object('area',?::text,'record_id',r.id,'module_id','commercial','version',1,'reason','Synthetic active state'),now()
FROM a_{$area} r WHERE r.tenant_id=? AND r.company_id=? AND r.id%3=1 ORDER BY r.id",
[$f['manager']->id, $f['tenant'], $f['company'], $area, $f['tenant'], $f['company']]);
}
// Noise belongs to another tenant or an unselected unit, never copied from a client.
DB::insert("INSERT INTO a_clients(tenant_id,company_id,unit_id,name,version,data,created_at,updated_at) SELECT ?,?,NULL,'LOAD-foreign-'||i,1,'{}',now(),now() FROM generate_series(1,100) i", [$f['otherTenant'], $f['foreign']]);
$hidden = $draftRows[0]; unset($hidden['source_id']); $hidden['unit_id'] = $f['hidden']; $hidden['name'] = 'LOAD-hidden';
DB::table('a_service_drafts')->insert($hidden);
foreach (['a_clients', 'a_services', 'a_service_drafts', 'a_billing_drafts', 'identity_audit'] as $table) DB::statement('ANALYZE '.$table);
return ['identity' => $f, 'ids' => $ids];
}
private function measure(array $spec, array $expected): array
{
['area' => $area, 'state' => $state, 'perPage' => $perPage, 'page' => $page, 'search' => $search] = $spec;
$out = ['label' => "$area/$state/size$perPage/page$page".($search !== '' ? '/search' : ''), 'status' => 'ok', 'samples' => []];
$plans = [];
for ($i = 0; $i < 3; $i++) {
DB::beginTransaction(); DB::statement("SET LOCAL statement_timeout = '3s'");
$this->queries = []; $this->measurement = ['queries' => 0, 'database_ms' => 0.0];
memory_reset_peak_usage(); $memory = memory_get_usage(true); $this->started = hrtime(true);
try {
$r = $this->getJson('/api/module-records?'.http_build_query(['area' => $area, 'state' => $state, 'per_page' => $perPage, 'page' => $page, 'search' => $search]));
$elapsed = (hrtime(true) - $this->started) / 1e6;
$metrics = $this->measurement; $this->measurement = null;
$r->assertOk()->assertJsonPath('pagination.total', count($expected));
$rows = $r->json('records.'.$area);
$this->assertSame(array_slice($expected, ($page - 1) * $perPage, $perPage), array_column($rows, 'id'));
foreach ($rows as $row) $this->assertFalse($row['dependencies_restricted']);
$out['samples'][] = [...$metrics, 'elapsed_ms' => round($elapsed, 3), 'database_ms' => round($metrics['database_ms'], 3),
'peak_php_bytes' => memory_get_peak_usage(true), 'transient_php_bytes' => max(0, memory_get_peak_usage(true) - $memory), 'response_bytes' => strlen($r->getContent()), 'rows' => count($rows)];
$plans = $this->queries;
} catch (\Illuminate\Database\QueryException $e) {
$this->measurement = null; $out['status'] = 'sql_timeout_or_error'; $out['sqlstate'] = $e->errorInfo[0] ?? $e->getCode(); $out['elapsed_ms'] = round((hrtime(true) - $this->started) / 1e6, 3); break;
} catch (\RuntimeException $e) {
$this->measurement = null;
if ($e->getMessage() !== 'CASE_BUDGET_REACHED') throw $e;
$out['status'] = 'case_budget_reached'; $out['elapsed_ms'] = round((hrtime(true) - $this->started) / 1e6, 3); break;
} finally { $this->measurement = null; DB::rollBack(); }
}
if (count($out['samples']) === 3) {
$times = array_column($out['samples'], 'elapsed_ms'); sort($times);
$out['median_ms'] = $times[1]; $out['min_ms'] = $times[0]; $out['max_ms'] = $times[2];
}
uasort($plans, fn ($a, $b) => $b['total_ms'] <=> $a['total_ms']);
$out['top_query_plans'] = [];
foreach (array_slice($plans, 0, 3, true) as $signature => $query) {
DB::beginTransaction(); DB::statement("SET LOCAL statement_timeout = '3s'");
try {
$raw = DB::select('EXPLAIN (ANALYZE,BUFFERS,FORMAT JSON) '.$query['sql'], $query['bindings']);
$plan = json_decode($raw[0]->{'QUERY PLAN'}, true, 512, JSON_THROW_ON_ERROR)[0];
$nodes = []; $this->nodes($plan['Plan'], $nodes);
$out['top_query_plans'][] = ['signature' => $signature, 'query_shape' => $query['sql'], 'calls_per_request' => $query['count'],
'cumulative_ms' => round($query['total_ms'], 3), 'planning_ms' => $plan['Planning Time'], 'execution_ms' => $plan['Execution Time'], 'nodes' => $nodes];
} catch (\Illuminate\Database\QueryException $e) { $out['top_query_plans'][] = ['signature' => $signature, 'status' => 'plan_timeout_or_error', 'sqlstate' => $e->errorInfo[0] ?? $e->getCode()];
} finally { DB::rollBack(); }
}
return $out;
}
private function nodes(array $plan, array &$nodes): void
{
$nodes[] = array_intersect_key($plan, array_flip(['Node Type', 'Relation Name', 'Index Name', 'Actual Rows', 'Actual Loops', 'Rows Removed by Filter', 'Shared Hit Blocks', 'Shared Read Blocks', 'Temp Read Blocks', 'Temp Written Blocks']));
foreach ($plan['Plans'] ?? [] as $child) $this->nodes($child, $nodes);
}
}
@@ -0,0 +1,97 @@
<?php
namespace Tests\Benchmarks;
use App\Models\User;
use App\Modules\Identity\Services\ModuleAssociationReview;
use App\Shared\Context\CompanyContext;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\DB;
use Tests\TestCase;
// Test subprocess only, outside the public root and normal test discovery.
final class ModuleAssociationReadWorkerTest extends TestCase
{
public function test_isolated_worker(): void
{
$run = (string) getenv('SGF_ISOLATED_REGRESSION_RUN');
$this->assertMatchesRegularExpression('/^[a-f0-9]{12}$/D', $run);
$file = realpath((string) getenv('SGF_ASSOCIATION_WORKER_INPUT'));
$private = realpath(base_path('../private'));
if (!$file || !$private || !str_starts_with($file, $private.DIRECTORY_SEPARATOR)) throw new \RuntimeException('Unexpected private input');
$p = json_decode(file_get_contents($file), true, 512, JSON_THROW_ON_ERROR);
$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);
DB::select("SELECT set_config('application_name',?,false)", [$p['application_name']]);
$this->withoutExceptionHandling();
$user = User::findOrFail($p['actor']);
$token = $p['context'] ?? bin2hex(random_bytes(24));
$this->actingAs($user)->withSession(['auth_version' => $user->auth_version, 'company_context' => [
'tenant_id' => $p['tenant'], 'company_id' => $p['company'], 'unit_id' => $p['unit'], 'module_id' => null, 'token' => $token,
]])->withHeader('X-SGF-Context', $token);
$result = ['mode' => $p['mode'], 'requests' => []];
if ($p['mode'] === 'read') {
DB::beginTransaction(); DB::statement('SET TRANSACTION READ ONLY'); DB::statement("SET LOCAL statement_timeout = '3s'");
try {
$this->getJson($p['url'])->assertOk();
$this->barrier($p);
$metrics = null;
DB::listen(function ($q) use (&$metrics) {
if ($metrics === null) return;
if (!preg_match('/^\s*(select|with)\b/i', $q->sql)) throw new \RuntimeException('READ_WORKER_WROTE_SQL');
$metrics['queries']++; $metrics['database_ms'] += $q->time;
});
for ($i = 0; $i < $p['requests']; $i++) {
gc_collect_cycles(); memory_reset_peak_usage(); $memory = memory_get_usage(false);
$metrics = ['queries' => 0, 'database_ms' => 0.0]; $start = hrtime(true); $wall = microtime(true);
try {
$r = $this->getJson($p['url']);
$elapsed = (hrtime(true) - $start) / 1e6; $finished = microtime(true); $stats = $metrics; $metrics = null;
$peak = memory_get_peak_usage(false);
$r->assertOk()->assertJsonPath('pagination.total', $p['total']);
$this->assertSame($p['response_sha256'], hash('sha256', $r->getContent()));
$result['requests'][] = ['status' => 'ok', 'elapsed_ms' => round($elapsed, 3), 'started_at' => $wall, 'finished_at' => $finished,
'queries' => $stats['queries'], 'database_ms' => round($stats['database_ms'], 3), 'incremental_php_bytes' => max(0, $peak - $memory), 'peak_php_bytes' => $peak];
} catch (\Illuminate\Database\QueryException $e) {
$metrics = null; $result['requests'][] = ['status' => 'sql_timeout_or_error', 'sqlstate' => $e->errorInfo[0] ?? $e->getCode(), 'elapsed_ms' => round((hrtime(true) - $start) / 1e6, 3)]; break;
}
}
} finally { DB::rollBack(); }
} elseif (in_array($p['mode'], ['confirm', 'confirm_http', 'preview_http'], true)) {
app(CompanyContext::class)->selectFor($p['actor'], $p['tenant'], $p['company'], $p['unit']);
$timeout = (int) ($p['lock_timeout_ms'] ?? 1000);
if ($timeout < 100 || $timeout > 10000) throw new \RuntimeException('Unexpected diagnostic lock timeout');
DB::select("SELECT set_config('lock_timeout',?,false)", [$timeout.'ms']);
DB::select("SELECT set_config('statement_timeout',?,false)", [isset($p['lock_timeout_ms']) ? '12s' : '5s']);
$this->barrier($p); $start = hrtime(true);
try {
if ($p['mode'] !== 'confirm') {
$this->withExceptionHandling();
$response = $this->postJson($p['mode'] === 'preview_http' ? '/api/module-records/preview' : '/api/module-records',
$p['mode'] === 'preview_http' ? $p['preview_input'] : ['review_token' => $p['review_token']]);
$response->assertStatus(423)->assertExactJson(['code' => 'association_busy',
'message' => 'A conferência está ocupada por outra operação. Aguarde e volte a confirmar; o estado será novamente validado.']);
$this->assertSame('no-store, private', $response->headers->get('Cache-Control'));
$result['confirmation'] = ['status' => 'http_busy', 'http_status' => $response->status(), 'code' => $response->json('code'),
'body_keys' => array_keys($response->json()), 'elapsed_ms' => round((hrtime(true) - $start) / 1e6, 3)];
} else {
$r = Request::create('/api/module-records', 'POST', ['review_token' => $p['review_token']]); $r->headers->set('X-SGF-Context', $token);
$result['confirmation'] = ['status' => ($p['expected_result'] ?? false) ? 'completed' : 'unexpected_success', 'result' => app(ModuleAssociationReview::class)->confirm($r), 'elapsed_ms' => round((hrtime(true) - $start) / 1e6, 3)];
}
} catch (\Symfony\Component\HttpKernel\Exception\HttpExceptionInterface $e) {
$result['confirmation'] = ['status' => 'refused', 'http_status' => $e->getStatusCode(), 'elapsed_ms' => round((hrtime(true) - $start) / 1e6, 3)];
} catch (\Illuminate\Database\QueryException $e) {
$result['confirmation'] = ['status' => 'database_error', 'sqlstate' => $e->errorInfo[0] ?? $e->getCode(), 'elapsed_ms' => round((hrtime(true) - $start) / 1e6, 3)];
}
} else { throw new \RuntimeException('Unknown worker mode'); }
$result['finished_at'] = microtime(true);
file_put_contents($p['result'], json_encode($result, JSON_PRETTY_PRINT | JSON_THROW_ON_ERROR));
}
private function barrier(array $p): void
{
file_put_contents($p['ready'], 'ready'); $deadline = microtime(true) + 45;
while (!is_file($p['go'])) { if (microtime(true) > $deadline) throw new \RuntimeException('Worker barrier timed out'); usleep(5000); }
}
}
@@ -0,0 +1,118 @@
<?php
namespace Tests\Benchmarks;
use Illuminate\Support\Facades\DB;
use Tests\Support\IdentityFixture;
require_once __DIR__.'/ModuleAssociationConcurrencyTest.php';
final class ModuleAssociationReleaseTest extends ModuleAssociationConcurrencyTest
{
// New cases only; the inherited 440-read benchmark is not executed.
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-release-'.$tag); mkdir($this->directory, 0700, true);
$before = $this->baseline(); $scope = IdentityFixture::create('-release-'.$tag);
foreach (['clients.view', 'clients.edit'] as $permission) IdentityFixture::grant($scope['manager']->id, $scope['tenant'], $scope['company'], $permission);
$cases = ['rollback_then_complete', 'commit_then_stale_revision', 'commit_then_same_token', 'commit_record_change_then_stale_version']; $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 association release '.$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(['synthetic_only' => true, 'tenant' => $scope['tenant'], 'company' => $scope['company'], 'unit' => $scope['unit'], 'actor' => $scope['manager']->id, 'clients' => $ids], JSON_PRETTY_PRINT | JSON_THROW_ON_ERROR));
$report = ['date' => '2026-11-02', 'run_tag' => $tag, 'identity' => $identity, 'source_sha256' => hash_file('sha256', base_path('app/Modules/Identity/Services/ModuleAssociationReview.php')),
'fixture_policy' => 'Four new QA-only clients committed for worker visibility; fixture and synthetic results retained in regression DB with private manifest.', 'cases' => []];
try {
foreach ($cases as $number => $name) {
$id = $ids[$name]; $input = ['area' => 'clients', 'id' => $id, 'version' => 1, 'module_id' => 'commercial', 'operation' => 'associate', 'reason' => 'Synthetic transaction release '.$name];
$first = $this->postJson('/api/module-records/preview', $input)->assertOk()->assertJsonPath('before', [])->json('review_token');
$second = $this->postJson('/api/module-records/preview', $input)->assertOk()->json('review_token');
$token = $name === 'commit_then_same_token' ? $first : $second;
$parent = (int) DB::selectOne('SELECT pg_backend_pid() AS pid')->pid;
DB::beginTransaction(); $job = null; $released = false; $firstEvent = null;
try {
if ($name === 'commit_record_change_then_stale_version') {
DB::table('companies')->where('tenant_id', $scope['tenant'])->where('id', $scope['company'])->lockForUpdate()->firstOrFail();
DB::table('a_clients')->where('id', $id)->update(['version' => 2, 'name' => 'Synthetic version changed during wait']);
} else {
$firstEvent = $this->postJson('/api/module-records', ['review_token' => $first])->assertOk()->assertJsonPath('replayed', false)->json('event_id');
}
$application = 'sgf_assoc_release_'.$tag.'_'.$number; $gate = $this->directory.'/go-'.$number;
$job = $this->launch(['mode' => 'confirm', 'expected_result' => true, 'lock_timeout_ms' => 8000,
'actor' => $scope['manager']->id, 'tenant' => $scope['tenant'], 'company' => $scope['company'], 'unit' => $scope['unit'],
'context' => $context, 'review_token' => $token, 'go' => $gate, 'application_name' => $application]);
$this->ready([$job]); file_put_contents($gate, 'go');
$wait = $this->waitForCompanyLock($application, $parent);
usleep(200000);
if ($name === 'rollback_then_complete') DB::rollBack(); else DB::commit();
$released = true;
$result = $this->collect($job)['confirmation'];
$events = DB::table('identity_audit')->where('tenant_id', $scope['tenant'])->where('company_id', $scope['company'])
->where('action', 'like', 'module.record.%')->whereRaw("details::jsonb->>'area' = 'clients'")->whereRaw("details::jsonb->>'record_id' = ?", [(string) $id])->count();
$version = (int) DB::table('a_clients')->where('id', $id)->value('version');
if (in_array($name, ['rollback_then_complete', 'commit_then_same_token'], true)) {
$this->assertSame('completed', $result['status']); $this->assertTrue($result['result']['ok']);
$this->assertSame($name === 'commit_then_same_token', $result['result']['replayed']);
$this->assertSame(1, $events); $this->assertSame(1, $version);
$replay = $this->postJson('/api/module-records', ['review_token' => $token])->assertOk()->assertJsonPath('replayed', true)->json('event_id');
$this->assertSame((int) $result['result']['event_id'], (int) $replay);
} else {
$this->assertSame('refused', $result['status']); $this->assertSame(409, $result['http_status']);
$this->postJson('/api/module-records', ['review_token' => $token])->assertConflict();
$this->postJson('/api/module-records/preview', [...$input, 'version' => $version])->assertOk();
$this->assertSame($name === 'commit_record_change_then_stale_version' ? 0 : 1, $events);
$this->assertSame($name === 'commit_record_change_then_stale_version' ? 2 : 1, $version);
if ($name === 'commit_then_stale_revision') $this->postJson('/api/module-records', ['review_token' => $first])->assertOk()->assertJsonPath('replayed', true);
}
$final = DB::table('identity_audit')->where('tenant_id', $scope['tenant'])->where('company_id', $scope['company'])->where('action', 'like', 'module.record.%')
->whereRaw("details::jsonb->>'area' = 'clients'")->whereRaw("details::jsonb->>'record_id' = ?", [(string) $id])->count();
$this->assertSame($events, $final, 'Retry must not append another decision');
$report['cases'][] = ['case' => $name, 'release' => $name === 'rollback_then_complete' ? 'rollback' : 'commit', 'wait' => $wait,
'second_status' => $result['status'], 'second_http_status' => $result['http_status'] ?? 200, 'second_replayed' => $result['result']['replayed'] ?? null,
'elapsed_ms' => $result['elapsed_ms'], 'record_version_after' => $version, 'decision_events_after_retry' => $final,
'first_event_persisted' => $firstEvent === null ? null : DB::table('identity_audit')->where('id', $firstEvent)->exists(), 'duplicate_decision' => false];
fwrite(STDOUT, "\nRELEASE $name status=".($result['http_status'] ?? 200)." events=$final version=$version\n");
} finally {
if (!$released && $job !== null && is_resource($job['process'])) { proc_terminate($job['process']); proc_close($job['process']); }
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();
}
}
private function waitForCompanyLock(string $application, int $parent): array
{
$deadline = microtime(true) + 5;
do {
DB::select('SELECT pg_stat_clear_snapshot()');
$row = DB::selectOne("SELECT pid,wait_event_type,wait_event,to_json(pg_blocking_pids(pid)) AS blockers,query FROM pg_stat_activity WHERE application_name=? AND wait_event_type='Lock'", [$application]);
if ($row) {
$this->assertContains($parent, json_decode($row->blockers, true));
$this->assertStringContainsString('"companies"', $row->query); $this->assertMatchesRegularExpression('/for update/i', $row->query);
$locks = DB::select('SELECT locktype,mode FROM pg_locks WHERE pid=? AND NOT granted', [$row->pid]);
return ['actual_wait_observed' => true, 'blocking_parent_verified' => true, 'statement_table' => 'companies', 'statement_for_update' => true,
'wait_event_type' => $row->wait_event_type, 'wait_event' => $row->wait_event, 'ungranted_locks' => $locks];
}
usleep(25000);
} while (microtime(true) < $deadline);
$this->fail('The second confirmation did not wait on the expected company lock');
}
}