fix(jobservice): CPU spin when cron expression can never fire
Author: nikitakodkanyCreated Sep 14, 2026Updated Sep 14, 2026
Description
Cron expressions that are syntactically valid but can never fire (e.g. `0 0 3 30 2 *` - Feb 30) cause jobservice to spin and saturate CPU.
robfig/cron validates each field individually against its allowed range, so day-of-month=30 and month=2 both pass Parse(). However schedule.Next() returns time.Time{} since no matching date exists within the five-year search window.
Root cause
Three compounding failures:
- ValidateCronString accepts the expression - Parse() succeeds, no error is returned.
- scheduleNextJobs loops indefinitely - time.Time{}.Before(horizon) is always true (year 1 < any real time), and each iteration overwrites the same Redis key via HMSET, saturating CPU.
- clearDirtyJobs cannot clean up the poisoned scheduled-queue entry - it queries ZRANGEBYSCORE with a lower bound of 0, but the zero-time job has score -62135596800 (negative), so it is never found.
Expected behavior
- ValidateCronString should reject cron expressions that can never fire.
- The scheduleNextJobs loop should exit cleanly when Next() returns zero time.
- clearDirtyJobs should clean up entries with negative epoch scores.
Steps to reproduce
Configure a Harbor GC schedule with cron 0 0 3 30 2 * via PUT /api/v2.0/system/gc/schedule. Observe jobservice CPU spike to near-limit.
Source: goharbor/harbor