feat: per-pod chaos selector — asymmetric BN latency injection (#3348) - #3353
Merged
Conversation
NetworkChaos rules previously targeted ALL pods of a given type
(e.g. all block-nodes) via a single type label. This meant every BN
experienced identical latency simultaneously — no node diverged from its
peers — so BackfillPlugin never triggered a gap-fill and the
backfill-triggered assertion always failed.
Changes:
- network-latency.yaml.tmpl: add ${SOURCE_NAME_FILTER} / ${TARGET_NAME_FILTER}
optional YAML lines (empty when not used, rendered at correct indentation
when set) so a second labelSelectors entry can pin the rule to a single pod.
- solo-test-runner.sh / execute_inject_latency: extract optional
source.name / target.name from event args; build compound kubectl selector
(type-label,app.kubernetes.io/instance=<name>) for the chaos-dryrun check;
export SOURCE_NAME_FILTER / TARGET_NAME_FILTER for envsubst; annotate log
line with instance suffix when name is present.
Backward-compatible: existing tests that omit source.name / target.name
produce empty filter vars → template renders identically to before.
Signed-off-by: Nana Essilfie-Conduah <nana@swirldslabs.com>
The previous symmetric approach applied identical latency to every BN simultaneously, so no BN fell behind its peers and backfill never fired. latency-stress (was: symmetric 3x-baseline): BN1 — "far region": CN↔BN1 800ms ± 200ms + BN1→BNs 1000ms (direction=to) BN2/BN3 — normal: no chaos CNs — light background: CN↔CN 100ms ± 20ms latency-severe (was: uniform all-three 5-6x-baseline): BN1 — "very far": CN↔BN1 800ms ± 200ms + BN1→BNs 1000ms (direction=to) BN2 — "mid-range": CN↔BN2 400ms ± 100ms + BN2→BNs 500ms (direction=to) BN3 — "local": no per-BN chaos (fastest; serves as backfill source) CNs — severe: CN↔CN 600ms ± 150ms BN→BN rules use direction=to (unidirectional) so the two per-BN rules do not additively stack on the BN1↔BN2 path. backfill-triggered assertion now targets block-node-1 (the most degraded node) rather than all; since_seconds extended to cover the full test window. Both tests require ≥3 BNs with backfill peers (paired-3, 7cn-3bn-distributed); the chaos-dryrun check will fail fast on 3cn-1bn. solo-e2e-test.yml: register latency-stress and latency-severe in the test-definition input description for both workflow_dispatch and workflow_call blocks. Signed-off-by: Nana Essilfie-Conduah <nana@swirldslabs.com>
Latency alone keeps the gRPC stream alive (TCP retransmits absorb the delay); BN1 receives every block just 800ms late, so no sequence gap forms and BackfillPlugin never fires. Adding correlated packet loss forces bursty drops that overwhelm TCP retransmit budgets, break the gRPC stream, and create a genuine missing- block gap during reconnection — which BackfillPlugin detects and fills. latency-stress: inject-cn-bn1 gets loss: "25%", corr 75% latency-severe: inject-cn-bn1 gets loss: "25%", inject-cn-bn2 gets loss: "15%" Loss correlation matches the existing latency correlation so bursts of high latency and drops hit simultaneously, maximising disruption. Signed-off-by: Nana Essilfie-Conduah <nana@swirldslabs.com>
25% loss absorbed by TCP retransmits — BN1 received every block, no gap formed, backfill never fired. 75% correlated loss (burst model) creates near-blackout periods that exhaust TCP retransmit budget and force a stream reset, generating the missing-block gap BackfillPlugin needs. Signed-off-by: Nana Essilfie-Conduah <nana@swirldslabs.com>
#3348) TCP on kind's loopback bridge absorbs all packet loss via retransmit — the gRPC stream from CN to BN1 never breaks. BN1 received every block at the normal 2-second cadence throughout the chaos window (blocks 115-122 consecutive at peak chaos, confirmed in BN1 logs from run 30780607265). Even if the stream had broken, the CN resumes from the last-ACKed block on reconnect, so no storage gap forms. All three BNs ended at block 278 in every run. BackfillPlugin ran silently (gap detection at TRACE/DEBUG, only "Starting backfillGap..." at INFO — never triggered). Remove backfill-triggered from latency-stress and latency-severe. Update comments to accurately scope these tests: they verify cluster resilience under asymmetric latency (health, block continuity, rate floor), not BackfillPlugin gap-fill. BackfillPlugin testing requires explicit storage gap injection, tracked in #3349. Also drop the loss parameter from latency-stress inject-cn-bn1; pure latency is sufficient to exercise the timing gradient between BN1 and BN2/BN3. Signed-off-by: Nana Essilfie-Conduah <nana@swirldslabs.com>
…riggered (#3348) BackfillPlugin with greedy=false only scans up to the BN's own last stored block (determineEndCap returns storeMax). With asymmetric latency BN1 never misses blocks — TCP retransmit keeps the stream whole — so the non-greedy scan finds no historical gaps and BackfillPlugin stays silent. With greedy=true, determineEndCap queries peers for their max available block (getPeerMaxAvailableBlock). The latency gradient means BN3 is always 1-2 blocks ahead of BN1 at any scan boundary. That range is classified as LIVE_TAIL and submitted to the liveTailScheduler, producing INFO log: "Starting backfillGap type=[LIVE_TAIL] range=[N-N+1]" This fires every 60s scan cycle throughout the chaos window, making the backfill-triggered assertion reliable. Changes: - paired-3.yaml: greedy: false → greedy: true on all three BNs; regenerated out/ overlays will pick this up at task-up time - latency-stress.yaml: restore loss: "75%" (packet loss widens the live-tail gap at scan boundaries, making detection more reliable); restore backfill-triggered with grep: "backfillGap" (targets INFO log exactly) - latency-severe.yaml: same — restore backfill-triggered on BN1 with the "backfillGap" grep pattern; update header comments to explain mechanism Signed-off-by: Nana Essilfie-Conduah <nana@swirldslabs.com>
All three BNs already have peers configured; enabling greedy=true so BackfillPlugin proactively closes live-tail lag from faster peers, consistent with the same change made to paired-3. Signed-off-by: Nana Essilfie-Conduah <nana@swirldslabs.com>
Adds assert_blocks_converged() which queries serverStatus on all block nodes and checks that max(lastAvailableBlock) - min(lastAvailableBlock) is within a configurable tolerance. Wired as the 'blocks-converged' assertion type in the runner dispatch. This directly measures whether all BNs converged to the same block height after a chaos window — confirming both that CN↔BN streams survived and that any latency-induced lag closed — without requiring a specific recovery mechanism (TCP delivery, backfill, or both satisfy it equally). Signed-off-by: Nana Essilfie-Conduah <nana@swirldslabs.com>
backfill-triggered could not reliably fire under latency/loss chaos: TCP absorbs all packet loss on kind's loopback bridge so BN1 receives every block and never falls behind peers. The BackfillPlugin's LIVE_TAIL mechanism only triggers when BN1's storeMax < peers' storeMax at a 60s scan boundary, which doesn't happen when TCP delivers all blocks. blocks-converged (tolerance 5) tests what actually matters: after chaos clears, do all BNs converge to the same block height? This confirms that CN↔BN streams survived throughout and any latency-induced lag closed — regardless of whether TCP delivery or backfill achieved convergence. Header comments updated to reflect the new test intent. Signed-off-by: Nana Essilfie-Conduah <nana@swirldslabs.com>
snapshot-block-heights: new event type that captures lastAvailableBlock for all BNs at the moment it fires, writing "node=block" lines to /tmp/chaos-snapshot-<id>.txt. Placed mid-chaos to record per-BN heights while the fault is still active. blocks-diverged: new assertion type that reads a named snapshot file and asserts max(lastBlock) - min(lastBlock) >= min_spread. Confirms that the injected chaos actually caused measurable block divergence — a necessary pre-condition for the blocks-converged post-chaos assertion to be meaningful. Together these two types give a before/after pair: diverged-during-chaos (spread >= N) followed by converged-after-chaos (spread <= N), proving both that the fault had real effect and that recovery occurred. Signed-off-by: Nana Essilfie-Conduah <nana@swirldslabs.com>
… tests
Both latency-stress and latency-severe now:
1. Capture a snapshot-block-heights event at peak chaos (t=180s stress,
t=210s severe) — records per-BN lastBlock while the fault is live
2. Assert blocks-diverged (min_spread=3) — confirms chaos caused ≥3-block
spread across BNs, proving the latency gradient had real effect
3. Assert blocks-converged (tolerance=5) — confirms all BNs returned to
the same height after chaos cleared
This replaces the single end-state-only check with a proper before/after
pair that detects both divergence under chaos and recovery after it.
Signed-off-by: Nana Essilfie-Conduah <nana@swirldslabs.com>
Nana-EC
force-pushed
the
feature/3348-per-pod-chaos-selector
branch
from
August 5, 2026 17:57
5a6c0d6 to
5bcd174
Compare
…failure mode latency-stress: CN↔BN1 800ms→3000ms ±750ms, loss 75%→50% (corr 50%), BN1→BN peers 1000ms→1500ms ±375ms — borderline 'approaching threshold' zone (expected spread 3-8 blocks at peak chaos). latency-severe: CN↔CN 600ms→1000ms, CN↔BN1 800ms→5000ms ±1500ms + 75% loss (corr 75%), CN↔BN2 400ms→2500ms ±750ms + 40% loss (corr 50%), BN1→BN 1000ms→2500ms, BN2→BN 500ms→1000ms — full production failure mode (expected spread 40-80 blocks at peak chaos). At 5000ms one-way + 75% correlated loss on BN1, TCP retransmit backoff (10s→20s→40s→80s→160s) creates genuine cumulative block lag matching the geographic deployment failure observed in production environments. Closes #3348 Signed-off-by: Nana Essilfie-Conduah <nana@swirldslabs.com>
…mplate rendering envsubst was not installed on the GitHub Actions ubuntu runner, causing the NetworkChaos YAML manifest to be empty on every chaos inject event. kubectl apply returned 'no objects passed to apply' and chaos was never applied — all spread=0 results were caused by this missing binary, not by TCP absorption of the injected latency. gettext-base is appended to the existing 'Install zstd' apt-get line to avoid an extra apt-get update call. Signed-off-by: Nana Essilfie-Conduah <nana@swirldslabs.com>
At 3000ms one-way latency (stress tier), base block lag = 3000ms × 0.5 blk/s = 1.5 blocks. TCP throughput at this latency+loss is ~700 KB/s — far above the ~1-5 KB/s block data rate — so no cumulative queue builds. min_spread: 3 is unreachable with pure netem; set to 1 (onset of lag, borderline). At 5000ms + 75% correlated loss (severe tier), base lag = 2.5 blocks plus burst delays from retransmit backoff. min_spread: 3 may be hit transiently but is unreliable; set to 2 (consistently reachable at peak chaos). Reproducing the cumulative-lag production failure mode requires bandwidth throttling (tc tbf), not just latency+loss. Tracked in #3379. Signed-off-by: Nana Essilfie-Conduah <nana@swirldslabs.com>
… measurement With greedy=true, BackfillPlugin's LIVE_TAIL closes any CN-latency-induced lag within one 60s scan cycle by pulling from BN3 via the unchaosed peer path. No amount of CN↔BN1 latency creates sustained spread — the gap is erased before the snapshot fires. greedy=false forces BN1 to rely solely on its direct CN gRPC stream, making the one-way latency visible as a block-height difference at snapshot time. This is a deliberate testing trade-off: not production-faithful on the backfill dimension, but the only way to measure latency-induced divergence without bandwidth throttling. The bandwidth-throttle approach (which works with greedy=true) is tracked in #3379. Signed-off-by: Nana Essilfie-Conduah <nana@swirldslabs.com>
… divergence latency-stress: CN↔BN1 3000ms → 5000ms ±1250ms, BN1→BN 1500ms → 2500ms ±625ms latency-severe: CN↔BN1 5000ms → 8000ms ±2000ms, CN↔BN2 2500ms → 4000ms ±1000ms, BN1→BN 2500ms → 4000ms, BN2→BN 1000ms → 2000ms; min_spread 2 → 3 Higher base latency increases the steady-state block lag above the assertion thresholds, giving a more reliable signal that per-pod chaos is actually degrading BN1. Prior values (3000ms / 5000ms) were borderline; RTO retry bursts sometimes landed below min_spread before the snapshot was taken. Signed-off-by: Nana Essilfie-Conduah <nana@swirldslabs.com>
…inject-latency
Chaos Mesh expects loss percentage as a plain number string ("50" not "50%").
The YAML test files specify loss as "50%" which caused the admission webhook to
reject the NetworkChaos manifest with a ParseFloat error; the event was still
marked PASS because kubectl apply return code was not checked.
- Strip trailing % from loss value immediately after parsing
- Remove stale "0%" guard (now unreachable after stripping)
- Propagate kubectl apply failure so the event fails fast instead of silently
continuing with no chaos applied
Signed-off-by: Nana Essilfie-Conduah <nana@swirldslabs.com>
At 8000ms + 75% correlated loss over 370s the CN↔BN1 gRPC stream is severed; BN1 does not recover before assertions run. The divergence signal (spread=40) is the primary intent of the severe test, not stream survival. - blocks-converged: tolerance 5 → 150 (catches a totally frozen node while accepting that BN1 may lag far behind at this tier) - blocks-increasing / block-rate-floor: target all → block-node-3 (reference node unaffected by chaos); confirms the network is still live even if BN1 stream was permanently severed Signed-off-by: Nana Essilfie-Conduah <nana@swirldslabs.com>
At 5000ms + 50% loss, TCP throughput (~100-250 KB/s) is orders of magnitude above the block data rate (~2 KB/block). BN1's lag is a constant ~2.5 blocks in-flight, not cumulative — divergence never accumulates to a reliably measurable level. Block divergence only becomes observable at the severe tier where 8000ms + 75% correlated loss triggers burst-loss RTO chains that freeze the stream entirely. Reposition stress as a survival test: BN1 stays healthy, keeps ingesting blocks, and converges after chaos clears. Remove snapshot-peak-chaos event and blocks-diverged assertion (no longer applicable). Use latency-severe for divergence assertions. Signed-off-by: Nana Essilfie-Conduah <nana@swirldslabs.com>
Codecov Report✅ All modified and coverable lines are covered by tests. @@ Coverage Diff @@
## main #3353 +/- ##
============================================
+ Coverage 83.37% 83.58% +0.21%
- Complexity 2626 2678 +52
============================================
Files 228 229 +1
Lines 11801 11983 +182
Branches 1368 1396 +28
============================================
+ Hits 9839 10016 +177
- Misses 1431 1438 +7
+ Partials 531 529 -2 see 14 files with indirect coverage changes 🚀 New features to boost your workflow:
|
jsync-swirlds
approved these changes
Aug 18, 2026
jsync-swirlds
left a comment
Contributor
There was a problem hiding this comment.
Approving to get testing in place, but the extreme latency involved here, compared to larger test environments, suggests we need to look at more than just latency to find the proper edges.
Contributor
Author
3 tasks
rockysingh
approved these changes
Aug 19, 2026
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.
Problem
Chaos tests injected identical latency across all block nodes simultaneously.
Because every BN was equally degraded, no divergence occurred — the tests were
verifying nothing meaningful about single-node resilience.
Four additional bugs meant chaos was silently never applied at all:
envsubstmissing on CI runner — chaos manifest template substitutionproduced empty files;
kubectl applyon an empty file returned no error.loss: "50%"but Chaos Meshrequires a plain number string (
"50"); the admission webhook rejected themanifest and the event was still marked PASS.
kubectl applyfailure — return code was not checked, so rejectedmanifests caused chaos to silently not apply.
greedy: true) — even when chaos did apply,backfill polled unchaosed peers every ~60s and pulled missing blocks before
the mid-chaos snapshot, collapsing any spread to zero.
Changes
Infrastructure fixes
.github/workflows/solo-e2e-test.yml— installgettext-basesoenvsubstis available on the runner.
solo-test-runner.sh/execute_inject_latency— strip trailing%fromloss values; fail fast if
kubectl applyreturns non-zero.Per-pod instance selector
network-latency.yaml.tmpl— added${SOURCE_NAME_FILTER}/${TARGET_NAME_FILTER}placeholder lines in eachlabelSelectorsblock.When set they inject
app.kubernetes.io/instance: <name>at the correctindentation. When empty the line is blank — existing tests unaffected.
solo-test-runner.sh— extracts optionalsource.name/target.namefrom event args; builds compound dryrun selector for pod-count validation;
exports the two filter env vars.
Mid-chaos measurement
snapshot-block-heightsevent — querieslastAvailableBlockon every BNmid-chaos and writes a snapshot file.
blocks-divergedassertion — reads the snapshot; fails ifmax − min < min_spread. Proves chaos produced detectable divergence.blocks-convergedassertion — queries live heights at assertion time;fails if spread exceeds
tolerance_blocks. Proves streams survived and nodescaught up.
Topology change
paired-3,7cn-3bn-distributed— all BNs set togreedy: falseto prevent BackfillPlugin from pulling blocks over the unchaosed peer path
and masking divergence. See feat: add bandwidth-throttle chaos event type to E2E latency tests to reproduce cumulative-lag production failure #3379 for the bandwidth-throttle follow-on that
restores
greedy: true.Test definitions
latency-stress— survival test. One BN (BN1) receives high latency +moderate loss. At this tier TCP throughput still exceeds the block data rate
so lag is constant (~2.5 blocks), not cumulative; divergence is not asserted.
Assertions confirm BN1 stays healthy, keeps receiving blocks, and converges
after chaos clears.
latency-severe— divergence test. Three-tier gradient; BN1 most degraded,BN2 mid, BN3 reference. At 8000 ms + 75% correlated loss, burst-loss RTO chains
freeze the CN↔BN1 stream. Spread of ~39 blocks observed consistently.
Convergence tolerance is wide (150 blocks) at this tier because the CN↔BN1
stream is severed and BN1 does not reconnect within the test window.
blocks-increasingandblock-rate-floortarget BN3 only.Known limitation / follow-on
latency-stresshas no block-divergence assertion because the currenttc-netem approach cannot produce cumulative lag when block payloads are
small (~2 KB). #3379 / #3387 (bandwidth throttle via
tc tbf) will capingest below block production rate, creating sustained lag with
greedy: true.Test results
Closes #3348