feat: let a job give up when it waits too long in the queue - #14
Open
stephenc wants to merge 2 commits into
Open
feat: let a job give up when it waits too long in the queue#14stephenc wants to merge 2 commits into
stephenc wants to merge 2 commits into
Conversation
`--timeout` limits the time that a job RUNS. This option limits the time
that a job WAITS. An agent that submits a job and waits for it can now
learn that the job will never start.
qex submit --max-queue-time 30m -- make test
The option is also a job file field, a pipeline stage field and
`[defaults] max_queue_time` in the config file.
A job that reaches the limit does not start. Its state becomes `expired`
and `qex wait` gives the code 123. That state is not `timeout`: a job
with `timeout` ran and wrote output, and a job with `expired` never got
the machine, so the two need different corrections and different codes.
The record says what the job waited for and how long it waited, because
a state with no reason gives the reader nothing to correct.
The clock starts at `submitted_at`, and not at the last scheduling pass.
A coordinator that starts again thus continues the same count. The wait
for a job in `needs` counts also: the option answers "does this id give
an answer inside this time", and a clock that stopped for a dependency
could not answer it.
There is no built-in default, and the config file has none until a user
writes one. A job that qex discards is work that a person wanted.
A coordinator that cannot obey the option refuses the job. Such a
coordinator would keep the field, give a job id, and let the job wait for
ever, which is the fault that the option removes.
Two guards stop the race that the record must not show: the job that a
scheduling pass chose is never in the list of jobs that expire, and the
code that writes `expired` refuses a job that is not in the queue. A job
that started thus never gets the state `expired`.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01KNvagiMEU3myn8EXGaGEM9
A review of this branch found three faults. A job that needed an expired job read "Read `qex logs <id>` for the cause." An expired job never started, so it wrote no output: the reader finds an empty file, `qex logs` gives the code 0, and the reader learns nothing. The same rule already held for a job that a user cancelled, and the guard tested that one state by name. It now holds a list of the states that a job reaches with no start. `qex top` showed an empty note for an expired job. Every other state that a job stops in gives a note there, and a reader who sees the state alone must open a second command. The remedy in the record was wrong for a job that expired while it waited for a job that it needs. It said "give the job a smaller claim, wait until the machine is quiet", and a smaller claim changes nothing for a job that waits for a dependency. The remedy now fits the cause. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01KNvagiMEU3myn8EXGaGEM9
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
What it does
--timeoutlimits the time that a job runs.--max-queue-timelimits thetime that a job waits before it starts.
The option is a
qex submitoption, a job file field (max_queue_time), apipeline stage field, and
[defaults] max_queue_timein the config file. It isplumbed in the same shape as
timeout:cli.rs->spec.rs-> the job record->
sched.rs.This closes part of issue #10 ("Smaller, agreed":
--max-queue-time, so a jobthat never starts gives up and says so).
The decisions, and why
A new state,
expired. Nottimeout. A job withtimeoutran, used themachine and wrote output; a job with
expirednever got the machine and its logfile is empty. The reader's next action is different for each: make the work
faster, against give the job a smaller claim or wait for a quiet machine. A
reader who cannot separate the two opens an empty log file and learns nothing.
A new exit code, 123. Not 125 (kill, timeout, out-of-memory) and not 126 (a
job that it needed failed). A script can thus separate "the work is too slow"
from "the machine never had capacity". The existing contract stays: 124 is still
the
timeoutcommand code for the wait itself, and--passthroughgives 123 aswell, because a job that never ran has no exit code of its own.
The reason text. The record says what the job waited for and how long:
No built-in default. A job that qex discards is work that a person wanted, so
a default value would throw away the work of a user who never asked for the rule,
on the day that the machine is busy.
[defaults] max_queue_timegives the ruleto every job of a user who does ask, one time.
The clock starts at
submitted_at. Not at the last scheduling pass. Acoordinator that stops and starts again continues the same count. Without that,
a restart would give each queued job a new full wait and the limit would promise
nothing.
A wait for a dependency counts. The option answers one question for the
reader: does this id give an answer inside this time? A clock that stopped while
a job waited for
needscould not answer it, because a chain of slow stageswould hold the clock for hours. The documentation says so, and it says the
remedy: give a stage a value that covers the whole pipeline, or give it none.
The capability
max-queue-time. A coordinator without it would keep thefield, give a job id, and let the job wait for ever — exactly the fault that the
option removes. The CLI refuses such a job and names the option.
The race that must not come back
A scheduling pass chooses a job, releases the lock, and
start_jobtakes thelock again and writes
starting. Two guards stop a pass from writingexpiredover a job that runs:
job that can start starts even in the second when its limit ends;
expirerefuses a job whose state is notqueued, which covers the momentbetween the two locks.
What I tested
New unit tests: the three sources of the value and
--max-queue-time 0; nolimit by default; a job past its limit expires with a reason that names the
wait; a job inside its limit and a job with no limit stay; a job that started
never expires (both guards); a coordinator that cannot obey the option refuses
the job.
New end-to-end tests:
a_job_that_never_starts_gives_up_and_says_why(a claimlarger than the budget with
oversized = "queue", so the job can never start:it becomes
expiredwith no start time, no exit code, and a reason that namesthe budget, and
qex waitgives 123) anda_job_that_started_at_its_queue_limit_keeps_its_result(ten jobs on a budget ofone core with a 2s limit, so the start and the limit meet: a record that says
expiredmust hold no start time and no exit code).What I measured
A manual run against a temporary state directory gave the record above, the same
text in
qex list,qex statusandqex wait, and the exit code 123.Cargo.tomlmoves from 0.7.1 to 0.8.0.🤖 Generated with Claude Code
https://claude.ai/code/session_01KNvagiMEU3myn8EXGaGEM9