Notes on ONTIC's current scheduler implementation, its limitations, and some good external references on distributed job scheduling.
Resource
- Design a Distributed Job Scheduler
- Design a Distributed Job Scheduler for Millions of Tasks in Daily Operations
Ontic Implementation
We are designing a cron job scheduler. The job of the scheduler is defining a set number of jobs and executing them at a specified time.
Here are the important points:
- How we are registering jobs
- How we are executing jobs
Terminology
JobDefinition
- Definition of a job.
- Whenever you create a job, you create a
JobDefinition. - It contains
cronExpressionfor when the job needs to be scheduled anddirtyfor whether the job still needs a trigger registered. - Whenever a new job is added we set
dirty=true. - A job with dirty
truemeans it does not have a trigger registered yet.
JobRunInfo
- After the job trigger is executed,
JobRunInfostores whether the job was successful or failed and the failure reason. - It contains
jobTriggerIdbecause it is associated with a trigger. - So each trigger will have one
JobRunInfo.
- After the job trigger is executed,
JobTrigger
- Everything about job execution.
- Contains
JobDefinition,previousFireTime,nextFireTime,JobRunInfo, andjobStatus. - If you want to execute a job you should create a trigger first.
Jobs Registration
- In ONTIC only developers run jobs, so the registration part is controlled by
InternalController, which is why it is not exposed externally. - You first define what job you want to schedule in
MandatoryJobs(enum). - A typical job enum includes:
jobNamecronExprjobLevel(GLOBAL,ORG,SPACE)jobData(job metadata)
- A typical job can be scheduled at 3 levels:
GLOBALORGSPACE
- You basically create a
JobDefinitionspecifying the level you want the job to run at. - Just defining the enum does not schedule the job.
- You still need to create a
JobDefinitionfor it. This is the first step of job registration. - You register the job by running the internal curl
retriggerJobs()method and passing thejobNamethat you added inMandatoryJobs. retriggerJobs()basically deletes any existingJobDefinitionandJobTriggerassociated with thatjobName, then creates a newJobDefinitionwithdirty=truebased on the appropriate level.
Unregister Jobs
- It is also possible to unregister jobs.
- Mark that
JobDefinitionasdirty=trueandenabled=false, then delete the correspondingJobTrigger.
Jobs Execution
OnticScheduleris responsible for executing all jobs.- It is deployed separately and executes all jobs by accessing the
schedulerMongo, which is at the global level. - The crux of job execution is:
- Create trigger
- Trigger all jobs
- Reconcile stuck jobs
- If you look at
SchedulerConfig, which is a configuration class, you will see all methods are@Scheduledwith their respective timings.
Trigger Scheduler
10 sec initial delay, runs every 2 mins
- Every job needs a trigger to run.
- This fetches all dirty jobs, which are the newly created ones, and creates triggers for them and saves them in Mongo.
- It sets the trigger status to
WAITING, setsnextFireTime, and so on.
Trigger Executor
15 sec initial delay, runs every 30 sec
- Every 30 seconds it executes all triggers.
- It checks the
jobTriggercollection and ifnextFireTime < currentTime + 30 sec, it collects that trigger for execution. - It first collects all
WAITINGjob triggers and executes them. - Then it collects all
FAILEDjob triggers and executes them. - For each trigger execution:
- Status is first changed to
QUEUED - A messaging event
JobExecutionEventis created and pushed to Kafka on thejob_executiontopic - During execution of
FAILEDjobs we first check if max retry count is reached - If max retry count is reached, mark the trigger as
ABORTED - Else, re-trigger it
- Status is first changed to
SchedulerTopicSubscriber
Listening on job_execution
- Acquire lock on
JobTriggerby updating the status toIN_PROGRESS - Execute job
- Mandatory jobs implement
OnticJob, which has theexecute()method
- Mandatory jobs implement
- Post execution:
- Set
JobRunInfo - Release lock
- If success, set
jobStatus=DONE - Else set
jobStatus=FAILED
- Set
Trigger Reconciler
20 sec initial delay, runs every 15 mins
- It executes stuck jobs, meaning jobs whose status is
IN_PROGRESSbut whosenextFireTimeis already in the past. - Find jobs whose
nextFireTimeis older than the last10 minsand which areIN_PROGRESS- These jobs are basically stuck during subscriber execution, so fail them, increase failure count, and update the
JobTrigger
- These jobs are basically stuck during subscriber execution, so fail them, increase failure count, and update the
- Find jobs whose
nextFireTimeis older than the last30 minsand which areQUEUED- These were never picked for execution, maybe because events were not consumed or were lost, so just fail them
Failure Scenarios and Re-Designing
Single Point of Failure
- Only one worker node (
SchedulerTopicSubscriber) - This creates a single point of failure, because if that node goes down, no jobs will be executed
- Only one worker node (
Limited Scalability
- With only one worker node, the system cannot scale horizontally to handle higher job loads
Lack of Proper Coordination
- There is no dedicated coordination layer with leader election
- That can create issues if multiple scheduler instances are ever run
Rudimentary Job Recovery
Trigger Reconcileronly runs every 15 minutes- Stuck jobs can remain inconsistent for too long
Tight Coupling
JobDefinition,JobTrigger, andJobRunInfoseem tightly coupled- That makes it harder to evolve the system independently
No Mention of Priorities
- There does not seem to be a mechanism for prioritizing jobs
- Important jobs may get delayed
Simplistic Load Balancing
- The system relies on Kafka partitioning for load balancing
- That might not be ideal for jobs with varying resource requirements
Lack of Resource Awareness
- There is no indication that the system considers job resource requirements or worker capacity
No Explicit Failure Handling Strategy
- There is retry logic
- But there is no clear strategy for permanent failures or more complex error scenarios
Minimal Monitoring
- There does not appear to be comprehensive monitoring and alerting for the scheduler itself
Manual Job Registration
- The requirement to manually register jobs through
InternalControllercould be cumbersome when the number of jobs grows
- The requirement to manually register jobs through
Design a Distributed Job Scheduler
These articles are really good: