Multi-tenant Job Scheduler
Dispatch pending jobs by priority, cap tenant streaks when alternatives exist, and reap soft cancellations.
Implement FairJobScheduler, a priority scheduler with a tenant streak cap and cancellation. Its fair-use rule limits consecutive dispatches when another tenant has live work waiting.
Requirements
- Constructor receives
max_consecutive_per_org. RaiseValueErrorifmax_consecutive_per_org <= 0. enqueue(org, job_id, priority)adds a job. Lower priority number runs first. RaiseValueErrorifjob_idis 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, latercancelreturnsFalse.- Canceling an already soft-canceled but unreaped ID also returns
True. Cancellation does not immediately free its ID. dispatch(count)returns up tocountjob IDs.- FIFO order 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_orgjobs from the same org in a row. The fairness streak is global and persists across separatedispatchcalls. - 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)withcount <= 0returns[]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.
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.