Files
coolify/tests/Feature/QueueRoutingTest.php
T
Andras Bacsai 452259aacc feat(scheduler): dispatch scheduled jobs by their stored next run time
The scheduled job manager checked every enabled schedule each minute and
evaluated its cron expression in PHP. On Coolify Cloud one run took more than
60 seconds, so runs overlapped and minutes were skipped.

Each schedule now stores its next due time:
- next_run_at on scheduled database backups, scheduled tasks, and volume
  backups, and docker_cleanup_next_run_at on server settings (UTC, indexed)
- model hooks calculate it when a schedule is created or its frequency or
  enabled flag changes; a server timezone change or a move to another server
  calculates it again for the affected schedules

The `scheduled:dispatch` command runs the manager every minute from the
scheduler, not from a queue. It selects only due rows and claims each
occurrence with an atomic update of next_run_at, together with its delivery
record, so parallel runs on several nodes never dispatch it twice.
- database and volume backups run once when they are late; tasks and Docker
  cleanups more than 10 minutes late are logged as missed
- a local time that occurs two times when the clocks go back runs once
- the migration continues each schedule after its last recorded occurrence and
  drops scheduled_job_states

Show the next run, in the server timezone, on the database backup, scheduled
task, volume backup, and Docker cleanup pages, or "Invalid frequency" when the
frequency cannot be calculated. scheduled:diagnostics now lists overdue
schedules and open deliveries.

Remove workarounds that are no longer needed: the queue overlap lock and its
stale-lock repair, the catch-up window, and debug-only logs.
2026-09-29 10:07:23 +02:00

72 lines
2.2 KiB
PHP

<?php
use App\Actions\Database\StartDatabase;
use App\Actions\Database\StartDatabaseProxy;
use App\Actions\Service\StartService;
use App\Jobs\DatabaseBackupJob;
use App\Models\ScheduledDatabaseBackup;
describe('deployment_queue helper', function () {
test('uses the high queue on self-hosted', function () {
config(['constants.coolify.self_hosted' => true]);
expect(deployment_queue())->toBe('high');
});
test('uses the deployments queue on cloud', function () {
config(['constants.coolify.self_hosted' => false]);
expect(deployment_queue())->toBe('deployments');
});
});
describe('crons_queue helper', function () {
test('uses the high queue on self-hosted', function () {
config(['constants.coolify.self_hosted' => true]);
expect(crons_queue())->toBe('high');
});
test('uses the crons queue on cloud', function () {
config(['constants.coolify.self_hosted' => false]);
expect(crons_queue())->toBe('crons');
});
});
describe('start action job routing', function () {
test('routes to the deployments queue on cloud', function (string $actionClass) {
config(['constants.coolify.self_hosted' => false]);
expect($actionClass::makeJob()->queue)->toBe('deployments');
})->with([
StartDatabase::class,
StartDatabaseProxy::class,
StartService::class,
]);
test('routes to the high queue on self-hosted', function (string $actionClass) {
config(['constants.coolify.self_hosted' => true]);
expect($actionClass::makeJob()->queue)->toBe('high');
})->with([
StartDatabase::class,
StartDatabaseProxy::class,
StartService::class,
]);
});
describe('scheduled job routing', function () {
test('scheduled jobs use the crons queue on cloud', function () {
config(['constants.coolify.self_hosted' => false]);
expect((new DatabaseBackupJob(new ScheduledDatabaseBackup))->queue)->toBe('crons');
});
test('scheduled jobs use the high queue on self-hosted', function () {
config(['constants.coolify.self_hosted' => true]);
expect((new DatabaseBackupJob(new ScheduledDatabaseBackup))->queue)->toBe('high');
});
});