WIP: Invariants test - #37415
Draft
def- wants to merge 6 commits into
Draft
Conversation
def-
marked this pull request as draft
July 2, 2026 11:16
def-
force-pushed
the
pr-invariants
branch
16 times, most recently
from
July 5, 2026 14:10
4edc634 to
dde9631
Compare
def-
force-pushed
the
pr-invariants
branch
4 times, most recently
from
July 16, 2026 19:36
5d2cd09 to
1c76f55
Compare
def-
force-pushed
the
pr-invariants
branch
3 times, most recently
from
August 4, 2026 14:33
592518b to
cbfccbc
Compare
def-
force-pushed
the
pr-invariants
branch
4 times, most recently
from
August 5, 2026 16:52
f0f1edc to
0f5f034
Compare
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Status: currently blocked by SQL-603
A correctness-under-chaos test framework: multi-threaded scenarios whose
invariants hold no matter which concurrent operations succeed, fail, or end up
in an unknown state, checked continuously while toxiproxy cuts connections and
processes are killed, and strictly again after healing.
Why another workload
The trick is picking invariants that are outcome-independent. A bank
transfer debits one account and credits another, so the grand total is
conserved whether the transfer committed, was rejected, or timed out with an
unknown outcome. That is what makes it safe to assert exact results during a
disruption, rather than waiting for quiescence and comparing against an oracle
that chaos has invalidated.
How a run works
--runtime, default 600s): worker threads issue writes, checkerthreads verify invariants, the disruptor injects/heals, the agitator flips
feature flags and cancels connections. Any checker failure fails the run
immediately.
the workers must have made progress in both halves of the chaos phase, and
the disruptor and agitator must have acted. A thread that wedges midway, or a
run where nothing was ever disrupted, fails instead of passing silently.
Each thread draws its own seeded RNG in a fixed order, so
--seedreproducesthe whole action/disruption sequence.
What gets disrupted
Toxiproxy fronts each leg: persist consensus (metadata store), persist blob,
envd<->clusterdstoragectl/computectl, and the source/sink/registryconnections. Disruptions are
disable,latency,timeout,limit_dataandbandwidth, applied to one direction only so half are asymmetric. On top ofthat the disruptor SIGKILLs and SIGSTOPs
environmentdand theclusterdprocesses, deliberately overlapping process kills with leg cuts and following
some heals with an immediate kill, since the post-heal window is where recovery
bugs live. Outages are capped per leg (a metadata cut must stay well under the
15-minute persist lease expiry). Coverage is reported at the end, and a
deterministic first sweep guarantees no leg goes untouched by RNG accident.
--upgrade-from=<image>starts on an older release and swaps in the currentbuild at the chaos midpoint: an upgrade under concurrent load and disruptions,
with the invariants never pausing.
Scenarios
--scenariotable-bankREFRESH EVERY, schema swaps, replacement MVs,COPY TO/FROMpg-cdc-bank,mysql-cdc-bank,sqlserver-cdc-bankkafka-ledgerkafka-upsertsink-roundtripwebhook-setavro-loopback--complexity=low|medium|highscales workers, disruption frequency andconcurrency.
--no-disruptionsruns the same workload and checkers clean, whichis how you tell a product bug from a chaos artifact.
Read-path coverage
The same invariant is verified through deliberately different plans and
protocols, because a wrong answer usually only shows up in one of them: one-shot
peeks (maintained MV, ad-hoc over base tables, and result-equivalent joins,
window functions, recursive CTEs and LATERAL subqueries),
SUBSCRIBEwithPROGRESS(both fresh and resumed from a durable timestamp), read-onlytransaction snapshots,
AS OFtime travel into retained history,COPY TOexport, and a post-run audit that replays the entire retained history and
validates every progress boundary, retroactively closing the rounds live
checkers had to skip during outages. Reads rotate over isolation levels, since a
timestamp-free invariant must hold under all of them.
What the checkers assert
A conserved total is safe to assert mid-disruption, which is what makes
continuous checking possible, but on its own it is a one-dimensional projection:
transfers are balanced pairs, so only a torn pair breaks the sum, and that is
the one thing atomic batch commit already prevents. So the oracles go past it,
while staying outcome-independent:
newest ops must have exactly its two rows summing to zero, its derived columns
must match its op id, committed ops must be present, ops never issued must not
be, and an op observed once must never be absent again. A lost transfer masked
by a duplicated one, a rewritten row, or a resurrected retraction all keep the
count and the sum intact and are only visible here.
SUBSCRIBEwhose cost isindependent of table size. The ledger is append-only, so any retraction is a
bug outright, even the ones that cancel against their insert and leave the
folded state looking correct.
timestamp-free invariant covers it, and it is what a stale read breaks first.
skip parts by their statistics, and getting that wrong is silent: the answer is
just too small. The same predicate is re-evaluated on the client over the rows
the same transaction returns, so the two must agree.
The ledger carries the values those oracles need in order to be able to fail: a
numeric mirror of each amount (its own encoding and scale handling), a tag
derived from the op id (so each row is checkable alone), floats covering NaN,
negative zero, the infinities and a denormal, and a nullable date. Predicates
over the last two are what pull filter pushdown onto the checked path.
Findings so far
Each finding has a concentrated reproducer, selectable via
--scenario:repro-per10)repro-blob-memory)repro-postheal-stall)as_of(repro-compute-asof)SUBSCRIBEloses its carried state (repro-durable-resume)Running it
Wired into Nightly as 11 steps (one per scenario, plus a large
table-bankandthe upgrade-under-load variant). Every failure annotation carries the exact
reproducer command, and the run log opens with the random seed that replays it.