Distributed Job Scheduler

Notes on ONTIC's current scheduler implementation, its limitations, and some good external references on distributed job scheduling.

Notes on ONTIC's current scheduler implementation, its limitations, and some good external references on distributed job scheduling.

Resource


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 cronExpression for when the job needs to be scheduled and dirty for whether the job still needs a trigger registered.
    • Whenever a new job is added we set dirty=true.
    • A job with dirty true means it does not have a trigger registered yet.
  • JobRunInfo

    • After the job trigger is executed, JobRunInfo stores whether the job was successful or failed and the failure reason.
    • It contains jobTriggerId because it is associated with a trigger.
    • So each trigger will have one JobRunInfo.
  • JobTrigger

    • Everything about job execution.
    • Contains JobDefinition, previousFireTime, nextFireTime, JobRunInfo, and jobStatus.
    • 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:
    • jobName
    • cronExpr
    • jobLevel (GLOBAL, ORG, SPACE)
    • jobData (job metadata)
  • A typical job can be scheduled at 3 levels:
    • GLOBAL
    • ORG
    • SPACE
  • You basically create a JobDefinition specifying the level you want the job to run at.
  • Just defining the enum does not schedule the job.
  • You still need to create a JobDefinition for it. This is the first step of job registration.
  • You register the job by running the internal curl retriggerJobs() method and passing the jobName that you added in MandatoryJobs.
  • retriggerJobs() basically deletes any existing JobDefinition and JobTrigger associated with that jobName, then creates a new JobDefinition with dirty=true based on the appropriate level.

Unregister Jobs

  • It is also possible to unregister jobs.
  • Mark that JobDefinition as dirty=true and enabled=false, then delete the corresponding JobTrigger.

Jobs Execution

  • OnticScheduler is responsible for executing all jobs.
  • It is deployed separately and executes all jobs by accessing the scheduler Mongo, 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 @Scheduled with 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, sets nextFireTime, and so on.

Trigger Executor

15 sec initial delay, runs every 30 sec

  • Every 30 seconds it executes all triggers.
  • It checks the jobTrigger collection and if nextFireTime < currentTime + 30 sec, it collects that trigger for execution.
  • It first collects all WAITING job triggers and executes them.
  • Then it collects all FAILED job triggers and executes them.
  • For each trigger execution:
    • Status is first changed to QUEUED
    • A messaging event JobExecutionEvent is created and pushed to Kafka on the job_execution topic
    • During execution of FAILED jobs we first check if max retry count is reached
    • If max retry count is reached, mark the trigger as ABORTED
    • Else, re-trigger it

SchedulerTopicSubscriber

Listening on job_execution

  • Acquire lock on JobTrigger by updating the status to IN_PROGRESS
  • Execute job
    • Mandatory jobs implement OnticJob, which has the execute() method
  • Post execution:
    • Set JobRunInfo
    • Release lock
    • If success, set jobStatus=DONE
    • Else set jobStatus=FAILED

Trigger Reconciler

20 sec initial delay, runs every 15 mins

  • It executes stuck jobs, meaning jobs whose status is IN_PROGRESS but whose nextFireTime is already in the past.
  • Find jobs whose nextFireTime is older than the last 10 mins and which are IN_PROGRESS
    • These jobs are basically stuck during subscriber execution, so fail them, increase failure count, and update the JobTrigger
  • Find jobs whose nextFireTime is older than the last 30 mins and which are QUEUED
    • 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
  • 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 Reconciler only runs every 15 minutes
    • Stuck jobs can remain inconsistent for too long
  • Tight Coupling

    • JobDefinition, JobTrigger, and JobRunInfo seem 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 InternalController could be cumbersome when the number of jobs grows

Design a Distributed Job Scheduler

These articles are really good: