Docs
Scheduling and running
The scheduler starts runs when they're due, retries the ones that fail, and starts each on this machine or on a cluster of its own. Every run is recorded in Postgres, so any of them can be resumed, and its tables say what made them.
The scheduler
orca scheduler links no pipeline. It knows one by the
describe that orca register recorded, and starts its runs as
<binary> resume <run>, so a run records its own progress
exactly as a run started by hand does. It needs ORCA_DATABASE_URL;
the command line lists its commands.
./build/orca register ./build/gharchive
./build/orca backfill gharchive 2025-01-01..2025-01-31
./build/orca scheduler
Each fire is a row with a unique (pipeline, time), inserted with its queued run in one
transaction, so two schedulers ticking at once, or one killed mid-tick, never fire
twice. Fires missed while it was down are made on its next tick, once each. A queued
run starts only while fewer than max_runs of the pipeline's runs are live,
counted under a lock on the pipeline's row, and none start while it's paused. A
scheduled, backfilled or sensed run that fails is started again after
ORCA_RETRY_SECONDS, doubled with each start, plus up to a tenth as jitter,
up to run_retries times; one whose process died is started again as soon
as its heartbeat is stale. A run you started by hand, or from the UI, isn't retried.
The scheduler holds nothing in memory, so it can be stopped and restarted at any time.
Registering a new build moves the pipeline's queued runs to the new code.
On Kubernetes and EC2
By default the scheduler starts a run as a process on its own machine, and the run
forks its Dista workers there. ORCA_RUNNER starts each run on a
cluster of its own instead, from the image the pipeline was registered with:
./build/orca register --image 123456789012.dkr.ecr.us-east-2.amazonaws.com/orca:data \
--head-type c7g.large --worker-type c7g.2xlarge --workers 3 ./build/taxi
ORCA_RUNNER=ec2 ./build/orca scheduler ORCA_RUNNER=ec2 starts a head instance, on demand,
running the Dista head and the driver (<binary> resume <run>),
and --workers worker instances pointed at it; --spot makes
the workers Spot instances, where the account allows them, and
--workers MIN..MAX starts MIN and adds or drains workers up
to MAX as the head's demand changes. Each instance is tagged with its
run, told to terminate when it shuts down, and recorded with its type, when it
started and ended, and why it ended (it shut itself down, it was terminated, or a
Spot interruption). ORCA_RUNNER=kubernetes creates a
driver Job, a head Pod and Service and the worker Pods in
ORCA_RUNS_NAMESPACE, all labelled with the run and owned by the Job.
A pipeline without Dista tasks gets only its driver.
Both clean up in layers. A run's instances or pods end when it does; every tick the
scheduler removes those of runs that aren't running any more and those older than
ORCA_RUN_DEADLINE, so nothing a killed scheduler left behind keeps
running. A run whose head goes is started again on new instances. Before making a
cluster, the scheduler adds up the vCPUs of every live run's pods or instances, and
a run that would take them past ORCA_VCPU_BUDGET waits.
orca aws up makes all of this on AWS as one stack: a control instance
running the scheduler with ORCA_RUNNER=ec2, Postgres and the UI, and
what run instances need (orca aws,
local or AWS?).
Runs in Postgres
With ORCA_DATABASE_URL set, the binary creates and migrates an
orca schema at start-up and records every run, every step of every
partition, and every attempt, with the inputs and outputs of each. Each change of
state is one transaction.
export ORCA_DATABASE_URL=postgresql:///orca
./build/gharchive run --partitions 2025-01-01..2025-01-03
./build/gharchive status 7 # run 7's steps and attempts
./build/gharchive resume 7 resume reruns every step that hasn't succeeded, in order, with its
recorded input; steps that succeeded are read back, not run again. A run started by a
binary with a different registry hash is refused unless
--allow-new-code is passed.
One process owns a run at a time. It holds the run with a heartbeat, written from a
thread with its own connection, and resume refuses a run whose heartbeat
is younger than ORCA_STALE_SECONDS. A run whose process was killed goes
stale and can be resumed; a run that ended can be resumed at once. Each attempt also
checks that its process still owns the run, so a process that lost its run stops at
its next step rather than racing the new owner. tests/kill.sh kills a
run at a random step with kill -9, resumes it, and checks every step
ran to success exactly once.
Postgres must keep synchronous_commit on: with it off, an immediate
restart of the server can lose commits it already acknowledged. The chaos test from
PR #6 found this when a run's row
vanished.
Logs
With ORCA_LOGS set to a directory, each attempt writes
<pipeline>/<run>/<step>/<partition>/<attempt>.log:
its start, its end, its error, and its ctx.log lines with UTC
timestamps. The path is recorded with the attempt and shown by
status <run>. A run with Dista tasks writes the runtime's timeline
beside them as timeline.json, in Chrome's trace format, which Perfetto
opens. The scheduler keeps a run's driver output as driver.log, and an
EC2 run's head and workers upload theirs to
cluster/<role>-<instance>.log every
ORCA_CLUSTER_LOG_SECONDS while they run, as an instance can go without
notice.
ORCA_LOGS and ORCA_WAREHOUSE can be locations in object
storage, s3://bucket/prefix, configured from the AWS_*
environment or the instance's role. An object can't be appended to, so a log is
written to a file under ORCA_LOG_STAGE and uploaded when it's complete:
an attempt's when the attempt ends, a timeline when the run does. The UI reads them
back from wherever they are.
Machine metrics
Beside every process doing a run's work, the runner starts
<binary> sample, which writes a JSON line of the machine's CPU,
memory, disk, disk I/O and network every ORCA_SAMPLE_SECONDS to
<pipeline>/<run>/metrics/<instance>.jsonl under
ORCA_LOGS. A run's page draws them as its Machines section, on the same
time axis as its steps, and marks the stretches where memory use or a disk was over 90%
or CPU steal over 20%, which explain most slow or failed runs.
Lineage
Every read and commit a step makes is recorded with its dataset, so lineage is two walks over them. Back from a snapshot: the run, step, partition and code that made it, the snapshots that attempt read, what made each of those, and so on, to sources no Orca run made. Forward: everything made from it. Both stop after 20 steps.
$ ./build/orca lineage lake.report
lake.report@3141592653
made from:
0 lake.report@3141592653 by report run 9 report code 0x5e1f... from lake.clean@2718281828
1 lake.clean@2718281828 by clean run 8 clean 2024-02 code 0x9a0c... from lake.raw@1618033988
2 lake.raw@1618033988 by ingest run 6 load 2024-02 code 0x77d2...
made from it:
nothing Rebuilds
$ ./build/orca rebuild raw.events 2024-03
rebuild 1
level 0 clean 2024-03
level 1 report 2024-03 A rebuild runs every pipeline downstream of a dataset, a sensor or not, over the partitions given. Each pipeline is placed at its longest distance from the dataset, so one that reads two rebuilt datasets runs after both, and the scheduler queues a level once every run before it has succeeded. A run that fails stops the rebuild; resuming it from the UI carries on. The rebuild's own commits don't also start the sensors it reruns, so each pipeline runs once.
Environment
| Variable | What it sets | Default |
|---|---|---|
ORCA_DATABASE_URL | the Postgres database runs are recorded in; unset, a pipeline's run runs in memory | unset |
ORCA_STALE_SECONDS | how old a run's heartbeat must be before another process may resume it | 30 |
ORCA_WORKERS | the local Dista workers a run forks | 4 |
ORCA_LOGS | where attempts' logs, timelines, driver logs, instance logs and metrics go: a directory or s3://bucket/prefix | unset |
ORCA_LOG_STAGE | where a log bound for object storage is written until it's complete | $TMPDIR/orca-logs |
ORCA_WAREHOUSE | the root of the filesystem catalog datasets are found in, a directory or s3://; ns.table is <warehouse>/ns/table | unset |
ORCA_RETRY_SECONDS | the scheduler's first wait before retrying a failed run, doubled with each start | 60 |
ORCA_SENSE_SECONDS | how often the scheduler asks a sensor's binary for its datasets' current snapshots | 60 |
ORCA_SAMPLE_SECONDS | how often a run's sampler writes a line of machine metrics; 0 turns it off | 5 |
ORCA_RUNNER | where the scheduler starts runs: local, kubernetes or ec2 | local |
ORCA_VCPU_BUDGET | the vCPUs all live runs' pods or instances may use together; a run that would go over waits | no limit |
ORCA_RUN_ENV | the scheduler's variables, by name and comma-separated, copied to every run's pods or instances | unset |
ORCA_RUN_DEADLINE | seconds after which a run's pods or instances are removed whatever their state | 86400 |
ORCA_IMAGE | the image a run's binary is in, recorded with its version; the runners set it | unset |
ORCA_EC2_HEAD_TYPE | the head instance's type, unless registration gave one | c7g.large |
ORCA_EC2_WORKER_TYPE | the worker instances' type, unless registration gave one | c7g.xlarge |
ORCA_EC2_AMI | the image run instances boot | unset |
ORCA_EC2_SUBNET, ORCA_EC2_SECURITY_GROUP, ORCA_EC2_PROFILE | where run instances go, their security group, and their instance profile | unset |
ORCA_EC2_DATABASE_URL | the database URL run instances use, such as the control instance's private address | ORCA_DATABASE_URL |
ORCA_EC2_PREAMBLE | shell run on each instance before the pipeline, such as an ECR login | unset |
ORCA_EC2_MODE | docker runs the image on the instance; direct runs the binary, for the tests' stand-in EC2 | docker |
ORCA_CLUSTER_LOG_SECONDS | how often a run instance uploads its head's or worker's log | 30 |
ORCA_RUNS_NAMESPACE | the Kubernetes namespace runs' pods go in | orca-runs |
ORCA_UI_ADDR | host:port orca ui listens on | 127.0.0.1:8270 |
ORCA_UI_LOGIN | required makes the UI's actions need a login even before any user exists | on once a user exists |
ORCA_UI_SESSION_HOURS | how long a login lasts | 12 |
ORCA_UI_SECURE_COOKIES | 1 marks the session cookie Secure, for a UI behind HTTPS | unset |
ORCA_UI_TRUST_PROXY | 1 takes a request's address from X-Forwarded-For, behind a proxy you run | unset |
ORCA_UI_URL | the UI's public address, for the Open in Perfetto link | unset |
ORCA_DOCS_URL | where the UI's Docs links point | https://mlake.dev/s/orca/docs/ |
ORCA_NOW | fixes the clock of fires and the scheduler, as 2024-03-10T07:00Z or epoch seconds, for tests | the clock |
ORCA_ZONEINFO | where time zones' TZif files are read from | /usr/share/zoneinfo |
DISTA_ROLE | head or worker: the binary runs as that Dista process instead | unset |
DISTA_HEAD | host:port of a Dista head to join instead of forking workers | local mode |
When things fail
A step that raises is retried up to its retries, then
fails. The steps downstream of it are skipped, while other partitions' steps that
don't depend on it carry on, so one bad month doesn't stop the others; the run ends
failed and can be resumed. A process that dies leaves its run
running with a stale heartbeat; resume marks the interrupted attempt
failed and continues from there. One that crashed has its attempts failed with its
stack dump's first frames as soon as its runner sees it end
(Debugging failures). A commit that landed before the crash
is found by the retry rather than made again. Fuzzing, and a chaos test that kills
runs, restarts Postgres and races two resumes over many seeds, run in
CI.