Relay logo

Rust · deterministic DAG scheduler

See a pipeline
schedule itself.

Relay is a from-scratch CI/CD engine whose scheduler is a pure, deterministic DAG executor you can unit-test without running anything. Give it a pipeline and it decides what runs, in what order, what runs in parallel, what gets skipped when a job fails, and what is served straight from a content-hash cache. Pick a preset below and press Run to watch the same rules resolve a job graph, live.

Run the visualizer View on GitHub
How to use this playground

Pick a preset to load a job graph: a diamond DAG, a failure that skips a subtree, a wide fan-out, or a cache hit. The jobs slider sets the concurrency limit (how many jobs may run at once), and fail-fast stops scheduling new work after the first failure.

Press Run and watch each node move through pending, running, and then success, failed, skipped, or cached, exactly as the scheduler resolves the graph. The event log and summary show the order and the peak concurrency actually observed against the limit. Reset clears the run.

Live pipeline visualizer

This is the Rust scheduler ported to JavaScript with a simulated executor. Pick a preset, set the concurrency limit, and press Run. Watch jobs go pending, running, then success, failed, skipped, or cached, exactly as the engine resolves them.

jobs 2
pending running success failed skipped cached

Event log

Summary

Press Run.
Peak concurrency observed: 0 (limit 2)

It schedules by rule, not by luck

The core is one function, schedule::run(&pipeline, &mut executor, limits) -> Report. The scheduler never spawns a process or a thread. It drives an Executor trait in virtual time, so a MockExecutor that records what ran concurrently lets every rule be asserted deterministically. Swap in the ShellExecutor and the same scheduler runs real commands.

$ relay run examples/failure.relay
  [ 0] setup                ok       success
  [ 1] unit                 xx       failed
  [ 2] docs                 ok       success
       package              --       skipped
       deploy               --       skipped
summary: 2 success, 0 cached, 1 failed, 2 skipped

A failed job marks its transitive dependents skipped and they never run, while independent branches keep going. Parallelism is real and bounded: the mock proves the peak concurrency is exactly the limit and never more. Cache keys are a hand-rolled FNV-1a digest of a job's config plus its declared inputs, folded together with every upstream key, so a change in a non-cached upstream busts the downstream key too. Same pipeline, same result and same order, every run, with ties broken by config order.

How it differs

Real CI platforms run your pipeline; Relay is built to let you study and test the one part they hide, the scheduler, without running anything.

GitHub Actions, GitLab CI

Hosted pipelines wired to runners, containers, and the network. The ordering, parallelism, skip, and cache logic is entangled with execution, so you cannot exercise a scheduling rule without actually running jobs.

Jenkins

A mature, plugin-driven automation server. Powerful and heavy, and the DAG behavior lives inside a running controller rather than in a small function you can read end to end.

Relay

Pulls the scheduler out as one pure function over an Executor trait. Every rule, dependency order, bounded parallelism, skip propagation, and upstream-aware caching, is asserted against a mock in virtual time, with no process or thread spawned. Swap in the ShellExecutor and the same scheduler runs real commands. Zero external dependencies, pure Rust std.

The parts that matter

Six guarantees the scheduler keeps.

Each is a real rule in src/schedule.rs, and each has a test that pins it against the mock executor.

pure DAG scheduler

schedule::run drives an Executor trait in virtual time and never spawns a process or a thread. That single seam is what makes every rule testable.

bounded Real parallelism

Up to N jobs run at once. The mock records what ran concurrently and proves the peak is exactly the limit and never more.

safe Skip propagation

A failed job marks its transitive dependents skipped and they never run. Independent branches keep going. A continue-on-error failure does not skip its dependents.

hashed Content-hash cache

A job's cache key is a hand-rolled FNV-1a digest of its config plus the content of its declared input paths. A stored match resolves the job cached and skips execution.

upstream Upstream-aware keys

The key folds in every dependency's resolved key and handed-down artifacts, so a change in a non-cached upstream job busts the downstream key too.

stable Deterministic order

Same pipeline, same result and same order, every run. Ties break by config order, and fail-fast stops scheduling new work after the first failure.

Use it

CLI

relay run, relay plan, and relay graph over a readable .relay pipeline file, with --jobs N and --fail-fast.

Library

The whole engine is schedule::run(&pipeline, &mut executor, limits) -> Report, one function over the Executor trait.

Executors

A MockExecutor that returns scripted outcomes for tests, and a ShellExecutor that runs real commands with per-job working dirs, artifact hand-off, and process-group timeouts.

Pipeline format

A text format with a parser and a matching printer that round-trip. Declared paths are confined to the workspace, so a pipeline cannot read or clobber files outside its base dir.

# the whole test suite runs with no real processes, over the mock executor
cargo test
cargo clippy --all-targets -- -D warnings

# plan the DAG and wave order, run nothing
relay plan  examples/diamond.relay --jobs 2

# run a pipeline: print each status and a summary
relay run   examples/failure.relay

# print just the dependency graph
relay graph examples/diamond.relay