README
¶
Load tests
Load/scale harnesses for Dexaflow's control plane. The README calls load tests the remaining Phase 6 gap; this directory is where they land, one experiment at a time.
Each experiment is a small package main that drives the real control-plane
code (not a mock) against a real Postgres and prints a table. They boot no
cluster and no executor, so they run on a laptop with just Postgres up.
Experiments
| # | Name | Dir | Status |
|---|---|---|---|
| 1 | Scheduler ceiling — per-tick cost vs. active runs N |
scheduler_ceiling/ |
✅ implemented |
| 2 | Dispatch throughput — queue depth vs. arrival rate | — | not yet (see Adding an experiment) |
| 3 | Reaper cost — orphan/heartbeat sweep vs. run count | — | not yet |
| 4 | API read fan-out — dashboard queries under many DAGs | — | not yet |
| 5 | Log tailer — SSE fan-out under many concurrent viewers | — | not yet |
Only Experiment 1 is built. The others are documented extension points.
Prerequisites
A migrated Postgres with the default tenant (migration 001 seeds it).
# 1. Postgres (leoflow/leoflow on :5432, database `leoflow`)
docker compose -f docker-compose.dev.yaml up -d postgres
# 2. Apply migrations (installs the migrate CLI first if you don't have it:
# make install-migrate)
export DATABASE_URL='postgres://leoflow:leoflow@localhost:5432/leoflow?sslmode=disable'
migrate -path migrations -database "$DATABASE_URL" up
Any migrated database works — point DATABASE_URL at it. make test-db (the
leoflow_test database) is a fine target too.
Experiment 1: scheduler ceiling
The leader scheduler runs Scheduler.Step once per tick (~1s). Step's dominant
read is Store.ActiveRuns, which issues 1 + 2N DB queries (one list, then
GetDagVersionByID + ListTaskInstancesByRun per run) and N spec unmarshals,
then advances every run serially. Experiment 1 measures how one tick's
wall-time grows with the number of active runs N, so we can find where a tick
approaches the loop interval (the ceiling).
The harness seeds N synthetic DAGs, each with one run pinned running and a
running task instance. Such a run stays in the active set every tick while
advance stays a no-op for it (nothing to dispatch, nothing to finalize) — so
what is timed is the steady-state read + per-run advance cost. N values are
seeded incrementally (ascending), so measuring 50,200,500,1000 seeds 1000
DAGs total, not 1750.
DATABASE_URL='postgres://leoflow:leoflow@localhost:5432/leoflow?sslmode=disable' \
go run ./test/load/scheduler_ceiling --n 50,200,500,1000 --window 5s
Flags:
| Flag | Default | Meaning |
|---|---|---|
--db |
$DATABASE_URL, then $LEOFLOW_DATABASE_URL |
Postgres URL |
--n |
50,200,500,1000 |
active-run counts to measure (sorted ascending) |
--window |
5s |
per-N window; Step is called back-to-back until it elapses |
--tasks |
1 |
task instances per synthetic run |
--warmup |
3 |
warm-up ticks discarded before timing each N |
--keep |
false |
keep the synthetic DAGs/runs instead of deleting them on exit |
Output is a table of N → loop-duration p50/p95/p99/max/mean plus the step-down
delta observed during each window. The harness cleans up every DAG it created on
exit (best-effort); a fresh migrate up on an empty database is always the clean
slate.
Known limitation: dexaflow_scheduler_loop_duration_seconds is not wired
The declared SLI dexaflow_scheduler_loop_duration_seconds is defined in
internal/observability/metrics.go but is never Observed inside the scheduler
loop (grep confirms: only metrics_test.go observes it). A /metrics scrape
today would therefore report an empty histogram for it. Until that observation is
wired into Scheduler.tick/Step, this harness times Step directly (the
faithful equivalent) and also feeds each measured duration into the real
histogram object, so a future scrape — or a one-line follow-up that moves the
Observe into the loop — sees the same distribution. dexaflow_scheduler_step_downs_total
is scraped from the live registry (it stays 0 here: leadership never churns in
the harness). dexaflow_dispatch_queue_depth is a dispatcher gauge and is out of
scope for Experiment 1 (no dispatcher is wired).
Adding an experiment
- Create
test/load/<name>/main.goas its ownpackage main. - Reuse the storage/scheduler/observability packages the way
scheduler_ceilingdoes — connect withstorage.NewPostgres, seed viastorage.Repository/storage.SchedulerStore, and read metrics from aprometheus.NewRegistry()you own. Do not modify product code. - Keep it cluster-free: prefer seeding rows directly over booting executors.
If a signal genuinely needs the full server (e.g. the SSE tailer for
Experiment 5), boot
dexaflow litethe waytest/e2e/lite-*.shdoes and scrape its/metrics, and say so in this table. - Add a row to the Experiments table above and a section here.
- It must
go build,go vet, andgofmtclean, and actually run.
Directories
¶
| Path | Synopsis |
|---|---|
|
Command scheduler_ceiling is Load Experiment 1: the scheduler ceiling.
|
Command scheduler_ceiling is Load Experiment 1: the scheduler ceiling. |