mirror of
https://github.com/tiennm99/coolify.git
synced 2026-08-21 06:26:22 +00:00
fix(backups): include id 0 when paging scheduled jobs
Start backup and task pagination without an id lower bound so a legacy coolify-db backup or scheduled task at id 0 is dispatched with later positive ids. Document instance sentinel ids so they are not treated as ordinary autoincrement rows.
This commit is contained in:
@@ -122,6 +122,23 @@ function loginAsRoot(): mixed
|
||||
- **Project/Environment** — Organizational hierarchy: Team → Project → Environment → Resources.
|
||||
- **Proxy** — Traefik reverse proxy managed per server.
|
||||
|
||||
### Instance sentinels (`id = 0`)
|
||||
|
||||
Coolify seeds **instance-owned** rows at primary key `0`. That value is a sentinel meaning “this is the Coolify instance itself”, not a normal autoincrement id. Do not migrate, resequence, or “fix” these to a positive id.
|
||||
|
||||
| Record | Model / lookup | Meaning |
|
||||
|---|---|---|
|
||||
| Root team | `Team::find(0)`, `team_id === 0` | Instance / root team. Cloud billing and many skip-checks exempt `team_id === 0`. |
|
||||
| Localhost server | `Server::find(0)` / `findOrFail(0)` | The machine running Coolify. Upgrades, instance backups, and docker inspect target this server. |
|
||||
| Instance settings | `InstanceSettings` with `id = 0` | Singleton settings row. Tests must seed `InstanceSettings::create(['id' => 0])` (or `forceCreate`). |
|
||||
| Instance Postgres | `StandalonePostgresql` `id = 0`, name `coolify-db` | Coolify’s own database. UI treats `database_id === 0` as the instance DB (e.g. hide delete on backup screens). |
|
||||
| Local docker dest | `StandaloneDocker` `id = 0` | Destination on the localhost server (`destination_id = 0`). |
|
||||
| Root user / default GitHub App | seeders | First-install defaults. |
|
||||
|
||||
**Do not assign `id = 0` to new or non-instance rows.** In particular, `ScheduledDatabaseBackup` and `ScheduledTask` are ordinary schedules. Legacy installs may still have a `coolify-db` backup at `id = 0`; resolve that backup via the `coolify-db` relation / uuid, not `ScheduledDatabaseBackup::find(0)`.
|
||||
|
||||
`0` is a PHP/Eloquent landmine (`empty(0)` is true; keyset pagination `where('id', '>', $cursor)` starting at `0` skips the row). Queries that page by id must include `id = 0` on the first page (no lower bound, or cursor `< 0`). Prefer `chunkById()` over a hand-rolled `id > 0` cursor.
|
||||
|
||||
### Frontend
|
||||
- Livewire 3 components with Alpine.js for client-side interactivity
|
||||
- Blade templates in `resources/views/livewire/`
|
||||
|
||||
@@ -149,8 +149,8 @@ class ScheduledJobManager implements ShouldQueue
|
||||
|
||||
private function processScheduledBackupsAndTasks(): void
|
||||
{
|
||||
$lastBackupId = 0;
|
||||
$lastTaskId = 0;
|
||||
$lastBackupId = null;
|
||||
$lastTaskId = null;
|
||||
|
||||
do {
|
||||
$backups = $this->scheduledBackupQuery($lastBackupId)->get();
|
||||
@@ -190,16 +190,16 @@ class ScheduledJobManager implements ShouldQueue
|
||||
}
|
||||
}
|
||||
|
||||
private function scheduledBackupQuery(int $lastBackupId): Builder
|
||||
private function scheduledBackupQuery(?int $lastBackupId): Builder
|
||||
{
|
||||
return ScheduledDatabaseBackup::with(['database', 'team.subscription'])
|
||||
->where('enabled', true)
|
||||
->where('id', '>', $lastBackupId)
|
||||
->when($lastBackupId !== null, fn (Builder $query) => $query->where('id', '>', $lastBackupId))
|
||||
->orderBy('id')
|
||||
->limit(self::CHUNK_SIZE);
|
||||
}
|
||||
|
||||
private function scheduledTaskQuery(int $lastTaskId): Builder
|
||||
private function scheduledTaskQuery(?int $lastTaskId): Builder
|
||||
{
|
||||
return ScheduledTask::with([
|
||||
'service.destination.server.settings',
|
||||
@@ -208,7 +208,7 @@ class ScheduledJobManager implements ShouldQueue
|
||||
'application.destination.server.team.subscription',
|
||||
])
|
||||
->where('enabled', true)
|
||||
->where('id', '>', $lastTaskId)
|
||||
->when($lastTaskId !== null, fn (Builder $query) => $query->where('id', '>', $lastTaskId))
|
||||
->orderBy('id')
|
||||
->limit(self::CHUNK_SIZE);
|
||||
}
|
||||
|
||||
@@ -1,14 +1,17 @@
|
||||
<?php
|
||||
|
||||
use App\Jobs\DatabaseBackupJob;
|
||||
use App\Jobs\ScheduledJobManager;
|
||||
use App\Jobs\ScheduledTaskJob;
|
||||
use App\Models\Application;
|
||||
use App\Models\Environment;
|
||||
use App\Models\PrivateKey;
|
||||
use App\Models\Project;
|
||||
use App\Models\ScheduledDatabaseBackup;
|
||||
use App\Models\ScheduledTask;
|
||||
use App\Models\Server;
|
||||
use App\Models\StandaloneDocker;
|
||||
use App\Models\StandalonePostgresql;
|
||||
use App\Models\Team;
|
||||
use Illuminate\Foundation\Testing\RefreshDatabase;
|
||||
use Illuminate\Support\Carbon;
|
||||
@@ -91,6 +94,63 @@ it('skips expensive dispatch for non-due schedules while seeding dedup cache', f
|
||||
expect(Cache::get("scheduled-task:{$task->id}"))->not->toBeNull();
|
||||
});
|
||||
|
||||
it('dispatches the instance coolify-db backup even when its id is zero', function () {
|
||||
config(['constants.coolify.self_hosted' => true]);
|
||||
Carbon::setTestNow(Carbon::create(2026, 5, 27, 0, 1, 0, 'UTC'));
|
||||
Queue::fake();
|
||||
|
||||
$database = createScheduledBackupDatabase();
|
||||
$backup = createScheduledDatabaseBackup($database, [
|
||||
'id' => 0,
|
||||
'frequency' => '* * * * *',
|
||||
]);
|
||||
|
||||
expect($backup->id)->toBe(0);
|
||||
|
||||
(new ScheduledJobManager)->handle();
|
||||
|
||||
Queue::assertPushed(DatabaseBackupJob::class, 1);
|
||||
Queue::assertPushed(DatabaseBackupJob::class, fn (DatabaseBackupJob $job) => $job->backup->id === 0);
|
||||
});
|
||||
|
||||
it('dispatches zero-id schedules and continues with positive ids', function () {
|
||||
config(['constants.coolify.self_hosted' => true]);
|
||||
Carbon::setTestNow(Carbon::create(2026, 5, 27, 0, 1, 0, 'UTC'));
|
||||
Queue::fake();
|
||||
|
||||
$application = createScheduledTaskApplication();
|
||||
$database = StandalonePostgresql::create([
|
||||
'name' => 'coolify-db',
|
||||
'image' => 'postgres:16-alpine',
|
||||
'postgres_user' => 'postgres',
|
||||
'postgres_password' => 'password',
|
||||
'postgres_db' => 'postgres',
|
||||
'status' => 'running',
|
||||
'environment_id' => $application->environment_id,
|
||||
'destination_id' => $application->destination_id,
|
||||
'destination_type' => $application->destination_type,
|
||||
]);
|
||||
|
||||
$zeroIdBackup = createScheduledDatabaseBackup($database, ['id' => 0]);
|
||||
$positiveIdBackup = createScheduledDatabaseBackup($database);
|
||||
$zeroIdTask = createScheduledApplicationTask($application, ['id' => 0]);
|
||||
$positiveIdTask = createScheduledApplicationTask($application);
|
||||
|
||||
expect($zeroIdBackup->id)->toBe(0)
|
||||
->and($positiveIdBackup->id)->toBeGreaterThan(0)
|
||||
->and($zeroIdTask->id)->toBe(0)
|
||||
->and($positiveIdTask->id)->toBeGreaterThan(0);
|
||||
|
||||
(new ScheduledJobManager)->handle();
|
||||
|
||||
Queue::assertPushed(DatabaseBackupJob::class, 2);
|
||||
Queue::assertPushed(DatabaseBackupJob::class, fn (DatabaseBackupJob $job) => $job->backup->id === 0);
|
||||
Queue::assertPushed(DatabaseBackupJob::class, fn (DatabaseBackupJob $job) => $job->backup->id === $positiveIdBackup->id);
|
||||
Queue::assertPushed(ScheduledTaskJob::class, 2);
|
||||
Queue::assertPushed(ScheduledTaskJob::class, fn (ScheduledTaskJob $job) => $job->task->id === 0);
|
||||
Queue::assertPushed(ScheduledTaskJob::class, fn (ScheduledTaskJob $job) => $job->task->id === $positiveIdTask->id);
|
||||
});
|
||||
|
||||
it('does not query relationships when constructing scheduled task jobs', function () {
|
||||
$application = createScheduledTaskApplication();
|
||||
|
||||
@@ -148,3 +208,53 @@ uZx9iFkCELtxrh31QJ68AAAAEXNhaWxANzZmZjY2ZDJlMmRkAQIDBA==
|
||||
'status' => 'running',
|
||||
]);
|
||||
}
|
||||
|
||||
function createScheduledBackupDatabase(): StandalonePostgresql
|
||||
{
|
||||
$application = createScheduledTaskApplication();
|
||||
|
||||
return StandalonePostgresql::create([
|
||||
'name' => 'coolify-db',
|
||||
'image' => 'postgres:16-alpine',
|
||||
'postgres_user' => 'postgres',
|
||||
'postgres_password' => 'password',
|
||||
'postgres_db' => 'postgres',
|
||||
'status' => 'running',
|
||||
'environment_id' => $application->environment_id,
|
||||
'destination_id' => $application->destination_id,
|
||||
'destination_type' => $application->destination_type,
|
||||
]);
|
||||
}
|
||||
|
||||
function createScheduledDatabaseBackup(StandalonePostgresql $database, array $overrides = []): ScheduledDatabaseBackup
|
||||
{
|
||||
$backup = new ScheduledDatabaseBackup;
|
||||
$backup->forceFill(array_merge([
|
||||
'enabled' => true,
|
||||
'save_s3' => false,
|
||||
'frequency' => '* * * * *',
|
||||
'database_id' => $database->id,
|
||||
'database_type' => $database->getMorphClass(),
|
||||
'team_id' => $database->environment->project->team_id,
|
||||
], $overrides));
|
||||
$backup->save();
|
||||
|
||||
return $backup->fresh();
|
||||
}
|
||||
|
||||
function createScheduledApplicationTask(Application $application, array $overrides = []): ScheduledTask
|
||||
{
|
||||
$task = new ScheduledTask;
|
||||
$task->forceFill(array_merge([
|
||||
'name' => 'scheduled-task',
|
||||
'command' => 'echo hello',
|
||||
'frequency' => '* * * * *',
|
||||
'timeout' => 300,
|
||||
'enabled' => true,
|
||||
'team_id' => $application->environment->project->team_id,
|
||||
'application_id' => $application->id,
|
||||
], $overrides));
|
||||
$task->save();
|
||||
|
||||
return $task->fresh();
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user