From 3fad3190537e0049c256068f3806a295c63b9104 Mon Sep 17 00:00:00 2001 From: nexxo Date: Sun, 26 Jul 2026 00:02:18 +0200 Subject: [PATCH] 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 --- app/Provisioning/Jobs/PurgeHost.php | 6 ++++++ tests/Feature/Provisioning/HostStepsTest.php | 21 ++++++++++++++++++++ 2 files changed, 27 insertions(+) diff --git a/app/Provisioning/Jobs/PurgeHost.php b/app/Provisioning/Jobs/PurgeHost.php index 6a13a38..3d395fb 100644 --- a/app/Provisioning/Jobs/PurgeHost.php +++ b/app/Provisioning/Jobs/PurgeHost.php @@ -54,6 +54,12 @@ class PurgeHost implements ShouldQueue 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 // has to happen first — otherwise the host's management address stays // published with nothing left to clean it up. diff --git a/tests/Feature/Provisioning/HostStepsTest.php b/tests/Feature/Provisioning/HostStepsTest.php index 7ff769c..d5d1a6a 100644 --- a/tests/Feature/Provisioning/HostStepsTest.php +++ b/tests/Feature/Provisioning/HostStepsTest.php @@ -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. 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(); +});