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
This commit is contained in:
Andras Bacsai
2026-10-08 13:43:45 +02:00
parent f86a02f409
commit 3b33e8a4ab
3 changed files with 48 additions and 7 deletions
+13 -1
View File
@@ -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);
+12 -6
View File
@@ -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];
}
+23
View File
@@ -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']);
});