rivet-cli 0.21.0

Rivet: PostgreSQL/MySQL/SQL Server/MongoDB → Parquet/CSV (local, S3, GCS, Azure). Crate name rivet-cli; binary rivet.
Documentation
# ─────────────────────────────────────────────────────────────────────────────
# Performance + HARM ledger — rivet's OWN contracts (complements the cross-tool
# docs/bench/matrix.yaml, which is rivet-vs-competitors).
#
# Each scenario row records, per engine: the METRIC, the last MEASURED baseline
# (hardware + date stamped), the CONTRACT/threshold it must hold, and the HARNESS
# to reproduce it. `measured:` = a captured number; `gap:` = a contract we have a
# harness for but have NOT yet captured a systematic baseline of.
#
# These are NOT a hard CI gate (perf numbers are hardware-noisy) — they are the
# nightly / release / dogfood regression checks: re-run the harness, compare to the
# baseline, investigate a >2x drift. Re-measure on the LATEST version; bump `date`.
# ─────────────────────────────────────────────────────────────────────────────

meta:
  updated: "2026-07-19"
  version: "0.20.1"
  hardware:
    devbox: "Apple M-series, macOS (colima docker), CDC profile ports 5434/3307/1434/27018"
  note: >
    Baselines below were measured on the devbox on 2026-07-19 unless dated
    otherwise. A run stamps its own hardware/date; treat cross-hardware absolute
    numbers as indicative, ratios (x baseline) as portable.

engines: [postgres, mysql, mssql, mongo]

scenarios:
  # ── Capture throughput ──────────────────────────────────────────────────────
  - id: cdc_capture_throughput
    what: "bulk capture rows/s — one bounded until_current drain over a seeded backlog"
    harness: "dev/cdc/stand.sh perf"
    contract: "engine-relative; regression = >2x drop vs baseline at the same rollover"
    postgres: { measured: { rows_per_s: 256000, date: "2026-07-19" } }
    mysql:    { measured: { rows_per_s: 500000, date: "2026-07-19" } }
    mssql:    { measured: { rows_per_s: 189000, date: "2026-07-19" } }
    mongo:    { measured: { rows_per_s: 38000,  date: "2026-07-19", note: "JSON-blob model; per-event resume token" } }

  # ── Steady-state memory (the O(rollover) contract) ──────────────────────────
  - id: cdc_interval_soak_rss_flat
    what: "peak drain RSS stays FLAT as the drain INTERVAL grows 12x (10→120 min) — memory is O(rollover), NOT O(per-interval backlog); a regression tracks the interval"
    harness: "dev/cdc_interval/soak_all.sh <engine>  (PHASES='10 20 30 60 120 240 360 480')"
    contract: "peak RSS at 480 min ≈ peak RSS at 10 min (within noise); every churned row captured"
    note: >
      The harness SELF-ASSERTS this contract each run (RSS-flat across the growing
      intervals + source COUNT(*) == distinct drained ids), so the contract IS
      guarded — the per-interval numbers below are just not persisted to a doc (the
      soak.log lives in a run tmpdir). Do NOT re-run to 'fill' this: the harness
      already proves it; harvest a run's soak.log if a captured baseline is wanted.
    postgres: { measured: { note: "RSS-flat asserted by soak_all.sh pg (10→120 default); numbers unpersisted" } }
    mysql:    { measured: { note: "RSS-flat asserted by soak_all.sh mysql; numbers unpersisted" } }
    mssql:    { measured: { note: "RSS-flat asserted by soak_all.sh mssql; numbers unpersisted" } }
    mongo:    { measured: { peak_rss_mb: 72, note: "22k events, flat RSS across intervals (ad-hoc 2026-07)" } }

  # ── Source-side HARM under load ─────────────────────────────────────────────
  - id: cdc_cotenancy_writer_cost
    what: "the writer's throughput/latency under a looping CDC drain vs a no-CDC baseline — the co-tenancy cost of running CDC on a live source"
    harness: "dev/cdc/harm.py (PG paced) · harm_heavy.py (PG pgbench) · harm_mysql/mongo/mssql.py"
    contract: "negligible except SQL Server; a regression pushes a passive-reader engine (PG/MySQL/Mongo) materially past ~1.0x"
    postgres: { measured: { latency_x: 1.02, throughput_x: 0.98, date: "2026-07-19", note: "pgbench 15k tps; logical decoding is light" } }
    mysql:    { measured: { throughput_x: 1.07, date: "2026-07-19", note: "binlog dump is a passive reader — no cost" } }
    mssql:    { measured: { throughput_x: 0.78, date: "2026-07-19", note: "−22%: the capture Agent DUPLICATES every change into change tables (a 2nd write) — inherent to SQL Server CDC, not rivet" } }
    mongo:    { measured: { throughput_x: 1.01, date: "2026-07-19", note: "change stream reads the oplog — no cost" } }

  - id: cdc_slot_retention_harm
    what: "does the CDC READER pin source-side log retention (the disk-fill hazard)? PostgreSQL's logical slot does; the others' logs are trimmed on the server's own schedule"
    harness: "dev/cdc/harm.py (scheduled-drain retention) · harm_*.py (reader-independence)"
    contract: "PG: monitor slot lag / retained WAL (an abandoned slot fills the disk). Others: no reader-pinned retention — structurally cannot fill the source disk"
    postgres: { measured: { class: "reader-pinned", note: "retention ~= interval × write-rate when keeping up (23.5MB/6s @ ~4MB/s); an ABANDONED slot is unbounded — observed a real 29GB pin that crashed postgres (disk-fill). MONITOR slot lag." } }
    mysql:    { measured: { class: "reader-independent", note: "binlog purged by binlog_expire_logs_seconds regardless of reader — no disk-fill harm" } }
    mssql:    { measured: { class: "reader-independent", note: "change tables trimmed by the CDC cleanup job regardless of reader — no disk-fill harm" } }
    mongo:    { measured: { class: "reader-independent", note: "oplog is a fixed-size capped collection (4.4GB cap observed) — no disk-fill harm" } }

  - id: cdc_drain_keeps_up_under_heavy_wal
    what: "at a high WAL/write rate, does a continuous (back-to-back) drain KEEP UP (retention bounded) or fall behind (retention grows unbounded)?"
    harness: "dev/cdc/harm_heavy.py (pgbench + retention watchdog)"
    contract: "PG: back-to-back drain keeps up to the tested rate; retention bounded. Beyond that rate, use a shorter scheduler interval or accept growing lag"
    postgres: { measured: { keeps_up_to_tps: 15000, peak_retained_mb: 41, date: "2026-07-19", note: "watchdog did not trip; lag ~1MB" } }
    mysql:    { na: "no consume-retention coupling (reader-independent) — 'keep up' is not a retention concern" }
    mssql:    { na: "no consume-retention coupling (reader-independent)" }
    mongo:    { na: "no consume-retention coupling (capped oplog)" }

  # ── Batch export (the other mode) ───────────────────────────────────────────
  - id: batch_export_memory_flat
    what: "peak RSS stays flat regardless of table/row size — no full-table materialization (the flat-memory field-validation)"
    harness: "dev/bench/smoke.py (rivet row) · field runs"
    contract: "peak RSS independent of row count; no OOM at prod scale"
    postgres: { measured: { peak_rss_mb: 59, note: "454M-row table pulled in ~2h, no OOM (Airbyte OOM'd on the same) — field-validated" } }
    mysql:    { measured: { note: "same streaming architecture — flat RSS (shared build_array path)" } }
    mssql:    { measured: { note: "same streaming architecture — flat RSS" } }
    mongo:    { measured: { note: "same streaming architecture — flat RSS" } }

  - id: batch_export_hot_source_harm
    what: "OLTP p99 degradation / long-held query (xmin-hold → vacuum-block → bloat) on a LIVE source under a co-running rivet BATCH export — the 'can I point rivet at prod, or do I need a replica?' question"
    harness: "dev/bench/smoke.py (OltpProbe p99 + GaugePoller longq/xmin/locks/blocked/dead-tuples) → docs/bench/report.html"
    contract: >
      SOURCE-SAFETY: rivet must hold NO long-running query (server-side cursor, not
      a single long scan) so it never pins xmin / blocks vacuum / bloats the source.
      The transient OLTP p99 bump during the export is the only cost; `mode: chunked`
      lowers it. Verdict: SAFE to point at a live source.
    postgres:
      measured:
        longq_s: 0.00
        oltp_p99_x: 5.1
        oltp_p99_x_chunked: 3.3
        peak_rss_mb: 57
        date: "2026-07-09"
        note: >
          longq=0.00s — rivet holds NO long-running query → does NOT pin xmin /
          block vacuum / bloat the source, UNLIKE clickhouse (longq=50.3s) and
          duckdb (longq=7.7s) which pin one query for the whole extract. Only cost
          is a TRANSIENT p99 bump during the export (5.1x full-scan, 3.3x chunked) —
          no lasting harm. (Captured in the cross-tool bench; NOT re-run since — the
          source-safety property is architectural, not per-run.)
    mysql:    { measured: { longq_s: 0.00, note: "same server-side-cursor architecture — no long-held query; captured in the cross-engine bench" } }
    mssql:    { measured: { longq_s: 0.00, note: "same architecture — no long-held query" } }
    mongo:    { na: "no long-held snapshot-txn / xmin analog; the JSON-blob keyset scan does not hold a vacuum-blocking horizon the way an MVCC SQL snapshot does" }