This document describes how the runner decides which job runs next, which CPUs
it gets, when a job is refused for capacity or disk, and how long it keeps job
history and workspaces. The code is in crates/motor-runner/src/engine.rs,
cpu.rs and disk.rs. architecture.md shows where these
parts sit; wire-protocol.md defines the refusals they
produce.
- A job claims exactly its
weight_hintvCPUs, or 2 when the request has none. The command's own flags and environment (such as-jorCARGO_BUILD_JOBS) do not change the claim. Test:claims_are_exact_and_unsized_jobs_always_claim_two(crates/motor-runner/src/tests/vcpu.rs). - A claim of 0, or larger than the host's allocatable CPUs, is refused before
start with
runner_fulland a detail naming the limit. Tests:oversized_and_zero_claims_are_refused_before_start_with_limit_detail,oversized_vcpu_refusal_detail_reaches_the_run_terminal(crates/motor-module/tests/it/recovery.rs). - A claim that fits the host but exceeds
max_weightis accepted but waits until it fits; it does not block other jobs. Test:pinned_job_waits_until_its_exact_claim_fits_max_weight. - The background warm build claims up to 4 vCPUs, fewer on a smaller host.
Test:
internal_warm_build_shrinks_to_a_small_host.
The runner gives each Linux job specific logical CPUs, records them in the job
record before setup, and pins the job's service to them with AllowedCPUs=.
- The topology comes from the host's online CPUs and their hyperthread
siblings. On a host with at least four physical cores, the first core and its
siblings are kept for the runner and the system; smaller hosts keep nothing
back. Tests:
topology_reads_online_siblings_and_reserves_cpu_zero_core,small_hosts_keep_every_cpu_allocatable(crates/motor-runner/src/cpu_tests.rs). - Placement prefers whole free sibling pairs; an odd claim prefers a pair that
is already split. Tests:
even_claims_use_whole_pairs_without_overlap_while_free,odd_claims_prefer_an_already_split_pair,fragmented_even_claim_uses_free_threads_before_overlap. - When no free CPUs are left, a claim overlaps the least-claimed CPUs. CPU sets
are therefore exclusive only while the host has free CPUs. Test:
overlap_uses_least_claimed_cpus_only_after_free_cpus. - Recorded CPU sets of jobs still running are registered again after a runner
restart. Test:
recorded_live_cpu_sets_reregister_after_restart. - The service's memory cap follows the claim; see
job-isolation.md. Test (operator-only):
isolated_four_vcpus_enforce_cpu_set_and_memory_max.
max_weight(default 28;serve --max-weight N) bounds the sum of vCPU claims of running jobs and setups in progress. Each setup counts as soon as it is chosen. Test:dispatch_indexes_add_each_starting_weight_and_cpu_claim_before_selecting_next(crates/motor-runner/src/tests/gate_priority.rs).max_queue_depth(default 64;serve --max-queue-depth N) bounds jobs that are accepted but not yet running, including those still uploading. A request beyond it is refused withrunner_fullbefore anyacceptedrecord. Tests:health_counts_admission_refusals_without_acceptance,queue_depth_counts_sixty_four_awaiting_bundles(crates/motor-module/tests/it/recovery.rs).- The Windows instance fixes every job's claim and
max_weightat 4, so it runs one job at a time; see windows-jobs.md. Test:windows_weights_are_fixed_and_warm_builds_and_caller_catalog_are_disabled.
A workspace runs one job at a time. Its oldest waiting job is its head; the dispatcher chooses among heads:
- Gate jobs (direct
ck motor runjobs, marked only when both sides negotiated thegate_jobscapability), in arrival order. Test:gate_gets_next_eight_permits_before_older_workers_and_later_gates,gate_mark_is_honored_only_after_capability_negotiation. - Heads that have waited 60 seconds or more, oldest first.
- Newer heads, rotating between workspaces by least recent dispatch.
- Background warm builds and cache fills, last.
When a head does not fit the free capacity, smaller heads behind it may run in
its place (backfill) for its first 60 seconds. After that, or when the blocked
head is a gate job, backfill stops until it fits. A head blocked by its own
workspace (a checkout in use, a deletion, a sibling conflict) does not stop
others. Running jobs are never stopped or moved to other CPUs, and no capacity
is permanently reserved. Tests: capped_head_allows_backfill_only_until_its_grace_expires
(crates/motor-runner/src/tests.rs), capacity_blocked_gate_still_fences_smaller_workers,
workspace_conflicted_gate_does_not_block_independent_workers,
waiting_gate_does_not_stop_or_repin_running_workers.
- A job's queue-wait clock starts when its bundles are verified and pinned, not
at acceptance; upload, fetch and setup do not count. Test:
upload_fetch_and_setup_do_not_spend_queue_wait_budget. - While the job is queued, its heartbeats carry
queue_wait_ms. The clock advances for capacity and other shared scheduler blockers, but pauses while the only blocker is an earlier job that owns the same workspace. If another blocker appears, queue time resumes from its accumulated value. Cancellation still removes a queued job; the request's execution timeout still starts when its command starts. Job status reports the current scheduler blocker inwaiting_on. The client keeps a local deadline fromqueue_wait_msand, when the budget runs out, asks the runner to withdraw the job. The runner answerswithdrawn(the job ends asrefused_before_start/queue_wait_exceeded) orstarted(too late; the job runs). Only that answer decides. Tests:zero_queue_wait_withdraws_and_cancel_is_idempotent(crates/motor-module/tests/it/recovery.rs),only_observed_permit_wait_can_withdraw_not_elapsed_acceptance,workspace_owner_wait_does_not_spend_queue_wait_budget,queue_wait_clock_resumes_for_capacity_after_workspace_owner_finishes,cancelled_workspace_waiter_is_removed_from_the_queue,jobs_waiting_on_a_workspace_keep_fifo_order(crates/motor-runner/src/tests.rs),streamed_elapsed_queue_time_sets_a_local_deadline(crates/motor-module/src/queue_tests.rs). - The budget is the request's
queue_wait_limit_s, elseck-motor'squeue_wait_limit_ssetting (default 300 seconds). Directck motor runjobs get 30 minutes. A gate job does not shorten the budget of jobs behind it. Test:worker_behind_gate_keeps_its_full_queue_wait_budget.
The runner checks free space on the filesystem holding its state before it accepts an upload, and again before setup.
- The hard floor is
serve --free-space-floor-bytes N, default 128 GiB (137438953472 bytes); 0 disables the check. The soft goal is twice the floor. - Below the soft goal, the runner first cleans up: it evicts workspaces not
used for 30 minutes, least recently used first, and stale staging. Under the
hard floor it also evicts recently used workspaces until it reaches the soft
goal. Tests:
disk_soft_cleanup_evicts_lru_before_the_hard_floor,disk_soft_pressure_evicts_only_candidates_used_over_30_minutes_ago,disk_hard_pressure_evicts_recent_candidates_in_lru_order_until_soft_goal,disk_soft_cleanup_does_nothing_at_twice_the_floor. - If space is still under the floor, the job is refused before upload with
runner_disk_full, withfree_bytesandfloor_bytesin the refusal. Tests:disk_admission_refuses_typed_when_reflinks_reclaim_too_little,disk_full_wire_refusal_carries_tag_and_bytes_before_upload,disk_full_reaches_run_and_prepare_callers_with_known_reason_and_detail(crates/motor-module/tests/it/recovery.rs). - Cleanup never touches running or reserved workspaces, the workspace the
incoming job needs, or its siblings. It removes only catalogued workspaces
under the allowed roots (default
/Users; add with--workspace-root). It does not follow symlinks or cross mounts. Tests:disk_eviction_skips_reserved_and_running_workspaces,disk_admission_preserves_incoming_ordinary_workspace_and_sibling,disk_admission_protects_canonical_alias_through_job_insertion. - The compile cache is evicted as a whole, and not while a fill is running; single entries are never deleted.
- Job records, output and replay history are kept for 24 hours
(
RETENTION_MS), counted from the later of the job's end and the time in its UUIDv7 ID. After that,attachreturnshistory_expired. Test:expired_history_never_becomes_before_start_refusal(crates/motor-module/tests/it/recovery.rs). - Snapshot refs, completed transfers and integration tips are pinned for the
same 24 hours, and while an active transfer or recorded workspace needs them.
Test:
refused_transfer_survives_maintenance_and_restart_until_retention_expires. - A task workspace (the default) is deleted after 24 hours idle. A workspace
marked
workspace_kind: headsurvives idle expiry but can still be evicted under disk pressure. Tests:head_workspace_survives_idle_expiry_and_task_defaults_expire,head_workspace_is_lru_evictable_outside_task_roots. - Output is kept in pages; when old output is evicted, replay starts with a
truncated_before_seqboundary rather than silently skipping. See wire-protocol.md.
Completed Linux jobs record CPU time and wall time from systemd's own summary,
as reservation_use with average_use_ppm (1,000,000 is 100%). runner.health
lists retained jobs that ran over ten minutes below 25% of their reservation.
Missing accounting is absent, never zero, and never delays the terminal record.
Tests: long_eight_vcpu_job_under_one_core_is_persisted_and_reported,
blocked_or_invalid_accounting_never_delays_or_changes_terminal.
runner.health also reports each repository's reserved vCPU-seconds, finished
job count, jobs grouped by requested vCPU size, and queue-wait refusals in the
rolling 24-hour window.
Repositories are grouped by the basename of their main checkout, so its worktrees
share one total; the report is rebuilt from retained job records after restart.