mirror of
https://github.com/tiennm99/coolify.git
synced 2026-10-15 08:17:02 +00:00
refactor(scheduler): extract cron scheduling logic to shared helper
Extract the shouldRunNow() method from ScheduledJobManager and ServerManagerJob into a reusable shouldRunCronNow() helper function. This centralizes cron scheduling logic and enables consistent deduplication behavior across all scheduled job types. - Create shouldRunCronNow() helper in bootstrap/helpers/shared.php with timezone and dedup support - Refactor ScheduledJobManager and ServerManagerJob to use the shared helper - Add ScheduledJobDiagnostics command for inspecting cache state and scheduling decisions across all scheduled jobs - Simplify shouldRunNow tests to directly test the helper function - Add DockerCleanupJob test for error handling and execution tracking - Increase scheduled log retention from 1 to 7 days
This commit is contained in:
1 parent
89f2b83104
commit
f8f27fff13
8 files changed
+446
-317
No files matched your search
@@ -0,0 +1,50 @@
|
||||
<?php
|
||||
|
||||
use App\Jobs\DockerCleanupJob;
|
||||
use App\Models\DockerCleanupExecution;
|
||||
use App\Models\Server;
|
||||
use App\Models\User;
|
||||
use Illuminate\Foundation\Testing\RefreshDatabase;
|
||||
|
||||
uses(RefreshDatabase::class);
|
||||
|
||||
it('creates a failed execution record when server is not functional', function () {
|
||||
$user = User::factory()->create();
|
||||
$team = $user->teams()->first();
|
||||
$server = Server::factory()->create(['team_id' => $team->id]);
|
||||
|
||||
// Make server not functional by setting is_reachable to false
|
||||
$server->settings->update(['is_reachable' => false]);
|
||||
|
||||
$job = new DockerCleanupJob($server);
|
||||
$job->handle();
|
||||
|
||||
$execution = DockerCleanupExecution::where('server_id', $server->id)->first();
|
||||
|
||||
expect($execution)->not->toBeNull()
|
||||
->and($execution->status)->toBe('failed')
|
||||
->and($execution->message)->toContain('not functional')
|
||||
->and($execution->finished_at)->not->toBeNull();
|
||||
});
|
||||
|
||||
it('creates a failed execution record when server is force disabled', function () {
|
||||
$user = User::factory()->create();
|
||||
$team = $user->teams()->first();
|
||||
$server = Server::factory()->create(['team_id' => $team->id]);
|
||||
|
||||
// Make server not functional by force disabling
|
||||
$server->settings->update([
|
||||
'is_reachable' => true,
|
||||
'is_usable' => true,
|
||||
'force_disabled' => true,
|
||||
]);
|
||||
|
||||
$job = new DockerCleanupJob($server);
|
||||
$job->handle();
|
||||
|
||||
$execution = DockerCleanupExecution::where('server_id', $server->id)->first();
|
||||
|
||||
expect($execution)->not->toBeNull()
|
||||
->and($execution->status)->toBe('failed')
|
||||
->and($execution->message)->toContain('not functional');
|
||||
});
|
||||
@@ -1,271 +1,168 @@
|
||||
<?php
|
||||
|
||||
use App\Jobs\ScheduledJobManager;
|
||||
use Illuminate\Support\Carbon;
|
||||
use Illuminate\Support\Facades\Cache;
|
||||
|
||||
beforeEach(function () {
|
||||
// Clear any dedup keys
|
||||
Cache::flush();
|
||||
});
|
||||
|
||||
it('dispatches backup when job runs on time at the cron minute', function () {
|
||||
// Freeze time at exactly 02:00 — daily cron "0 2 * * *" is due
|
||||
Carbon::setTestNow(Carbon::create(2026, 2, 28, 2, 0, 0, 'UTC'));
|
||||
|
||||
$job = new ScheduledJobManager;
|
||||
|
||||
// Use reflection to test shouldRunNow
|
||||
$reflection = new ReflectionClass($job);
|
||||
|
||||
$executionTimeProp = $reflection->getProperty('executionTime');
|
||||
$executionTimeProp->setAccessible(true);
|
||||
$executionTimeProp->setValue($job, Carbon::now());
|
||||
|
||||
$method = $reflection->getMethod('shouldRunNow');
|
||||
$method->setAccessible(true);
|
||||
|
||||
$result = $method->invoke($job, '0 2 * * *', 'UTC', 'test-backup:1');
|
||||
$result = shouldRunCronNow('0 2 * * *', 'UTC', 'test-backup:1');
|
||||
|
||||
expect($result)->toBeTrue();
|
||||
});
|
||||
|
||||
it('catches delayed job when cache has a baseline from previous run', function () {
|
||||
// Simulate a previous dispatch yesterday at 02:00
|
||||
Cache::put('test-backup:1', Carbon::create(2026, 2, 27, 2, 0, 0, 'UTC')->toIso8601String(), 86400);
|
||||
|
||||
// Freeze time at 02:07 — job was delayed 7 minutes past today's 02:00 cron
|
||||
Carbon::setTestNow(Carbon::create(2026, 2, 28, 2, 7, 0, 'UTC'));
|
||||
|
||||
$job = new ScheduledJobManager;
|
||||
|
||||
$reflection = new ReflectionClass($job);
|
||||
|
||||
$executionTimeProp = $reflection->getProperty('executionTime');
|
||||
$executionTimeProp->setAccessible(true);
|
||||
$executionTimeProp->setValue($job, Carbon::now());
|
||||
|
||||
$method = $reflection->getMethod('shouldRunNow');
|
||||
$method->setAccessible(true);
|
||||
|
||||
// isDue() would return false at 02:07, but getPreviousRunDate() = 02:00 today
|
||||
// lastDispatched = 02:00 yesterday → 02:00 today > yesterday → fires
|
||||
$result = $method->invoke($job, '0 2 * * *', 'UTC', 'test-backup:1');
|
||||
$result = shouldRunCronNow('0 2 * * *', 'UTC', 'test-backup:1');
|
||||
|
||||
expect($result)->toBeTrue();
|
||||
});
|
||||
|
||||
it('does not double-dispatch on subsequent runs within same cron window', function () {
|
||||
// First run at 02:00 — dispatches and sets cache
|
||||
Carbon::setTestNow(Carbon::create(2026, 2, 28, 2, 0, 0, 'UTC'));
|
||||
|
||||
$job = new ScheduledJobManager;
|
||||
$reflection = new ReflectionClass($job);
|
||||
|
||||
$executionTimeProp = $reflection->getProperty('executionTime');
|
||||
$executionTimeProp->setAccessible(true);
|
||||
$executionTimeProp->setValue($job, Carbon::now());
|
||||
|
||||
$method = $reflection->getMethod('shouldRunNow');
|
||||
$method->setAccessible(true);
|
||||
|
||||
$first = $method->invoke($job, '0 2 * * *', 'UTC', 'test-backup:2');
|
||||
$first = shouldRunCronNow('0 2 * * *', 'UTC', 'test-backup:2');
|
||||
expect($first)->toBeTrue();
|
||||
|
||||
// Second run at 02:01 — should NOT dispatch (previousDue=02:00, lastDispatched=02:00)
|
||||
Carbon::setTestNow(Carbon::create(2026, 2, 28, 2, 1, 0, 'UTC'));
|
||||
$executionTimeProp->setValue($job, Carbon::now());
|
||||
|
||||
$second = $method->invoke($job, '0 2 * * *', 'UTC', 'test-backup:2');
|
||||
$second = shouldRunCronNow('0 2 * * *', 'UTC', 'test-backup:2');
|
||||
expect($second)->toBeFalse();
|
||||
});
|
||||
|
||||
it('fires every_minute cron correctly on consecutive minutes', function () {
|
||||
$job = new ScheduledJobManager;
|
||||
$reflection = new ReflectionClass($job);
|
||||
|
||||
$executionTimeProp = $reflection->getProperty('executionTime');
|
||||
$executionTimeProp->setAccessible(true);
|
||||
|
||||
$method = $reflection->getMethod('shouldRunNow');
|
||||
$method->setAccessible(true);
|
||||
|
||||
// Minute 1
|
||||
Carbon::setTestNow(Carbon::create(2026, 2, 28, 10, 0, 0, 'UTC'));
|
||||
$executionTimeProp->setValue($job, Carbon::now());
|
||||
$result1 = $method->invoke($job, '* * * * *', 'UTC', 'test-backup:3');
|
||||
expect($result1)->toBeTrue();
|
||||
expect(shouldRunCronNow('* * * * *', 'UTC', 'test-backup:3'))->toBeTrue();
|
||||
|
||||
// Minute 2
|
||||
Carbon::setTestNow(Carbon::create(2026, 2, 28, 10, 1, 0, 'UTC'));
|
||||
$executionTimeProp->setValue($job, Carbon::now());
|
||||
$result2 = $method->invoke($job, '* * * * *', 'UTC', 'test-backup:3');
|
||||
expect($result2)->toBeTrue();
|
||||
expect(shouldRunCronNow('* * * * *', 'UTC', 'test-backup:3'))->toBeTrue();
|
||||
|
||||
// Minute 3
|
||||
Carbon::setTestNow(Carbon::create(2026, 2, 28, 10, 2, 0, 'UTC'));
|
||||
$executionTimeProp->setValue($job, Carbon::now());
|
||||
$result3 = $method->invoke($job, '* * * * *', 'UTC', 'test-backup:3');
|
||||
expect($result3)->toBeTrue();
|
||||
expect(shouldRunCronNow('* * * * *', 'UTC', 'test-backup:3'))->toBeTrue();
|
||||
});
|
||||
|
||||
it('does not fire non-due jobs on restart when cache is empty', function () {
|
||||
// Time is 10:00, cron is daily at 02:00 — NOT due right now
|
||||
Carbon::setTestNow(Carbon::create(2026, 2, 28, 10, 0, 0, 'UTC'));
|
||||
|
||||
$job = new ScheduledJobManager;
|
||||
$reflection = new ReflectionClass($job);
|
||||
|
||||
$executionTimeProp = $reflection->getProperty('executionTime');
|
||||
$executionTimeProp->setAccessible(true);
|
||||
$executionTimeProp->setValue($job, Carbon::now());
|
||||
|
||||
$method = $reflection->getMethod('shouldRunNow');
|
||||
$method->setAccessible(true);
|
||||
|
||||
// Cache is empty (fresh restart) — should NOT fire daily backup at 10:00
|
||||
$result = $method->invoke($job, '0 2 * * *', 'UTC', 'test-backup:4');
|
||||
$result = shouldRunCronNow('0 2 * * *', 'UTC', 'test-backup:4');
|
||||
expect($result)->toBeFalse();
|
||||
});
|
||||
|
||||
it('fires due jobs on restart when cache is empty', function () {
|
||||
// Time is exactly 02:00, cron is daily at 02:00 — IS due right now
|
||||
Carbon::setTestNow(Carbon::create(2026, 2, 28, 2, 0, 0, 'UTC'));
|
||||
|
||||
$job = new ScheduledJobManager;
|
||||
$reflection = new ReflectionClass($job);
|
||||
|
||||
$executionTimeProp = $reflection->getProperty('executionTime');
|
||||
$executionTimeProp->setAccessible(true);
|
||||
$executionTimeProp->setValue($job, Carbon::now());
|
||||
|
||||
$method = $reflection->getMethod('shouldRunNow');
|
||||
$method->setAccessible(true);
|
||||
|
||||
// Cache is empty (fresh restart) — but cron IS due → should fire
|
||||
$result = $method->invoke($job, '0 2 * * *', 'UTC', 'test-backup:4b');
|
||||
$result = shouldRunCronNow('0 2 * * *', 'UTC', 'test-backup:4b');
|
||||
expect($result)->toBeTrue();
|
||||
});
|
||||
|
||||
it('does not dispatch when cron is not due and was not recently due', function () {
|
||||
// Time is 10:00, cron is daily at 02:00 — last due was 8 hours ago
|
||||
Carbon::setTestNow(Carbon::create(2026, 2, 28, 10, 0, 0, 'UTC'));
|
||||
|
||||
$job = new ScheduledJobManager;
|
||||
$reflection = new ReflectionClass($job);
|
||||
|
||||
$executionTimeProp = $reflection->getProperty('executionTime');
|
||||
$executionTimeProp->setAccessible(true);
|
||||
$executionTimeProp->setValue($job, Carbon::now());
|
||||
|
||||
$method = $reflection->getMethod('shouldRunNow');
|
||||
$method->setAccessible(true);
|
||||
|
||||
// previousDue = 02:00, but lastDispatched was set at 02:00 (simulate)
|
||||
Cache::put('test-backup:5', Carbon::create(2026, 2, 28, 2, 0, 0, 'UTC')->toIso8601String(), 86400);
|
||||
|
||||
$result = $method->invoke($job, '0 2 * * *', 'UTC', 'test-backup:5');
|
||||
$result = shouldRunCronNow('0 2 * * *', 'UTC', 'test-backup:5');
|
||||
expect($result)->toBeFalse();
|
||||
});
|
||||
|
||||
it('falls back to isDue when no dedup key is provided', function () {
|
||||
// Time is exactly 02:00, cron is "0 2 * * *" — should be due
|
||||
Carbon::setTestNow(Carbon::create(2026, 2, 28, 2, 0, 0, 'UTC'));
|
||||
expect(shouldRunCronNow('0 2 * * *', 'UTC'))->toBeTrue();
|
||||
|
||||
$job = new ScheduledJobManager;
|
||||
$reflection = new ReflectionClass($job);
|
||||
|
||||
$executionTimeProp = $reflection->getProperty('executionTime');
|
||||
$executionTimeProp->setAccessible(true);
|
||||
$executionTimeProp->setValue($job, Carbon::now());
|
||||
|
||||
$method = $reflection->getMethod('shouldRunNow');
|
||||
$method->setAccessible(true);
|
||||
|
||||
// No dedup key → simple isDue check
|
||||
$result = $method->invoke($job, '0 2 * * *', 'UTC');
|
||||
expect($result)->toBeTrue();
|
||||
|
||||
// At 02:01 without dedup key → isDue returns false
|
||||
Carbon::setTestNow(Carbon::create(2026, 2, 28, 2, 1, 0, 'UTC'));
|
||||
$executionTimeProp->setValue($job, Carbon::now());
|
||||
|
||||
$result2 = $method->invoke($job, '0 2 * * *', 'UTC');
|
||||
expect($result2)->toBeFalse();
|
||||
expect(shouldRunCronNow('0 2 * * *', 'UTC'))->toBeFalse();
|
||||
});
|
||||
|
||||
it('catches delayed docker cleanup when job runs past the cron minute', function () {
|
||||
// Simulate a previous dispatch at :10
|
||||
Cache::put('docker-cleanup:42', Carbon::create(2026, 2, 28, 10, 10, 0, 'UTC')->toIso8601String(), 86400);
|
||||
|
||||
// Freeze time at :22 — job was delayed 2 minutes past the :20 cron window
|
||||
Carbon::setTestNow(Carbon::create(2026, 2, 28, 10, 22, 0, 'UTC'));
|
||||
|
||||
$job = new ScheduledJobManager;
|
||||
$reflection = new ReflectionClass($job);
|
||||
|
||||
$executionTimeProp = $reflection->getProperty('executionTime');
|
||||
$executionTimeProp->setAccessible(true);
|
||||
$executionTimeProp->setValue($job, Carbon::now());
|
||||
|
||||
$method = $reflection->getMethod('shouldRunNow');
|
||||
$method->setAccessible(true);
|
||||
|
||||
// isDue() would return false at :22, but getPreviousRunDate() = :20
|
||||
// lastDispatched = :10 → :20 > :10 → fires
|
||||
$result = $method->invoke($job, '*/10 * * * *', 'UTC', 'docker-cleanup:42');
|
||||
$result = shouldRunCronNow('*/10 * * * *', 'UTC', 'docker-cleanup:42');
|
||||
|
||||
expect($result)->toBeTrue();
|
||||
});
|
||||
|
||||
it('does not double-dispatch docker cleanup within same cron window', function () {
|
||||
// First dispatch at :10
|
||||
Carbon::setTestNow(Carbon::create(2026, 2, 28, 10, 10, 0, 'UTC'));
|
||||
|
||||
$job = new ScheduledJobManager;
|
||||
$reflection = new ReflectionClass($job);
|
||||
|
||||
$executionTimeProp = $reflection->getProperty('executionTime');
|
||||
$executionTimeProp->setAccessible(true);
|
||||
$executionTimeProp->setValue($job, Carbon::now());
|
||||
|
||||
$method = $reflection->getMethod('shouldRunNow');
|
||||
$method->setAccessible(true);
|
||||
|
||||
$first = $method->invoke($job, '*/10 * * * *', 'UTC', 'docker-cleanup:99');
|
||||
$first = shouldRunCronNow('*/10 * * * *', 'UTC', 'docker-cleanup:99');
|
||||
expect($first)->toBeTrue();
|
||||
|
||||
// Second run at :11 — should NOT dispatch (previousDue=:10, lastDispatched=:10)
|
||||
Carbon::setTestNow(Carbon::create(2026, 2, 28, 10, 11, 0, 'UTC'));
|
||||
$executionTimeProp->setValue($job, Carbon::now());
|
||||
|
||||
$second = $method->invoke($job, '*/10 * * * *', 'UTC', 'docker-cleanup:99');
|
||||
$second = shouldRunCronNow('*/10 * * * *', 'UTC', 'docker-cleanup:99');
|
||||
expect($second)->toBeFalse();
|
||||
});
|
||||
|
||||
it('seeds cache with previousDue when not due on first run', function () {
|
||||
Carbon::setTestNow(Carbon::create(2026, 2, 28, 10, 0, 0, 'UTC'));
|
||||
|
||||
$result = shouldRunCronNow('0 2 * * *', 'UTC', 'test-seed:1');
|
||||
expect($result)->toBeFalse();
|
||||
|
||||
// Verify cache was seeded with previousDue (02:00 today)
|
||||
$cached = Cache::get('test-seed:1');
|
||||
expect($cached)->not->toBeNull();
|
||||
expect(Carbon::parse($cached)->format('H:i'))->toBe('02:00');
|
||||
});
|
||||
|
||||
it('catches next occurrence after cache was seeded on non-due first run', function () {
|
||||
// Step 1: 10:00 — not due, but seeds cache with previousDue (02:00 today)
|
||||
Carbon::setTestNow(Carbon::create(2026, 2, 28, 10, 0, 0, 'UTC'));
|
||||
expect(shouldRunCronNow('0 2 * * *', 'UTC', 'test-seed:2'))->toBeFalse();
|
||||
|
||||
// Step 2: Next day at 02:03 — delayed 3 minutes past cron.
|
||||
// previousDue = 02:00 Mar 1, lastDispatched = 02:00 Feb 28 → fires
|
||||
Carbon::setTestNow(Carbon::create(2026, 3, 1, 2, 3, 0, 'UTC'));
|
||||
expect(shouldRunCronNow('0 2 * * *', 'UTC', 'test-seed:2'))->toBeTrue();
|
||||
});
|
||||
|
||||
it('cache survives 29 days with static 30-day TTL', function () {
|
||||
Carbon::setTestNow(Carbon::create(2026, 2, 28, 2, 0, 0, 'UTC'));
|
||||
|
||||
shouldRunCronNow('0 2 * * *', 'UTC', 'test-ttl:static');
|
||||
expect(Cache::get('test-ttl:static'))->not->toBeNull();
|
||||
|
||||
// 29 days later — cache (30-day TTL) should still exist
|
||||
Carbon::setTestNow(Carbon::create(2026, 3, 29, 0, 0, 0, 'UTC'));
|
||||
expect(Cache::get('test-ttl:static'))->not->toBeNull();
|
||||
});
|
||||
|
||||
it('respects server timezone for cron evaluation', function () {
|
||||
// UTC time is 22:00 Feb 28, which is 06:00 Mar 1 in Asia/Singapore (+8)
|
||||
Carbon::setTestNow(Carbon::create(2026, 2, 28, 22, 0, 0, 'UTC'));
|
||||
|
||||
$job = new ScheduledJobManager;
|
||||
$reflection = new ReflectionClass($job);
|
||||
|
||||
$executionTimeProp = $reflection->getProperty('executionTime');
|
||||
$executionTimeProp->setAccessible(true);
|
||||
$executionTimeProp->setValue($job, Carbon::now());
|
||||
|
||||
$method = $reflection->getMethod('shouldRunNow');
|
||||
$method->setAccessible(true);
|
||||
|
||||
// Simulate that today's 06:00 UTC run was already dispatched (at 06:00 UTC)
|
||||
Cache::put('test-backup:7', Carbon::create(2026, 2, 28, 6, 0, 0, 'UTC')->toIso8601String(), 86400);
|
||||
|
||||
// Cron "0 6 * * *" in Asia/Singapore: local time is 06:00 Mar 1 → previousDue = 06:00 Mar 1 SGT
|
||||
// That's a NEW cron window (Mar 1) that hasn't been dispatched → should fire
|
||||
$resultSingapore = $method->invoke($job, '0 6 * * *', 'Asia/Singapore', 'test-backup:6');
|
||||
expect($resultSingapore)->toBeTrue();
|
||||
// Cron "0 6 * * *" in Asia/Singapore: local time is 06:00 Mar 1 → new window → should fire
|
||||
expect(shouldRunCronNow('0 6 * * *', 'Asia/Singapore', 'test-backup:6'))->toBeTrue();
|
||||
|
||||
// Cron "0 6 * * *" in UTC: previousDue = 06:00 Feb 28 UTC, already dispatched at 06:00 → should NOT fire
|
||||
$resultUtc = $method->invoke($job, '0 6 * * *', 'UTC', 'test-backup:7');
|
||||
expect($resultUtc)->toBeFalse();
|
||||
// Cron "0 6 * * *" in UTC: previousDue = 06:00 Feb 28, already dispatched → should NOT fire
|
||||
expect(shouldRunCronNow('0 6 * * *', 'UTC', 'test-backup:7'))->toBeFalse();
|
||||
});
|
||||
|
||||
it('passes explicit execution time instead of using Carbon::now()', function () {
|
||||
// Real "now" is irrelevant — we pass an explicit execution time
|
||||
Carbon::setTestNow(Carbon::create(2026, 2, 28, 15, 0, 0, 'UTC'));
|
||||
|
||||
$executionTime = Carbon::create(2026, 2, 28, 2, 0, 0, 'UTC');
|
||||
$result = shouldRunCronNow('0 2 * * *', 'UTC', 'test-exec-time:1', $executionTime);
|
||||
|
||||
expect($result)->toBeTrue();
|
||||
});
|
||||
@@ -1,6 +1,5 @@
|
||||
<?php
|
||||
|
||||
use App\Jobs\ServerManagerJob;
|
||||
use Illuminate\Support\Carbon;
|
||||
use Illuminate\Support\Facades\Cache;
|
||||
|
||||
@@ -9,94 +8,81 @@ beforeEach(function () {
|
||||
});
|
||||
|
||||
it('catches delayed sentinel restart when job runs past midnight', function () {
|
||||
// Simulate previous dispatch yesterday at midnight
|
||||
Cache::put('sentinel-restart:1', Carbon::create(2026, 2, 27, 0, 0, 0, 'UTC')->toIso8601String(), 86400);
|
||||
|
||||
// Job runs 3 minutes late at 00:03
|
||||
Carbon::setTestNow(Carbon::create(2026, 2, 28, 0, 3, 0, 'UTC'));
|
||||
|
||||
$job = new ServerManagerJob;
|
||||
$reflection = new ReflectionClass($job);
|
||||
|
||||
$executionTimeProp = $reflection->getProperty('executionTime');
|
||||
$executionTimeProp->setAccessible(true);
|
||||
$executionTimeProp->setValue($job, Carbon::now());
|
||||
|
||||
$method = $reflection->getMethod('shouldRunNow');
|
||||
$method->setAccessible(true);
|
||||
|
||||
// isDue() would return false at 00:03, but getPreviousRunDate() = 00:00 today
|
||||
// lastDispatched = yesterday 00:00 → today 00:00 > yesterday → fires
|
||||
$result = $method->invoke($job, '0 0 * * *', 'UTC', 'sentinel-restart:1');
|
||||
$result = shouldRunCronNow('0 0 * * *', 'UTC', 'sentinel-restart:1');
|
||||
|
||||
expect($result)->toBeTrue();
|
||||
});
|
||||
|
||||
it('catches delayed weekly patch check when job runs past the cron minute', function () {
|
||||
// Simulate previous dispatch last Sunday at midnight
|
||||
Cache::put('server-patch-check:1', Carbon::create(2026, 2, 22, 0, 0, 0, 'UTC')->toIso8601String(), 86400);
|
||||
|
||||
// This Sunday at 00:02 — job was delayed 2 minutes
|
||||
// 2026-03-01 is a Sunday
|
||||
// This Sunday at 00:02 — job was delayed 2 minutes (2026-03-01 is a Sunday)
|
||||
Carbon::setTestNow(Carbon::create(2026, 3, 1, 0, 2, 0, 'UTC'));
|
||||
|
||||
$job = new ServerManagerJob;
|
||||
$reflection = new ReflectionClass($job);
|
||||
|
||||
$executionTimeProp = $reflection->getProperty('executionTime');
|
||||
$executionTimeProp->setAccessible(true);
|
||||
$executionTimeProp->setValue($job, Carbon::now());
|
||||
|
||||
$method = $reflection->getMethod('shouldRunNow');
|
||||
$method->setAccessible(true);
|
||||
|
||||
$result = $method->invoke($job, '0 0 * * 0', 'UTC', 'server-patch-check:1');
|
||||
$result = shouldRunCronNow('0 0 * * 0', 'UTC', 'server-patch-check:1');
|
||||
|
||||
expect($result)->toBeTrue();
|
||||
});
|
||||
|
||||
it('catches delayed storage check when job runs past the cron minute', function () {
|
||||
// Simulate previous dispatch yesterday at 23:00
|
||||
Cache::put('server-storage-check:5', Carbon::create(2026, 2, 27, 23, 0, 0, 'UTC')->toIso8601String(), 86400);
|
||||
|
||||
// Today at 23:04 — job was delayed 4 minutes
|
||||
Carbon::setTestNow(Carbon::create(2026, 2, 28, 23, 4, 0, 'UTC'));
|
||||
|
||||
$job = new ServerManagerJob;
|
||||
$reflection = new ReflectionClass($job);
|
||||
|
||||
$executionTimeProp = $reflection->getProperty('executionTime');
|
||||
$executionTimeProp->setAccessible(true);
|
||||
$executionTimeProp->setValue($job, Carbon::now());
|
||||
|
||||
$method = $reflection->getMethod('shouldRunNow');
|
||||
$method->setAccessible(true);
|
||||
|
||||
$result = $method->invoke($job, '0 23 * * *', 'UTC', 'server-storage-check:5');
|
||||
$result = shouldRunCronNow('0 23 * * *', 'UTC', 'server-storage-check:5');
|
||||
|
||||
expect($result)->toBeTrue();
|
||||
});
|
||||
|
||||
it('seeds cache on non-due first run so weekly catch-up works', function () {
|
||||
// Wednesday at 10:00 — weekly cron (Sunday 00:00) is not due
|
||||
Carbon::setTestNow(Carbon::create(2026, 2, 25, 10, 0, 0, 'UTC'));
|
||||
|
||||
$result = shouldRunCronNow('0 0 * * 0', 'UTC', 'server-patch-check:seed-test');
|
||||
expect($result)->toBeFalse();
|
||||
|
||||
// Verify cache was seeded
|
||||
expect(Cache::get('server-patch-check:seed-test'))->not->toBeNull();
|
||||
|
||||
// Next Sunday at 00:02 — delayed 2 minutes past cron
|
||||
// Catch-up: previousDue = Mar 1 00:00, lastDispatched = Feb 22 → fires
|
||||
Carbon::setTestNow(Carbon::create(2026, 3, 1, 0, 2, 0, 'UTC'));
|
||||
|
||||
$result2 = shouldRunCronNow('0 0 * * 0', 'UTC', 'server-patch-check:seed-test');
|
||||
expect($result2)->toBeTrue();
|
||||
});
|
||||
|
||||
it('daily cron fires after cache seed even when delayed past the minute', function () {
|
||||
// Step 1: 15:00 — not due for midnight cron, but seeds cache
|
||||
Carbon::setTestNow(Carbon::create(2026, 2, 28, 15, 0, 0, 'UTC'));
|
||||
|
||||
$result1 = shouldRunCronNow('0 0 * * *', 'UTC', 'sentinel-restart:seed-test');
|
||||
expect($result1)->toBeFalse();
|
||||
|
||||
// Step 2: Next day at 00:05 — delayed 5 minutes past midnight
|
||||
// Catch-up: previousDue = Mar 1 00:00, lastDispatched = Feb 28 00:00 → fires
|
||||
Carbon::setTestNow(Carbon::create(2026, 3, 1, 0, 5, 0, 'UTC'));
|
||||
|
||||
$result2 = shouldRunCronNow('0 0 * * *', 'UTC', 'sentinel-restart:seed-test');
|
||||
expect($result2)->toBeTrue();
|
||||
});
|
||||
|
||||
it('does not double-dispatch within same cron window', function () {
|
||||
Carbon::setTestNow(Carbon::create(2026, 2, 28, 0, 0, 0, 'UTC'));
|
||||
|
||||
$job = new ServerManagerJob;
|
||||
$reflection = new ReflectionClass($job);
|
||||
|
||||
$executionTimeProp = $reflection->getProperty('executionTime');
|
||||
$executionTimeProp->setAccessible(true);
|
||||
$executionTimeProp->setValue($job, Carbon::now());
|
||||
|
||||
$method = $reflection->getMethod('shouldRunNow');
|
||||
$method->setAccessible(true);
|
||||
|
||||
$first = $method->invoke($job, '0 0 * * *', 'UTC', 'sentinel-restart:10');
|
||||
$first = shouldRunCronNow('0 0 * * *', 'UTC', 'sentinel-restart:10');
|
||||
expect($first)->toBeTrue();
|
||||
|
||||
// Next minute — should NOT dispatch again
|
||||
Carbon::setTestNow(Carbon::create(2026, 2, 28, 0, 1, 0, 'UTC'));
|
||||
$executionTimeProp->setValue($job, Carbon::now());
|
||||
|
||||
$second = $method->invoke($job, '0 0 * * *', 'UTC', 'sentinel-restart:10');
|
||||
$second = shouldRunCronNow('0 0 * * *', 'UTC', 'sentinel-restart:10');
|
||||
expect($second)->toBeFalse();
|
||||
});
|
||||
Reference in new issue
Block a user