Skip to content
MediumSchedulingPython 3

Multi-tenant Job Scheduler

Dispatch pending jobs by priority, cap tenant streaks when alternatives exist, and reap soft cancellations.

45m3 sample tests8 hidden tests

Implement FairJobScheduler, a with a tenant streak cap and cancellation. Its limits consecutive dispatches when another tenant has live work waiting.

Requirements

  • Constructor receives max_consecutive_per_org. Raise ValueError if max_consecutive_per_org <= 0.
  • enqueue(org, job_id, priority) adds a job. Lower priority number runs first. Raise ValueError if job_id is already active (pending, including soft-canceled jobs that have not yet been reaped).
  • cancel(job_id) marks a pending job canceled and returns whether it was still active. After a job has been successfully dispatched, later cancel returns False.
  • Canceling an already soft-canceled but unreaped ID also returns True. Cancellation does not immediately free its ID.
  • dispatch(count) returns up to count job IDs.
  • breaks ties within the same priority.
  • When the fairness cap blocks the next job from last_org, pick the highest-priority pending job from another org (FIFO among equals).
  • If another org has pending work, dispatch may not return more than max_consecutive_per_org jobs from the same org in a row. The fairness streak is global and persists across separate dispatch calls.
  • Canceled jobs are skipped and reaped (their IDs leave the active set) when dropped from the pending queue.
  • At the start of each positive-count dispatch iteration, remove all canceled entries before choosing a job. They neither count toward the streak nor qualify as another org's pending work.
  • A dispatched or reaped ID may be enqueued again; the new arrival gets a new FIFO position.
  • dispatch(count) with count <= 0 returns [] without changing state or reaping cancellations. If only one org has live work, continue dispatching it even beyond the cap.

Example

Although b1 has lower priority than a3, the two-job fairness cap gives organization b a turn before more work from a.

python
1s = FairJobScheduler(2) 2s.enqueue("a", "a1", 1) 3s.enqueue("a", "a2", 1) 4s.enqueue("a", "a3", 1) 5s.enqueue("b", "b1", 5) 6assert s.dispatch(4) == ["a1", "a2", "b1", "a3"]

Splitting that request into dispatch(2) followed by dispatch(2) must return ["a1", "a2"], then ["b1", "a3"]. Starting a new API call does not reset the streak. Skipping a canceled job also does not reset it.

This is a job-count rule, not a guarantee of equal compute time or eventual service for every tenant. If a and b continuously supply high-priority jobs, alternating them can keep a low-priority c waiting forever. Aging or weighted scheduling is a different policy. Dispatch only removes queued jobs; actual worker execution is outside this exercise.

Constraints

  • Keep state in memory.
  • Org and job IDs are strings, priorities and counts are integers, and the fairness cap is an integer. Negative priorities are valid and sort before zero. Type validation is outside the contract; the specified invalid-cap and duplicate-ID errors are required.
  • Calls are sequential. Cancel applies only to pending work and cannot stop a job that has already been dispatched.
  • Optimize for correctness and clear invariants before heap efficiency.

Editor