Design a cron job scheduler, similar to Airflow, that creates scheduled job occurrences and assigns them to available workers.
The scheduler must support one-time and recurring jobs, multiple workers, failed-attempt retries, duplicate-dispatch prevention, and execution-status monitoring.
Time is represented by nonnegative integer seconds. A cron schedule is represented by its first execution time and a fixed interval between later executions.
CronJobScheduler
public CronJobScheduler()
0 seconds.public boolean registerWorker(String workerId)
true when the worker is registered successfully.false when workerId is blank or already registered.currentTimeInSeconds.public boolean scheduleJob( String jobId, long firstRunTimeInSeconds, long intervalInSeconds, int maxRetries, long currentTimeInSeconds)
jobId.firstRunTimeInSeconds.intervalInSeconds is 0, the job has exactly one occurrence.intervalInSeconds is positive, later occurrences are scheduled every intervalInSeconds seconds.maxRetries is the maximum number of additional attempts allowed after the first attempt fails.currentTimeInSeconds is the time at which the scheduling request is made.true when the job is scheduled successfully.false when jobId is blank, the job ID already exists, or firstRunTimeInSeconds is earlier than currentTimeInSeconds.public List<String> dispatchJobs( long currentTimeInSeconds)
currentTimeInSeconds."jobId,scheduledTimeInSeconds,attemptNumber,workerId".1 and increase by one after each failed attempt that is eligible for a retry.maxRetries + 1.workerId order.QUEUED, and an occurrence waiting for a retry remains RETRY_PENDING.currentTimeInSeconds does not create or dispatch a duplicate occurrence.public boolean completeJob( String jobId, long scheduledTimeInSeconds, int attemptNumber, boolean successful, long currentTimeInSeconds)
jobId and scheduledTimeInSeconds.attemptNumber identifies the attempt whose result is being reported.attemptNumber must equal the number of the occurrence's currently running attempt.currentTimeInSeconds is the time at which the worker reports the attempt's result.successful is true, the occurrence becomes SUCCEEDED and is never dispatched again.successful is false and another retry is allowed, the occurrence becomes RETRY_PENDING.dispatchJobs call, including another call with the same currentTimeInSeconds.successful is false and no retry remains, the occurrence becomes FAILED.true when the completion report is accepted because the specified attempt is currently running.false when the specified occurrence does not currently have a running attempt or attemptNumber is not the number of its currently running attempt.currentTimeInSeconds still becomes the scheduler's current time.public String getJobStatus( String jobId, long scheduledTimeInSeconds, long currentTimeInSeconds)
jobId and scheduledTimeInSeconds.currentTimeInSeconds is the time at which the status is requested."SCHEDULED" when the specified time belongs to the job's schedule but the occurrence has not yet been created by dispatchJobs."QUEUED" when the occurrence is waiting for its first attempt."RETRY_PENDING" when a failed occurrence is waiting for another attempt."RUNNING", "SUCCEEDED", or "FAILED" when the occurrence is in that state."" when the job does not exist or scheduledTimeInSeconds is not one of its scheduled occurrence times.currentTimeInSeconds first advances the scheduler's current time to that value.currentTimeInSeconds values are monotonically nondecreasing.currentTimeInSeconds.dispatchJobs creates due occurrences and assigns them to workers.registerWorker has no time-dependent behavior and does not change the scheduler's current time.firstRunTimeInSeconds.firstRunTimeInSeconds + k * intervalInSeconds for every integer k ≥ 0.jobId and scheduledTimeInSeconds.1, and are never reused.maxRetries + 1.String.compareTo.1 ≤ workerId.length() ≤ 1001 ≤ jobId.length() ≤ 100','.1,000 workers are registered.100,000 jobs are scheduled.0 ≤ firstRunTimeInSeconds ≤ 1,000,000,000,0000 ≤ intervalInSeconds ≤ 1,000,000,0000 ≤ scheduledTimeInSeconds ≤ 1,000,000,000,0000 ≤ currentTimeInSeconds ≤ 1,000,000,000,0000 ≤ maxRetries ≤ 101 ≤ attemptNumber ≤ 11currentTimeInSeconds values are monotonically nondecreasing.scheduleJob call satisfies firstRunTimeInSeconds ≥ currentTimeInSeconds.200,000 occurrences become due.100,000.null.CronJobScheduler scheduler = new CronJobScheduler()
scheduler.registerWorker( workerId = "worker-b") returns true.
scheduler.registerWorker( workerId = "worker-a") returns true.
scheduler.scheduleJob( jobId = "inventory-sync", firstRunTimeInSeconds = 10, intervalInSeconds = 5, maxRetries = 1, currentTimeInSeconds = 0) returns true.
scheduler.dispatchJobs( currentTimeInSeconds = 9) returns [] because the first occurrence is not yet due.
scheduler.getJobStatus( jobId = "inventory-sync", scheduledTimeInSeconds = 10, currentTimeInSeconds = 9) returns "SCHEDULED".
scheduler.dispatchJobs( currentTimeInSeconds = 10) returns ["inventory-sync,10,1,worker-a"]. Both workers are available, so the lexicographically smaller worker is selected.
scheduler.completeJob( jobId = "inventory-sync", scheduledTimeInSeconds = 10, attemptNumber = 1, successful = true, currentTimeInSeconds = 11) returns true.
scheduler.getJobStatus( jobId = "inventory-sync", scheduledTimeInSeconds = 10, currentTimeInSeconds = 11) returns "SUCCEEDED".
scheduler.dispatchJobs( currentTimeInSeconds = 15) returns ["inventory-sync,15,1,worker-a"] for the next recurring occurrence.
CronJobScheduler scheduler = new CronJobScheduler()
scheduler.registerWorker( workerId = "worker-7") returns true.
scheduler.scheduleJob( jobId = "daily-report", firstRunTimeInSeconds = 20, intervalInSeconds = 0, maxRetries = 1, currentTimeInSeconds = 0) returns true.
scheduler.dispatchJobs( currentTimeInSeconds = 20) returns ["daily-report,20,1,worker-7"].
scheduler.completeJob( jobId = "daily-report", scheduledTimeInSeconds = 20, attemptNumber = 1, successful = false, currentTimeInSeconds = 21) returns true.
scheduler.getJobStatus( jobId = "daily-report", scheduledTimeInSeconds = 20, currentTimeInSeconds = 21) returns "RETRY_PENDING".
scheduler.dispatchJobs( currentTimeInSeconds = 21) returns ["daily-report,20,2,worker-7"].
scheduler.completeJob( jobId = "daily-report", scheduledTimeInSeconds = 20, attemptNumber = 1, successful = true, currentTimeInSeconds = 22) returns false because attempt 2, not attempt 1, is currently running.
scheduler.getJobStatus( jobId = "daily-report", scheduledTimeInSeconds = 20, currentTimeInSeconds = 22) returns "RUNNING" because the rejected completion report did not modify attempt 2.
scheduler.completeJob( jobId = "daily-report", scheduledTimeInSeconds = 20, attemptNumber = 2, successful = false, currentTimeInSeconds = 23) returns true.
scheduler.getJobStatus( jobId = "daily-report", scheduledTimeInSeconds = 20, currentTimeInSeconds = 23) returns "FAILED" because the initial attempt and the single allowed retry both failed.
CronJobScheduler scheduler = new CronJobScheduler()
scheduler.registerWorker( workerId = "worker-z") returns true.
scheduler.registerWorker( workerId = "worker-a") returns true.
scheduler.scheduleJob( jobId = "beta", firstRunTimeInSeconds = 30, intervalInSeconds = 0, maxRetries = 0, currentTimeInSeconds = 0) returns true.
scheduler.scheduleJob( jobId = "alpha", firstRunTimeInSeconds = 30, intervalInSeconds = 0, maxRetries = 0, currentTimeInSeconds = 0) returns true.
scheduler.dispatchJobs( currentTimeInSeconds = 30) returns ["alpha,30,1,worker-a", "beta,30,1,worker-z"]. Job IDs and worker IDs are both processed in lexicographically ascending order.
scheduler.dispatchJobs( currentTimeInSeconds = 30) returns [] because both occurrences are already running and cannot be dispatched twice.