From 3b33e8a4ab753308dde26282ebe057e578d561d2 Mon Sep 17 00:00:00 2001 From: Andras Bacsai <5845193+andrasbacsai@users.noreply.github.com> Date: Thu, 8 Oct 2026 13:43:45 +0200 Subject: [PATCH] fix(domains): stop the DNS poll from flooding the high queue The Domains page polls every 2 seconds while a DNS check runs. Each poll parsed the compose file, and the parser queued one ServerFilesFromServerJob per volume. The jobs starved the DNS check jobs and deployments. - Skip the compose parse in pollDnsChecks and keep the known services. - Make ServerFilesFromServerJob unique per resource until it runs. Fixes #12098 --- app/Jobs/ServerFilesFromServerJob.php | 14 +++++++++++- app/Livewire/Project/Application/Domains.php | 18 ++++++++++----- tests/Feature/ApplicationDomainsTest.php | 23 ++++++++++++++++++++ 3 files changed, 48 insertions(+), 7 deletions(-) diff --git a/app/Jobs/ServerFilesFromServerJob.php b/app/Jobs/ServerFilesFromServerJob.php index 58455df2f4..73641131fe 100644 --- a/app/Jobs/ServerFilesFromServerJob.php +++ b/app/Jobs/ServerFilesFromServerJob.php @@ -7,20 +7,32 @@ use App\Models\ServiceApplication; use App\Models\ServiceDatabase; use Illuminate\Bus\Queueable; use Illuminate\Contracts\Queue\ShouldBeEncrypted; +use Illuminate\Contracts\Queue\ShouldBeUniqueUntilProcessing; use Illuminate\Contracts\Queue\ShouldQueue; use Illuminate\Foundation\Bus\Dispatchable; use Illuminate\Queue\InteractsWithQueue; use Illuminate\Queue\SerializesModels; -class ServerFilesFromServerJob implements ShouldBeEncrypted, ShouldQueue +class ServerFilesFromServerJob implements ShouldBeEncrypted, ShouldBeUniqueUntilProcessing, ShouldQueue { use Dispatchable, InteractsWithQueue, Queueable, SerializesModels; + /** + * Only one sync per resource can wait in the queue. The compose parser dispatches + * this job once per volume on every parse, so without this the queue fills with copies. + */ + public int $uniqueFor = 600; + public function __construct(public ServiceApplication|ServiceDatabase|Application $resource) { $this->onQueue('high'); } + public function uniqueId(): string + { + return $this->resource::class.':'.$this->resource->id; + } + public function handle() { $this->resource->getFilesFromServer(isInit: true); diff --git a/app/Livewire/Project/Application/Domains.php b/app/Livewire/Project/Application/Domains.php index a0b54b7bb3..f1f897b855 100644 --- a/app/Livewire/Project/Application/Domains.php +++ b/app/Livewire/Project/Application/Domains.php @@ -178,11 +178,11 @@ class Domains extends Component $this->loadDomainState(); } - public function refreshDomains(): void + public function refreshDomains(bool $parseCompose = true): void { $editingRow = $this->editingIndex !== null ? ($this->domainRows[$this->editingIndex] ?? null) : null; - $this->loadDomainState(); + $this->loadDomainState($parseCompose); if ($editingRow !== null) { $index = collect($this->domainRows)->search(fn (array $row): bool => $row['url'] === $editingRow['url'] @@ -199,7 +199,9 @@ class Domains extends Component ->where('dns_status', 'checking') ->values(); - $this->refreshDomains(); + // The poll only refreshes DNS results. Parsing the compose file here would + // queue file sync jobs on every tick and starve the DNS check jobs. + $this->refreshDomains(parseCompose: false); foreach ($checkingRows as $checkingRow) { $row = collect($this->domainRows)->first(fn (array $row): bool => $row['url'] === $checkingRow['url'] @@ -258,7 +260,7 @@ class Domains extends Component $this->dispatch('success', 'HTTP to HTTPS redirect updated.'); } - public function loadDomainState(): void + public function loadDomainState(bool $parseCompose = true): void { $this->application->refresh(); $this->application->loadMissing(['destination.server', 'settings', 'additional_servers']); @@ -293,23 +295,27 @@ class Domains extends Component $this->serverIpConfigured = null; } - $this->composeServices = []; $pendingRedirect = $this->pendingRedirectService !== null ? ($this->serviceRedirects[$this->serviceRedirectWireKey($this->pendingRedirectService)] ?? null) : null; $this->serviceRedirects = []; - if ($this->isCompose) { + if (! $this->isCompose) { + $this->composeServices = []; + } elseif ($parseCompose) { try { $parsed = $this->application->parse() ?? []; } catch (\Throwable) { $parsed = []; } + $this->composeServices = []; $services = data_get($parsed, 'services', []); foreach ($services as $serviceName => $service) { if (! isDatabaseImage(data_get($service, 'image'))) { $this->composeServices[] = $serviceName; } } + } + if ($this->isCompose) { if ($this->newDomainService === null && count($this->composeServices) > 0) { $this->newDomainService = $this->composeServices[0]; } diff --git a/tests/Feature/ApplicationDomainsTest.php b/tests/Feature/ApplicationDomainsTest.php index 5ffea4298f..f468dcfc88 100644 --- a/tests/Feature/ApplicationDomainsTest.php +++ b/tests/Feature/ApplicationDomainsTest.php @@ -2,6 +2,7 @@ use App\Jobs\CheckDomainDnsJob; use App\Jobs\ConfigureDnsRecordJob; +use App\Jobs\ServerFilesFromServerJob; use App\Livewire\Project\Application\Domains; use App\Livewire\Project\Application\PreviewDomains; use App\Livewire\Project\Application\Previews; @@ -3656,3 +3657,25 @@ it('offers dns provider record creation for a public server ip', function () { ->toContain('Add with Cloudflare') ->not->toContain('The server has no public IP address; add the DNS record manually.'); }); + +it('does not queue file sync jobs when polling dns checks for compose applications', function () { + Queue::fake(); + + $this->application->update([ + 'build_pack' => 'dockercompose', + 'docker_compose_raw' => "services:\n web:\n image: nginx:alpine\n volumes:\n - web-data:/data\n api:\n image: nginx:alpine\n volumes:\n - api-data:/data\n - api-conf:/etc/api\nvolumes:\n web-data:\n api-data:\n api-conf:\n", + 'docker_compose_domains' => json_encode([ + 'web' => ['domain' => 'https://compose-poll-test.invalid'], + ]), + 'fqdn' => null, + ]); + + $component = Livewire::test(Domains::class, ['application' => $this->application->fresh()]); + $queuedOnMount = Queue::pushed(ServerFilesFromServerJob::class)->count(); + + $component->call('pollDnsChecks')->call('pollDnsChecks')->call('pollDnsChecks'); + + expect($queuedOnMount)->toBe(1) + ->and(Queue::pushed(ServerFilesFromServerJob::class))->toHaveCount(1) + ->and($component->get('composeServices'))->toBe(['web', 'api']); +});