Repository navigation
Upgrading from 0.32.1 — read these first:
-
State migration v37 → v38 (the load spec keyed by export, table and source). The first run
of 0.32.2 migrates the state and leaves the v37 copy beside it; 0.32.1 refuses a v38 state
(RIVET_STATE_SCHEMA_NEWER), so the way back is that copy (docs/reference/state.md, "Rolling
back an upgrade"). -
A run now waits for a held lease for up to
--lock-waitseconds (default 600, also
RIVET_LOCK_WAIT) instead of refusing at once; when the wait runs out it stops with the new
RIVET_STATE_LOCK_TIMEOUT, exit 2. Pass--lock-wait 0to keep the immediate refusal. -
New refusals:
- two configs on one CDC checkpoint:
RIVET_SOURCE_CDC_CHECKPOINT_SHARED, exit 5, before a change
is read (a checkpoint 0.32.1 wrote is claimed by the first run of 0.32.2 that reads it); rivet initover a CDC checkpoint or destination another config names:RIVET_INIT_CDC_NAME_TAKEN,
exit 5, nothing written;rivet load/rivet compactwithout aload:block:RIVET_LOAD_BLOCK_MISSING, exit 1 (was
uncoded);- a whole-table pass over a base-and-buffer base that still holds uncompacted changes:
RIVET_LOAD_BUFFER_NOT_COMPACTED. - a 0.32.2 command started while a 0.32.1 run holds a lease on the same state:
RIVET_STATE_UPGRADE_BESIDE_LIVE_RUN, exit 2, after waiting up to--lock-wait; the state is
not migrated, so the 0.32.1 run and its next run go on (upgrade once it ends).
- two configs on one CDC checkpoint:
-
rivet init --mode cdc -o <file>names the slot,server_id, checkpoint and destination after
the file (-o rivet.yaml->./cdc/rivet_<table>.ckpt,./output/rivet/<table>_cdc/). Existing
configs are read as before. -
The SQLite lease file is named by a hash of its key; until 0.34.0 a run also holds the 0.32.1
name, so a 0.32.1 run still going when 0.32.2 starts the same export is refused as before. -
PostgreSQL CDC: the first 0.32.2 run of a stream 0.32.1 left with its slot past its checkpoint
takes the slot's position once (a warning names both); a slot another reader advanced is refused
RIVET_SOURCE_CDC_LOG_GAPas in every 0.32.x. -
Every MySQL run opened an extra source connection to detect a proxy. The fix that keeps
ProxySQL multiplexed (0.32.2, literal session SETs) detected the proxy on a connection dialled for
that alone, so a batch run opened 4 MySQL connections instead of 2 and a CDC run 3 instead of 2.
Now the proxy is detected once per source URL per process, on the pool's own first connection,
by statements that keep a ProxySQL session multiplexed (PROXYSQL INTERNAL SESSION,
SHOW GLOBAL VARIABLES LIKE …in place ofSELECT @@…): a batch run opens 2 and a CDC run 2
again. What to do: nothing. -
A rivet with a newer state schema, started beside a live run of an older release, migrated the
state under that run, even when it was then refused. Every command opened and migrated the state
before it took its lease, so arivet runorrivet loadrefused as busy (is writing,
RIVET_STATE_RUN_IN_PROGRESS) had already moved the state to the new schema, and the next run of
the older release on that state was refusedRIVET_STATE_SCHEMA_NEWER(every release with a state
migration; found by the 0.32.2 pre-release gate on the v37 → v38 migration). Now a migration
first asks whether another live process holds a lease on the state (a locked lease file beside a
SQLite state; an unexpiredstate_leaserow of a live holder on a PostgreSQL state). While one
does, it waits up to--lock-waitand then stops with the newRIVET_STATE_UPGRADE_BESIDE_LIVE_RUN
(busy, exit 2), the state not migrated and nothing written. Behaviour change: the first command
of a release with a newer state schema, started while a run of the older release holds any lease
on the same state, waits for that run or stops by this code instead of migrating the state beside
it; a state no live run holds is migrated as before, and state written before the upgrade is
unchanged. What to do: nothing; a scheduler that retries exit 2 runs the upgrade once the older
run ends. -
A MySQL CDC config written by
rivet init --mode cdcwas refused when another database on the
server held a table of the same name.initwrites the tables unqualified (tables: [orders]),
and every stream run stopped withRIVET_SOURCE_CDC_TABLE_UNRESOLVED("ordersis unqualified …
<other_db>also holds a table of that name"), at the catalog check or on the first foreign event
in the binlog (0.32.1 and earlier). Now a bare name means the connection's own database when
that database holds the table, and the same name's events from any other database are not
captured. When the connection's database does not hold it and exactly one other database does,
the stream captures that one (0.32.1 stopped there with MySQL error 1146 at the table probe).
With none or several, it is refused as before. The catalog check, the table probe, the schema
read, the baseline/backfill read and the binlog routing take that one rule, so the baseline and
the stream read one table. The destination, the warehouse tables and the state of an existing
stream are unchanged. What to do: nothing; a config that was refused for this runs as
generated. -
A first CDC load into a BigQuery base-and-buffer table reported nothing loaded. The load landed
the baseline in the base, but itsper_table[]entry (--json,--summary-output) and its
CDC LOAD OKline counted only the stream leg:rows: 0,target: <table>__changes, "no changes
buffered" for a 6000-row baseline (0.32.0 and 0.32.1). Nowrowsis both legs' rows,target
is the base when a baseline landed,buffernames the change log when the same load also
appended changes (ADR-0039 D3, amended), and the line says what the baseline landed and where.
What to do: nothing; a scheduler that readtargetof a CDC load as the change log now gets
the base on a cycle that landed a baseline. -
Harness / release process: a tag could be cut on a commit the full gate never graded, with seeded
recall,cargo auditand the generated docs outside the gate. Seeded-defect recall was opt-in, so
24 seed patches had stopped applying to main unseen. Nowmake release-oracle-fullruns the
recall (a MISSED or STALE seed fails the gate),cargo auditanddocgen --checkare gate stages,
the gate records its verdict with the full commit, its kind and whether the tree was clean at its
start, anddev/pytools/release.pyrefuses to tag a commit without a clean RELEASE-READY
release-oracle-fullrun of exactly that commit. The 24 patches are re-cut, and the offline
self-test fails a PR whose seed no longer applies. What to do: release from a commit the full
gate passed; pass--gate-record FILEwhen the gate ran on another machine. -
Harness: the rig oracle read rivet's ledger from the first schema that held a table of its name.
A schema an older test left in the stand's PostgreSQL state (rivet_wayback_nocreate) came first
forfile_log, so every cell graded on that state readfile_log records 0 of this run's N declared part(s)(the pool-ledger and state-ledger reds). Now the oracle resolves each table through the
state connection'ssearch_path, as rivet does, and the seeded recall's runs use its scratch state
DB under every state knob, never the shared one. What to do: nothing. -
rivet initscaffolded GCS and S3 destinations but not Azure. Now--azure-container <name> --azure-account <name>writesdestination: type: azurewith the sameprefix: exports/<table>/
(cdc/for a CDC stream),account_name:andaccount_key_env: AZURE_STORAGE_KEY, never a key;
--azure-endpoint <url>addsendpoint:(Azurite). It excludes--gcs-bucket,--s3-bucket
(RIVET_CLI_OPTION_UNSUPPORTED/ a clap conflict, as between those two) and--discover, and it is
a bucket--clickhouse-urlcan stage in. What to do: nothing; setAZURE_STORAGE_KEY(or swap in
sas_token_env:) before the first run. -
Two CDC configs written by
rivet init -ointo one directory shared one checkpoint and one
destination. Through 0.32.1init --mode cdcnamed the checkpoint after the table or schema
(./cdc/<name>.ckpt) and the CDC destinationcdc//./output/cdc/(per table<t>_cdc/)
whatever-osaid, so a second config of the same source, written beside the first, resumed the
first one's position: each stream skipped what the other had read, exit 0. Nowinit -o <stem>.yamlnames the checkpoint./cdc/<stem>_<name>.ckptand the destination
<stem>/cdc//./output/<stem>/cdc/(per tableexports/<stem>/<t>_cdc/), and refuses
RIVET_INIT_CDC_NAME_TAKEN, exit 5, writing nothing, when the checkpoint already exists or another
config in the directory names the same checkpoint or destination. A run records which config and
export own its checkpoint (<checkpoint>.owner) and refuses a checkpoint another config still
reads,RIVET_SOURCE_CDC_CHECKPOINT_SHARED, exit 5, before it reads a change. A checkpoint written
before 0.32.2 has no owner record and is claimed by the first run that reads it; one whose owner no
longer names it is claimed too. What to do: nothing for one config per checkpoint; for two
configs on one checkpoint, give one of them a checkpoint and destination of its own and re-baseline it. -
Two exports whose names differ only in characters a file name cannot hold shared one run lease
on a SQLite state. Through 0.32.1 the lease file was named by a lossy token of its key, so
orders#1andorders-1(ororders xandorders-x, or two Cyrillic names) locked one file, and
with--parallel-exportsone of them was refusedRIVET_STATE_RUN_IN_PROGRESSbeside no live run of
its own. Now the file name carries a hash of the whole key after the readable part
(.rivet_state.db.lease-<token>-<hash>). A PostgreSQL state was not affected. Until 0.34.0 a run
also holds the 0.32.1 name of the file, shared, so a 0.32.1 run still going when the upgraded
binary starts the same export is refused as before; that second take is removed in 0.34.0.
What to do: nothing. -
Fixed: the upgrade from 0.32.1 no longer stops a PostgreSQL CDC stream with a false log gap.
0.32.1 released a trailing span of uncaptured transactions by advancing its slot without saving the
checkpoint, so its slot could sit past its checkpoint. The 0.32.2 guard against another reader of
the slot (#649) read that as changes consumed by someone else and refused the first upgraded run
RIVET_SOURCE_CDC_LOG_GAP, exit 5, with a re-baseline remedy, although nothing was lost. Now a
checkpoint without theconfirmed_firstmark that every 0.32.2 checkpoint carries (one an older
release wrote) is moved to the slot's position once, with a warning naming both positions, and
rewritten in the new form; from then on a slot past the checkpoint is refused as before. 0.32.1
still reads a checkpoint 0.32.2 wrote. What to do: nothing. -
A SQLite state write could still be refused
database is lockedbeside a busy sibling. 0.32.1
made the writers of one state file in one process take turns, but a writer that waited longer than
its patience for a turn (a slow or descheduled holder) left the queue for SQLite's own busy wait,
where a sibling committing back to back took the lock in every gap again: refused after the busy
timeout, exit 1 (seen once under CI load). Now a writer that ran out of patience keeps its
place: no later writer of the process is served before it has written. What to do: nothing. -
A column added to a captured table while a CDC run was open lost its values, exit 0. A
continuous run (until_current: false) froze its schema when it opened, so a hotALTER TABLE … ADD COLUMNleft the new column out of every change the run read after it, and the run acked them
(#644; PostgreSQL, MySQL, SQL Server). Now the first change that names the new column closes
the table's part under the old schema, the table's columns are read again and judged by
on_schema_drift(failstops before the change is acknowledged), and the run continues in a
new part with the column and its values;rivet loadtakes both schemas into one change log.
SQL Server's capture instance cannot carry a column added after it was created, so a run that
sees its table outgrow the instance now stops withRIVET_SOURCE_CDC_PREREQUISITE, the refusal a
run opening on that instance already met, instead of ending clean. What to do: on SQL Server,
re-enable the table with the full column list and re-baseline it; elsewhere nothing. -
rivet loadandrivet compacton a config without aload:block stopped with no code.
Through 0.32.1 they exited 1 withconfig has no top-level \load:` blockand noRIVET_*code, so a scheduler could not tell the config mistake from any other failure. **Now** the stop isRIVET_LOAD_BLOCK_MISSING(usage, exit 1), raised by one function both commands call. **What to do:** nothing; add a top-levelload:` block to load or compact. -
Two
rivet initruns over one MongoDB database could list its collections in different orders.
listCollectionshas no order, and the SQL engines' listings followed each server's collation (SQL
Server's is case-insensitive). Now every engine's listing goes through one ordering in rivet (byte
order of the name), so the scaffold is the same on every run; on SQL Server a schema with mixed-case
names may list its exports in a different order than 0.32.1 did. What to do: nothing. -
The MongoDB reference said BigQuery loads
documentasBYTES. Throughrivet loadit
lands asJSON(only a schema-less autoload givesBYTES).docs/reference/init.mdnow names Oracle
among the enginesrivet initreads. What to do: nothing. -
A second run of an export was refused at once while the first held its run lease. Through 0.32.1
two triggers of one schedule made one of them a failed task (RIVET_STATE_RUN_IN_PROGRESS; a load
beside a load or compact,RIVET_LOAD_TABLE_BUSY). Now the run lease and a load's table lease
are waited on for up to--lock-wait <seconds>(a global flag, orRIVET_LOCK_WAIT, default 600, passed on to process
children), with a log line naming the lease and its holder (pid and host on a PostgreSQL state); when
the wait runs out the command stops with the newRIVET_STATE_LOCK_TIMEOUT, exit 2.--lock-wait 0
keeps the immediate refusal. The staging cleanup still skips a busy prefix and never waits (ADR-0039
D7). What to do: nothing; pass--lock-wait 0where a busy export must fail at once. -
Behind ProxySQL every chunk sent its MySQL session SETs again, some of them expressions. Through
0.32.1 each read of a chunked export sentSET time_zone,SET SESSION sql_mode = REPLACE(@@sql_mode, ..)
andmax_execution_timebefore it and@@global.*resets after it; ProxySQL cannot parse the
expressions, logsUnable to parse unknown SET query, and locks the session to its hostgroup. The
values were already right on 0.32.x (measured through a default-config ProxySQL in front of a +03:00
server: every TIMESTAMP and DATETIME equal to the server's UTC read, single and parallel-4; an off-UTC
session is refused since 0.32.1). Now rivet sets the session by literal SETs only, once per pooled
connection (sql_modeis the server's withoutNO_BACKSLASH_ESCAPES, read once withSHOW GLOBAL VARIABLES), and reads no@@variable on a reading session, so ProxySQL keeps multiplexing it and
replays the tracked variables on whichever backend it picks; a later export with another
statement_timeout_ssends that one literal again. What to do: nothing. -
A SQLite state could only reach PostgreSQL by hand. Through 0.32.1 the only way to move a config to
a shared PostgreSQL state was a hand-written table copy; pointingRIVET_STATE_URLat an empty
database started every export over and left the load journal behind. Nowrivet state copy -c <config> [--to postgresql://…]copies every table of the config's SQLite state into an empty
PostgreSQL state in one transaction, checks each table's row count before the commit, and only
reads the SQLite file (a state of an older release, v31 of 0.30.0 included, is migrated on an
in-memory copy). The next run continues its cursors and CDC streams, andrivet loadkeeps the
tables earlier loads wrote. A target holding state is refused (RIVET_STATE_COPY_TARGET_NOT_EMPTY,
exit 5). A CDCcdc.checkpointfile is not part of the state: it stays where the config names it,
and the copy prints its path. What to do: to move a config, stop its runs, runrivet state copy,
then setRIVET_STATE_URLfor every command of it. -
A scheduler could not turn rivet's own notifications off. Through 0.32.1 a config's
notifications:(an inlinewebhook_url:included) fired on every failed run, so a scheduler that
alerts through its own channel sent each alert twice. Now the global flag--no-notifysends no
notification from the process, and is passed on to every process child--parallel-export-processes
starts (ADR-0039 D9). Every global flag is now either passed on to the children or declared
parent-only, checked against the clap definition. What to do: nothing; a scheduler integration
passes--no-notify. -
The
rivet load/rivet compactdocument said how many rows, not what the warehouse did with
them or what it cost. Now eachper_table[]entry also carriestarget(the object the command wrote, named exactly as
its loader addressed it: a load's table or__changeslog, a compact's base) andbuffer(the buffer a
compact read),rows_inserted,rows_updated,
rows_deleted,rows_merged,base_rows_after,files,runs,warehouse_job_ids,
bytes_processedandbytes_billed(ADR-0039 D3). Every count is what the warehouse or the ledger
returned: a compact's inserted / updated / deleted rows are its BigQuery MERGE jobs'dmlStats(a
script's from its child jobs), the bytes are the jobs'totalBytesProcessed/totalBytesBilled, the
job ids are BigQuery job ids or ClickHouse query ids,files/runsare what the load consumed from
the ledger. A value nobody returned isnull, never 0: the bytes on ClickHouse and Snowflake, the
job ids on Snowflake, a load's updates and deletes, the deletes of a compact underload.delete_flag
(BigQuery counts a flagged delete as an update). The only added read is onejobs.listper compact
script (metadata, not billed). What to do: nothing; the keys are new. -
A whole-table pass replaced an existing base-and-buffer base, and every row only the base held was lost.
Underload.layout: base_buffer(BigQuery), an incremental run with no cursor to resume from (the first
run afterrivet state reset, a lost state DB) and a CDC re-baseline (the remedy every CDC data-loss
refusal prints) landed their whole pass OVER the base, exit 0: rows the source had since deleted, the
tombstones, and any history copied into the base were gone (measured 3334 -> 2178 and 16681 -> 10772
rows). 0.32.1 and earlier. Now the pass over an existing base is appended to<table>__changesand
rivet compactmerges it by key: a row the pass does not hold stays in the base. After a CDC
re-baseline, a live base row the new baseline does not hold is flagged__is_deleted = TRUE(the
baseline read the whole table after the stream's anchor, so the source no longer has it); a base
without the delete flag keeps such rows as they are. Only an absent base is still created from the
pass. A buffer that still holds older changes is refused (RIVET_LOAD_BUFFER_NOT_COMPACTED) before the
pass joins it. What to do: nothing; a base replaced by 0.32.1 can be restored with BigQuery time
travel within 7 days. -
Two TLS advisories in the dependency tree.
rustls-webpki0.103.15 could accept a distrusted
intermediate (RUSTSEC-2026-0382) andwebpki-roots1.0.9 bundled roots that trusted the distrusted
emSign intermediates (RUSTSEC-2026-0381); both reach rivet throughreqwest(object stores, BigQuery,
ClickHouse). Now they are 0.103.16 and 1.0.10; nothing else in the lock moved. What to do:
nothing. -
rivet loadrefused every table on a host without the source's variable. Since the load spec
became keyed by source (state v38, not released), the load read that key from the config's source,
which does not resolve whereurl_env:is unset; a load task that runs without the source credential
(the common shape, and everyrivet initconfig) found no spec and stopped with
RIVET_LOAD_SPEC_MISSING, exit 5. Now a load whose source does not resolve reads the one source
that recorded a spec of the export and table on this state; when several did, it is refused with
RIVET_LOAD_SPEC_SOURCE_UNRESOLVED(exit 1) instead of guessing. What to do: nothing; with
several configs of one export name on one state, giverivet loadthe source's variable too. -
rivet loadandrivet compacttold a scheduler what happened to each table only in prose. Through
0.32.1 a per-table outcome was aLOAD OK/COMPACT SKIPline, and both commands always took every
table of the config. Now both take--export <name>and--table <table>to work on a subset, and
--json(stdout) /--summary-output <file>print one object withrun_idandper_table[]: each
table'sexport,table,status(loaded/compacted,skipped,failed),skip_reason
(up_to_date;no_buffer,full_load,warehouse_never_compacts,log_view),rowsand, for a
failed table, the error object (code,kind,class,exit_code,retryable,action,message;
ADR-0039 D1/D3). Under--jsonthe result lines go to stderr. A name the config does not load is
refused,RIVET_CLI_EXPORT_UNKNOWNor the newRIVET_CLI_TABLE_UNKNOWN, exit 1. Two detail lines of
rivet load(run(s) produced no files,gc-orphans: removed) moved from stdout to stderr.
What to do: nothing; a scheduler can read the object instead of the lines. -
Configs of one export name on one shared state loaded by each other's key. The load spec
(export_load_spec: columns and the keyrivet initrecorded) was one row per export name, so
on a PostgreSQL state shared by configs reading different databases the last writer won: a
query:export loaded by another source's key and failedRIVET_LOAD_KEY_COLUMN_INVALID, or
keyed its dedup by the wrong column (0.32.1 and earlier). Now the spec is keyed by export
name, table and source (state schema v38); a spec written before v38 stays the spec of any
source of that name until the source records its own. What to do: nothing; the first run of
this release migrates the state and leaves the v37 copy for the way back
(docs/reference/state.md, "Rolling back an upgrade"). -
Two CDC configs
rivet initwrote for one cluster named the same slot andserver_id.
Every PostgreSQL scaffold named its slotrivet_<table>and every MySQL oneserver_id: 4271,
so a second config (staging beside production) read the first one's stream (0.32.1 and
earlier). Nowinit --mode cdc -o <file>names them after the file (-o staging.yaml->
slotrivet_staging_public_cdc, aserver_idfrom a stable hash of the stem in 1000000 to
1999999); a scaffold printed to stdout keeps the old names and marks them one per cluster.
What to do: nothing for new configs; give an existing second config its own slot and
server_idby hand. -
A file error whose path read like a network word was retried as transient. The retry
classifier scanned the whole error text, paths included, so an unwritable--summary-output
under a directory whose name containeddns(a temp dir such as.tmprDnsA7),resetor
broken pipeexited 2 (retry) instead of 1 (0.32.1 and earlier). Now the kind of an
io::Errorin the chain decides first (a missing or unwritable path is permanent, a dropped
connection transient), and the text is read only when no kind applies. What to do: nothing. -
A PostgreSQL CDC export whose slot another config consumed continued past the lost changes.
Two configs of one cluster with the sameslot:read in turn: the second took the changes the
first had not read, and the first one's next run continued from the slot with exit 0, the
changes between gone from its output (0.32.1 and earlier). Now rivet records its checkpoint
before every advance of the slot it makes (the empty-span release included), and a run whose
slot was confirmed past its checkpoint is refusedRIVET_SOURCE_CDC_LOG_GAP, exit 5, naming the
other reader. What to do: give every config its own slot, checkpoint and state (see "Copying
a CDC config" indocs/reference/cdc.md), then re-baseline the stream the refusal names. -
A SQLite state could only live beside the config.
RIVET_STATE_URLtook only a PostgreSQL
URL, so a deployment whose config directory is read-only, or that keeps several configs' state
in one place, had no way to point rivet at a SQLite file (0.32.1 and earlier). Now
RIVET_STATE_URL=sqlite:<path>names the SQLite file for every command,initincluded; a
relative path is anchored to the config's directory. A move that would leave the state beside
the config behind (it holds cursors or runs, the new file none) is refused
RIVET_STATE_URL_LEAVES_STATE_BEHIND, exit 5. What to do: to move a state, copy it with
every run stopped (sqlite3 .rivet_state.db ".backup <new path>"), then set the variable.