fix(hosts): re-read the host before deciding there is no DNS record

A registration finishing while the purge waited on the run locks wrote
dns_record_id after the purge had already loaded the host, so it skipped the
deletion and then deleted the row holding the only id.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
feat/portal-design
nexxo 2026-07-26 00:02:18 +02:00
parent 7b57ab21cb
commit 3fad319053
2 changed files with 27 additions and 0 deletions

View File

@ -54,6 +54,12 @@ class PurgeHost implements ShouldQueue
Cache::lock('run:'.$run->uuid, 2200)->block(2100, fn () => $run->delete()); Cache::lock('run:'.$run->uuid, 2200)->block(2100, fn () => $run->delete());
} }
// Re-read after waiting on the run locks above: a registration that was
// still running may have written dns_record_id in the meantime, and the
// stale copy loaded before the wait would skip the deletion and then
// delete the row that held the id.
$host->refresh();
// The record identifier lives on the row about to be deleted, so this // The record identifier lives on the row about to be deleted, so this
// has to happen first — otherwise the host's management address stays // has to happen first — otherwise the host's management address stays
// published with nothing left to clean it up. // published with nothing left to clean it up.

View File

@ -388,3 +388,24 @@ it('does not hand the same name to two datacenter codes that look alike', functi
// failed the unique constraint mid-onboarding. // failed the unique constraint mid-onboarding.
expect($names)->toBe(['eu-west-01', 'eu-west-02']); expect($names)->toBe(['eu-west-01', 'eu-west-02']);
}); });
it('sees a DNS record registered while the purge was waiting', function () {
$s = fakeServices();
app()->instance(App\Services\Dns\HetznerDnsClient::class, $s['dns']);
$host = App\Models\Host::factory()->active()->create([
'datacenter' => 'fsn', 'wg_ip' => '10.66.0.90', 'dns_name' => null,
]);
$purge = new App\Provisioning\Jobs\PurgeHost($host->uuid);
// Registration finishes while the purge is blocked on the run lock: the
// copy it loaded first still says there is no record to delete.
$run = App\Models\ProvisioningRun::factory()->forHost($host)->create(['pipeline' => 'host']);
(new App\Provisioning\Steps\Host\RegisterHostDns($s['dns']))->execute($run);
$fqdn = $host->fresh()->dns_name.'.node.clupilot.com';
$purge->handle();
expect($s['dns']->records)->not->toHaveKey($fqdn)
->and(App\Models\Host::query()->whereKey($host->id)->exists())->toBeFalse();
});