morca · API

orca.scheduler

The scheduler: fires schedules, senses datasets, queues backfills and rebuilds, and starts runs.

orca register ./build/taxi             runs `taxi describe`, records the binary
orca backfill taxi 2011-01..2024-12    a run per partition that isn't done
orca backfill labels @all              keyed: runs of keys_per_run undone keys
orca rebuild taxi.trips 2024-01        the pipelines downstream, in order
orca tick                              one pass, then exits
orca scheduler                         a pass every second

The scheduler links no pipeline. It knows one by the describe it printed, kept in orca.pipelines with the binary's path, and reads the schedule, partitions and limits from that JSON in SQL. Everything it decides is in Postgres, so it can be stopped and restarted, or run twice, at any time:

  • A fire is a row. orca.fires is keyed by (pipeline, fire time), and a fire's row and its queued run are inserted in one transaction, so a fire is never made twice. Fires are counted from the last one recorded (or from when the pipeline was registered), so the fires missed while the scheduler was down are made once each, or, with catchup=False, only the latest. A fire whose partition is before the pipeline's first is recorded without a run.
  • One live run per partition. Every way a run is queued checks, under a lock on the pipeline's row, for a live run (orca.live) of the same keys: a fire whose partition a live run has records that run as its own, a sensor leaves such keys to it (the snapshot's run, if it left nothing else), a rebuild's level waits for it, the UI refuses to start one, and a backfill skips the key. A partition that's done is run again by a fire, sensor, rebuild or start, and its steps append again; orca register warns of a schedule that fires more than once for one partition.
  • Starting a run is resume. A queued run has nothing done, or what a resume or rerun from orca ui left it, which is what <binary> resume <run> takes (with --allow-new-code when the resume said so). The scheduler marks the run running, with no heartbeat and itself as owner, and execs the binary, which claims it. It doesn't wait: the run records its own progress.
  • max_runs. Under a lock on the pipeline's row, the runs that are live (a fresh heartbeat, or started by the scheduler less than the stale limit ago) are counted, and only that many fewer than max_runs are started.
  • Memory pressure (orca.pressure, orca.admission). On the local runner and for control-placed runs, none starts while this host's PSI some avg10 is over ORCA_ADMIT_MEMORY_PRESSURE (10%), each queued run held saying so; then the grants of a pipeline with a request (orca.resources).
  • Backfills queue what isn't done. A key is done when every mapped step has succeeded for it in a run of the pipeline's current code (DONE_SQL). A backfill drops the keys that are done or in a live run (queued, or running with its process alive: live_sql), sorts the rest and queues a run per keys_per_run of them; orca backfill says on stderr which keys it dropped and why.
  • Where runs run. ORCA_RUNNER is local (a process here, the default), kubernetes (a dista cluster of pods per run, orca.kube) or ec2 (a head instance and worker instances per run, orca.ec2). A pipeline registered with --placement control runs here whatever the runner, within the control instance's limits (orca.control); one with --placement ec2 waits on a scheduler whose runner isn't ec2. One with --placement worker:<pool> is never started here: a remote worker of the pool claims its queued runs over the UI's API (orca.workers), and the scheduler queues its due retries and its dead runs again for one, its fill runs too (held for a worker, fill_max_runs across the pool), and a run whose worker is gone whatever its trigger.
  • Overlapping instances (orca.instances, docs/overlap.md). With ORCA_OVERLAP=on a deploy starts the new image's scheduler beside the old one: the newest live instance leads and alone decides, and an older one starts nothing more, supervises its runs until they end (or kills them after ORCA_DRAIN_SECONDS) and exits. A local or control-placed run records the instance that started it; while that instance lists it the run is live (orca.live.held_sql), counted against max_runs, the control slots and the grants, and left alone. Once the instance is gone and the run's heartbeat stale, it's queued again whatever its trigger, audited.
  • Sensors and rebuilds (orca.sensors). A sensor pipeline's pass looks for snapshots of the datasets it reads, in orca.commits on every tick and from <binary> sense every ORCA_SENSE_SECONDS (asked in a child the next ticks check on, killed after ORCA_BINARY_SECONDS), and queues a run, trigger sensor, for those it hasn't seen; with keys_per_run above 1 the keys go first to its queued sensor runs that haven't started, up to that, then to new runs of that many (#240). A keyed sensor's keys dataset is sensed too: a new snapshot of it queues the keys that aren't done, in runs of keys_per_run. Each rebuild queues its next level once the one before has succeeded.
  • A snapshot it can't run is seen all the same. One whose keys aren't partitions of the sensor's kind (a week in orca.partitions, or names for a dated sensor) is recorded in orca.sensed with no run and why, and the others the tick saw are run. A pass that fails, such as a sense that exits non-zero, is kept as the pipeline's sense_error, which orca ui shows, and printed once rather than on every tick.
  • Fills (orca.fill). A pipeline registered with fills = "all" in orca.toml has a second pass after its other runs': runs with trigger fill, keys_per_run partitions each, over the partitions of its range no run has taken (fill_status), up to fill_max_runs beside max_runs, none while its other runs are queued, it's paused, outside fill_window or over fill_budget_per_day.
  • Retries. A scheduled, backfilled, sensed or fill run that failed is started again after the pipeline's retry_seconds (else ORCA_RETRY_SECONDS, default
    1. doubling with each start, at most its max_retry_seconds if set, plus up to a tenth more as jitter, up to run_retries times. One whose process died (its heartbeat went stale) is started again at once, within the same budget. A manual, UI or rerun run is never retried by the scheduler: one whose process died ends failed, with the error interrupted: its process stopped, so it can be resumed and holds its keys no longer. A run whose step raised defer ends deferred with retry_at the time it asked for, and is started again then, whatever its trigger; those starts (deferrals) don't count against run_retries. A run asked to cancel whose process died ends cancelled.
  • New code. orca register of a new build moves the queued runs to it, and the failed ones waiting for a retry that have no step done. A retry of a run with steps done by other code would be refused by the binary's resume, so the scheduler fails it at once instead of starting it, with an error (runs.error) saying to resume it with --allow-new-code. A local run whose process exits before claiming its run fails with the exit code and the last line of its driver log as its error; one whose process crashes after (a segfault, abort) has the attempts it left running failed with the crash's first frames (orca.crash).

The scheduler's clock is orca.civil.now, which ORCA_NOW fixes; fire times and retry times are on it. Heartbeats are on the database's clock. A run's output goes to <ORCA_LOGS>/<pipeline>/<run>/driver.log when ORCA_LOGS is set: written there by a local run, published there when the process is reaped if ORCA_LOGS is in object storage, or kept from the driver pod's log on Kubernetes (orca.kube). Beside a local run's process the scheduler starts <binary> sample (orca.sample), which writes the machine's metrics to .../<run>/metrics/local.jsonl until the process ends; ORCA_SAMPLE_SECONDS=0 turns it off, on every runner. Apart from that, each tick reads each local and control-placed run's process tree in /proc, and once the process is reaped its attempts get their peak memory and CPU (orca.usage). orca scheduler gives each local and control-placed run a cgroup of its own where it can (orca.cgroup): a memory limit at its grant, an out-of-memory kill that fails its attempts saying so, and a retry that asks for more.

Structs

Scheduler

struct Scheduler

Implements Movable

Fields
db: State
stale: Float64
retry_seconds: Int64
sense_seconds: Float64
start_grace: Float64: How long a run the scheduler started may go without a heartbeat.
budget: Int: ORCA_VCPU_BUDGET in thousandths of a vCPU; 0 for none.
runner: Optional[KubeRunner]: Set with ORCA_RUNNER=kubernetes.
ec2: Optional[Ec2Runner]: Set with ORCA_RUNNER=ec2; with neither, runs are local processes.
pools: PoolScaler: The pool autoscaler's state between passes (orca.pool_scale).
logs: String
wait_for_binaries: Bool: Whether a tick waits for the binaries it asks (orca tick, which has no later tick to collect them), rather than checking next tick.
expire_seconds: Float64: How often every dataset is expired by its own policy (ORCA_EXPIRE_HOURS, 24; 0 turns it off).
maintain: String: The orca_maintain binary that expires, or empty for none.
fleet_seconds: Float64: How often the fleet page's samples are read from the instances' files (ORCA_FLEET_SECONDS, 30; 0 turns it off).
watchdog_seconds: Float64: How often the watchdog's rules are checked (ORCA_WATCHDOG_SECONDS, 60; 0 turns it off): orca.watchdog.
limits: ControlLimits: What a control-placed run may use (orca.control).
room: Room: What this host can grant the runs of pipelines with a request (orca.resources): its capacity less its reserve.
psi: Source: Where this host's memory pressure is read from (orca.pressure), found once; none on macOS or without PSI.
admission: Admission: Whether runs may start on this host for its memory pressure, from each tick's reading.
cgroups: Cgroups: Where its runs' cgroups go (orca.cgroup); off until contain.
instance: Int: This scheduler's row of orca.schedulers (orca.instances), or 0 when it isn't an instance: ORCA_OVERLAP off, or orca tick.
leading: Bool: Whether this instance holds the lead (always, for no instance).
draining: Bool: Whether this instance drains: it starts nothing, and exits once the processes it started have ended.
drain_seconds: Float64: ORCA_DRAIN_SECONDS (3600): how long a drain waits for its runs before it kills what's left.
finished: Bool: Whether a drained instance has ended: orca scheduler exits.

__init__

def __init__(out self, url: String)

join

def join(mut self)

With ORCA_OVERLAP=on, makes this scheduler an instance (orca.instances): it leads only once it holds the lead.

reconnect

def reconnect(mut self, url: String)

A new connection after a tick failed, keeping what this instance supervises; the lead, which was the old connection's, is taken again on the next tick if no newer instance has it.

say_pressure

def say_pressure(self)

Says once, for orca scheduler, where this host's memory pressure is read from and what it holds (orca.pressure).

contain

def contain(mut self)

Turns on a cgroup per run (orca.cgroup) where this host lets orca make them, saying once which, and picks up the empty cgroups an earlier scheduler left. orca scheduler does this; orca tick and the other commands don't, as they don't reap what they start.

backfill

def backfill(mut self, pipeline: String, keys: String, rerun: Bool = False, allow_append: Bool = False) -> Queued

Queues a run for each partition in keys that has none; see backfill.

Aliases

comptime AUTO_ONE_AT_A_TIME = "waiting: max_runs = \22auto\22 starts one run at a time until its runs have a request to weigh (cpus, memory_gb, learned or declared)"

Why a queued run of a max_runs = "auto" pipeline with no request waits while another runs.

comptime RETRIED_SQL = "trigger IN ('schedule', 'backfill', 'sensor', 'fill')"

_RETRIED, for orca.actions.