Katkat

Build a Concurrent Recurring Job Scheduler

NuroSoftware Engineer · Technical Screen

Last updated

This problem tests your ability to design a thread-safe, concurrent task scheduling system that manages recurring executions while handling lifecycle events like cancellation and graceful shutdown.

Problem Statement

Design a JobScheduler class that allows clients to submit recurring tasks to be executed at a fixed interval. Your scheduler must support high concurrency, ensuring that adding or removing jobs does not corrupt internal state or cause memory leaks.

Requirements

  1. submit(task: Callable, interval_ms: int) -> JobID: Schedules a task to run repeatedly every interval_ms. Returns a unique identifier.
  2. cancel(job_id: JobID): Stops the job associated with the ID. If the job is currently running, the execution should be allowed to complete, but no future iterations should trigger.
  3. shutdown(): Stops all running jobs and prevents new submissions. The system should wait for active tasks to complete.

Constraints

  • Tasks must run independently; a slow task should not delay the scheduling of other tasks.
  • You must handle "cancellation races": a job should not be rescheduled if cancel() is called while it is executing.
  • Assume interval_ms is at least 10ms.

Example

# Execution flow
scheduler = JobScheduler(num_workers=4)
job_id = scheduler.submit(lambda: print("Tick"), 1000)

# ... some time later ...
scheduler.cancel(job_id)

Considerations

  • How will you track the next scheduled execution time for each job?
  • How do you ensure that shutdown() does not result in a deadlock?
  • How do you isolate exceptions occurring within a task so they don't crash the scheduler?

Hints

Hint 1

Use a priority queue (or a heap) to store tasks ordered by their next execution time, protected by a mutex.

Hint 2

Maintain a separate dictionary mapping `JobID` to task metadata to allow O(1) lookups for cancellation.

Hint 3

Use a dedicated worker pool (e.g., `ThreadPoolExecutor`) to execute tasks, ensuring the scheduler loop itself remains non-blocking.

Follow-up questions

  • How would you modify the system to handle tasks that take longer than their `interval_ms` (i.e., overlapping executions)?
  • If the scheduler process crashes, how could you persist the schedule to recover exactly where you left off?
  • How would you implement a 'jitter' feature to prevent a thundering herd problem when many jobs are scheduled at the same time?