Skip to content

feat: bandwidth-throttle chaos event type for cumulative-lag testing (#3379) - #3387

Draft
Nana-EC wants to merge 40 commits into
mainfrom
feature/3379-bandwidth-throttle-chaos
Draft

feat: bandwidth-throttle chaos event type for cumulative-lag testing (#3379)#3387
Nana-EC wants to merge 40 commits into
mainfrom
feature/3379-bandwidth-throttle-chaos

Conversation

@Nana-EC

@Nana-EC Nana-EC commented Aug 6, 2026

Copy link
Copy Markdown
Contributor

Summary

  • New Chaos Mesh template network-bandwidth.yaml.tmpl — renders a NetworkChaos resource with action: bandwidth (wraps tc tbf, token bucket filter). Caps throughput between source and target pods to a configurable rate.
  • New execute_inject_bandwidth function in solo-test-runner.sh — mirrors the structure of execute_inject_latency but exports BANDWIDTH_RATE, BANDWIDTH_LIMIT, and BANDWIDTH_BUFFER instead of LATENCY/JITTER/CORRELATION. Includes the same CRD-presence retry and pod-selector dryrun checks.
  • Event dispatchinject-bandwidth routes to execute_inject_bandwidth; clear-bandwidth reuses execute_clear_latency (same kctl delete networkchaos / chaos_resource_name logic).
  • Bandwidth test suite — three tiers:
    • bandwidth-lag.yaml (baseline, 500kbps) — confirms the failure mode exists; lag builds slowly at just-below-threshold rate.
    • bandwidth-lag-severe.yaml (200kbps) — severe tier; models the DR-split scenario (13 of 39 CNs reaching BN1); expects ≥15 blocks divergence at peak chaos.
    • bandwidth-lag-stress.yaml (50kbps) — stress tier; models near-complete link failure (all 39 CN→BN1 paths effectively blocked); expects ≥30 blocks divergence.
  • Two fixes from PR chore: solo chaos review followups #3313 (also addresses chore: follow-up on non-blocking review comments from #2798 (test: Add solo-chaos latency considerations to e2e test framework) #3312):
    • fix(chaos): correct bidirectional yq coercion and resource-name trailing dash — yq .bidirectional // true now uses if has("bidirectional") then .bidirectional else true end so an explicit bidirectional: false is respected; chaos resource names strip trailing - after 63-char slice.
    • chore(chaos): update stale fixture class name, add envsubst check, add test:scripts taskTaskfile.yml now checks envsubst is installed before chaos injection; test:scripts task added; stale fixture class name corrected.
  • CI workflowbandwidth-lag, bandwidth-lag-severe, and bandwidth-lag-stress added to the test-definition description string in both workflow_call and workflow_dispatch triggers. Chaos Mesh install condition now also fires on bandwidth tests.

Why bandwidth throttling is needed

The existing latency-stress and latency-severe tests inject tc netem latency + packet loss via action: netem. Even at 5000 ms RTT + 75% correlated loss, TCP throughput remains ~700 KB/s — roughly 140,000× the block data rate (~1–5 KB/block). TCP has no trouble keeping up; it adds a fixed one-way delay but causes no cumulative queue build-up. The largest realistic block lag is one-way-latency × block_rate ≈ 2–3 blocks.

The production failure mode is different: a BN that falls permanently and increasingly further behind the live tip over time. That is a throughput-constrained failure. action: bandwidth / tc tbf is the only Chaos Mesh mechanism that directly caps ingest throughput, causing the block queue to grow monotonically until chaos clears.

Bandwidth test tier rationale

All three tests use the same paired-3 topology (CN1 → BN1 exclusively; BN2/BN3 unthrottled as reference). Rate selection is derived from the production traffic profile:

Tier Rate Physics Production analog
Baseline 500 kbps Just above 400 kbps data rate; lag builds slowly if blocks > 50 KB Minor degradation, slow lag accumulation
Severe 200 kbps ~50% of data rate; lag accrues at ~0.25 blk/s → ≈37 blocks in 150s DR-split (13/39 CNs reaching BN1 — matching the 3-BN DR target)
Stress 50 kbps ~12.5% of data rate; lag accrues at ~0.44 blk/s → ≈66 blocks in 150s Near-complete link failure (all CN→BN1 paths blocked)

The limit parameter is scaled proportionally to each rate (~10s of burst capacity at the throttled rate):

Rate limit (bytes)
500 kbps 1,000,000 (~16s burst)
200 kbps 250,000 (~10s burst)
50 kbps 62,500 (~10s burst)

BN-peer paths are throttled identically in each tier so greedy backfill cannot neutralise the ingest cap during the chaos window. The stress tier uses an extended load-stop delay (510s vs 420s) to give BN1 more time post-chaos to begin closing its large backfill gap.

Test plan

  • Deploy with TOPOLOGY=paired-3 (≥ 3 BNs required for spread measurement)
  • Install Chaos Mesh: CHAOS_ENABLED=true task chaos:install
  • Run baseline: CHAOS_ENABLED=true TOPOLOGY=paired-3 task test:run TEST_FILE=tests/bandwidth-lag.yaml
  • Run severe: CHAOS_ENABLED=true TOPOLOGY=paired-3 task test:run TEST_FILE=tests/bandwidth-lag-severe.yaml
  • Run stress: CHAOS_ENABLED=true TOPOLOGY=paired-3 task test:run TEST_FILE=tests/bandwidth-lag-stress.yaml
  • Verify blocks-diverged snapshot shows BN1 lagging by ≥ min_spread at each tier
  • Verify blocks-converged passes after chaos clears (tolerance scales with tier)
  • If blocks-diverged fails, check block sizes in run output; lower rate one tier and re-run
  • Trigger via CI: set test-definition: bandwidth-lag-severe or bandwidth-lag-stress with topology: paired-3

Closes #3379
Also addresses #3312

Base automatically changed from feature/3348-per-pod-chaos-selector to main August 19, 2026 17:44
@Nana-EC
Nana-EC force-pushed the feature/3379-bandwidth-throttle-chaos branch from a0e50e1 to 68fc549 Compare August 20, 2026 21:27
@Nana-EC Nana-EC added this to the 0.42.0 milestone Aug 25, 2026
@Nana-EC Nana-EC self-assigned this Aug 25, 2026
Nana-EC added 12 commits August 24, 2026 22:17
…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>
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>
… 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>
…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>
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>
Add Chaos Mesh bandwidth throttle support to the E2E test harness.

- New chaos template: network-bandwidth.yaml.tmpl — uses Chaos Mesh
  action: bandwidth (tc tbf) to cap throughput. Mirrors the structure
  of network-latency.yaml.tmpl with BANDWIDTH_RATE/LIMIT/BUFFER vars.
- New function execute_inject_bandwidth in solo-test-runner.sh — args
  match execute_inject_latency (name, source/target kind+name,
  bidirectional) plus rate/limit/buffer with sane defaults
  (1mbps / 1000000 / 10000).
- Event dispatch: inject-bandwidth → execute_inject_bandwidth;
  clear-bandwidth reuses execute_clear_latency (same kctl delete
  networkchaos logic, same chaos_resource_name scheme).

Closes part of #3379.

Signed-off-by: Nana Essilfie-Conduah <nana@swirldslabs.com>
Add tests/bandwidth-lag.yaml:
- Throttles CN→BN1 and all BN-peers→BN1 to 500kbps using the new
  inject-bandwidth event type; BN2/BN3 run at full speed as reference.
- 500kbps rate: just above the ~400kbps needed to deliver 0.5 blk/s
  at 100 KB/block, so lag builds slowly and is measurable.
- Chaos window: t=30–335s (305s); snapshot at t=180s (peak chaos).
- Asserts min_spread≥5 at peak, tolerance≤10 blocks after recovery
  (BN1 may have a larger backfill gap than in latency tests).
- Header comment explains the rate rationale and tuning guidance.
- Register bandwidth-lag in both test-definition descriptions in
  .github/workflows/solo-e2e-test.yml (workflow_call + workflow_dispatch).

Closes #3379.

Signed-off-by: Nana Essilfie-Conduah <nana@swirldslabs.com>
…ing dash

- yq '.bidirectional // true' treats boolean false as falsy and returns
  the default true, making bidirectional=false impossible to express.
  Use 'if has("bidirectional") then .bidirectional else true end' instead.
- chaos_resource_name slices the combined slug to 63 chars which can
  leave a trailing '-' that kubectl apply rejects. Strip it after slicing.

Fixes items from #3312.

Signed-off-by: Nana Essilfie-Conduah <nana@swirldslabs.com>
…d test:scripts task

- Update sample-logs-with-backfill.txt: class name was
  org.hiero.block.node.verification.VerificationServicePlugin (old module);
  corrected to org.hiero.block.node.block.verification.VerificationServicePlugin.
- Add envsubst to the Taskfile check task: chaos latency injection uses
  envsubst to render network-latency.yaml.tmpl but it was not validated.
- Add test:scripts Taskfile task to run test-chaos-assertions.sh without
  needing a live cluster (previously run-only manually).

All 20 script unit tests pass. Closes items from #3312.

Signed-off-by: Nana Essilfie-Conduah <nana@swirldslabs.com>
…-lag testing

Adds two new bandwidth throttle test tiers derived from the production
traffic profile (39 CNs worst case / 13 in 3-BN DR split):

- bandwidth-lag-severe.yaml (200 kbps): models the DR-split scenario;
  BN1 receives blocks at ~50% of normal rate, producing ~37 blocks of
  divergence in the 150s chaos window. Assertion: min_spread ≥ 15.

- bandwidth-lag-stress.yaml (50 kbps): models near-complete link failure;
  BN1 starved to ~12.5% of normal rate, producing ~66 blocks of divergence.
  Extended load-stop window (510s) to allow post-chaos backfill to begin.
  Assertion: min_spread ≥ 30.

CI: adds both tests to the test-definition description in workflow_dispatch
and workflow_call; expands Chaos Mesh install condition to also trigger on
bandwidth tests (previously only chaos/latency).

Signed-off-by: Nana Essilfie-Conduah <nana@swirldslabs.com>
@Nana-EC
Nana-EC force-pushed the feature/3379-bandwidth-throttle-chaos branch from 68fc549 to b7b37d9 Compare August 25, 2026 02:48
Nana-EC added 10 commits August 24, 2026 23:28
The 500kbps cap on CN→BN1 and peer→BN1 in the bandwidth-lag test
produced 0 block spread in CI (all BNs at block 176 at peak chaos),
failing the blocks-diverged assertion (min_spread=5). The cap wasn't
tight enough to push BN1's ingest rate far enough below the observed
block production rate within the 150s chaos window. Lowering to
100kbps, per the fallback already documented in the test file's rate
rationale comment.

Signed-off-by: Nana Essilfie-Conduah <nana@swirldslabs.com>
bandwidth-lag failed with 0 block spread at both 500kbps and 100kbps
caps, identical results despite a 5x tighter rate. This points to the
NetworkChaos resource not actually taking effect rather than the cap
being too loose, but execute_inject_bandwidth/execute_inject_latency
never checked whether the applied resource reached AllInjected before
the test proceeded — a silent injection failure looked identical to
"chaos had no measurable effect" downstream.

Add wait_for_networkchaos_injected, called after apply in both inject
functions, to fail fast with kubectl describe output when injection
doesn't succeed within 30s. Also fixes execute_inject_bandwidth not
checking kctl apply's exit code (inject-latency already did).

Signed-off-by: Nana Essilfie-Conduah <nana@swirldslabs.com>
bandwidth-lag has now produced identical zero block-spread results
across three CI runs (500kbps, 100kbps, and with AllInjected
confirmed) — Chaos Mesh reports the rule as successfully injected,
but block production shows no measurable ingest slowdown on BN1.

Given the sustained ~6000 TPS load, blocks are almost certainly large
enough that a genuine 100kbps cap on the ingest path should cause
severe, easily detectable lag. A real effect this small points to the
throttle not actually shaping the traffic that carries blocks to BN1,
despite Chaos Mesh's own AllInjected status.

Add dump_bandwidth_chaos_diagnostics, called after successful
injection in execute_inject_bandwidth, to log tc qdisc counters and
the chaos-mesh ipset from the matched source pods on the next run —
this should show directly whether the intended traffic is being
shaped, without needing live cluster access mid-test.

Signed-off-by: Nana Essilfie-Conduah <nana@swirldslabs.com>
The tc/ipset diagnostics added in 779f1bf failed outright in CI:
"exec: tc: executable file not found in \$PATH" on both the CN
(root-container) and BN (block-node-server) app images, since neither
ships iproute2/ipset. That left the actual question — whether the
Chaos Mesh bandwidth rule is shaping real traffic — still unanswered.

Switch to `kubectl debug --target=<container> --image=nicolaka/netshoot`,
which attaches an ephemeral container that shares the existing pod's
network namespace (no extra privilege or node access needed), and
netshoot ships tc/ip/ipset. Target container name is derived from the
selector's label key since CN and BN pods use different container
names.

Signed-off-by: Nana Essilfie-Conduah <nana@swirldslabs.com>
The netshoot-based diagnostics added in abbb7be produced no output
and no error in CI — `kubectl debug` without -i/--stdin creates the
ephemeral container and returns immediately without waiting for it to
start or attaching to it (per its own --attach flag docs: default
false unless -i is set). That made every DIAG block silently empty.

Add -i so --attach defaults to true, which waits for the container to
run and streams its output back, as intended.

Signed-off-by: Nana Essilfie-Conduah <nana@swirldslabs.com>
Root cause of 5 consecutive CI runs showing 0 block spread regardless
of rate (500kbps, then 100kbps) found via the tc qdisc dump added in
38631f2: chaos-mesh's bandwidth "rate" field uses "kbit" for
kilobits/s and "kbps" for kilobytes/s (8x kbit) — confirmed directly
in CI output, where a configured "100kbps" produced an actual
`qdisc tbf ... rate 800Kbit`.

All three bandwidth-lag test tiers wrote their rate rationale in
kilobits/s (matching networking convention and their own math, e.g.
"~50 KB/s = 400kbps needed") but used the "kbps" suffix, so every
configured cap was applied 8x looser than intended — enough slack
that BN1 never fell behind regardless of the nominal number.

Switch all rate values (bandwidth-lag: 100kbps, bandwidth-lag-severe:
200kbps, bandwidth-lag-stress: 50kbps) to the "kbit" suffix with the
same numeric value, and update the rationale comments to document the
unit distinction so it isn't reintroduced.

Signed-off-by: Nana Essilfie-Conduah <nana@swirldslabs.com>
With correct kbit units (2ec035a), the tc dump now shows something
more concrete than a loose rate: the tbf (throttled) band shows 0
bytes sent for the *entire* chaos window on every CN and BN pod, while
all real traffic flows through the untouched sfq band instead. That
points to the ipset-based packet classifier not matching the intended
traffic at all, rather than a rate/timing issue.

The ipset dump added in abbb7be has been failing with "Kernel error
received: Operation not permitted" the whole time — the default
netshoot debug container lacks NET_ADMIN. Add --profile=netadmin so
`ipset list` can actually run and show whether BN1's pod IP is present
in the matched set.

Signed-off-by: Nana Essilfie-Conduah <nana@swirldslabs.com>
The netadmin-profile ipset dump (6853f2b) confirmed the tbf ipset had
exactly one correct-looking member (BN1's pod IP), yet the throttled
tc band still showed 0 bytes for the entire chaos window. Root cause:
CN and BN-peers reach BN1 via its k8s Service DNS name, resolving to
its ClusterIP, not its pod IP directly. chaos-mesh's bandwidth rule is
installed inside the *selector* pod's own netns and matches on pod
IPs — with selector=sender/direction=to, the packet is still addressed
to BN1's ClusterIP at that point (DNAT to the pod IP happens later, on
the host side), so it never matches the ipset. This is a documented
chaos-mesh/Service interaction limitation.

Flip all three bandwidth-lag test tiers to selector=block-node-1 /
target=sender / direction=from, installing the shaping on BN1's own
ingress instead, filtered by source IP — which survives the Service
DNAT since only the destination gets rewritten.

Add an explicit `direction` arg to inject-bandwidth (overriding the
bidirectional-derived to/both default) since the existing bidirectional
boolean has no way to express "from".

Signed-off-by: Nana Essilfie-Conduah <nana@swirldslabs.com>
The last CI run silently skipped every event after inject-cn-bn1-bw
and jumped straight to assertions — 5 events (inject-peer-bn1-bw,
snapshot-peak-chaos, both clear-bandwidth events, load-stop) never
ran. Root cause: run_events' main loop is
`... | jq -c '.[]' | while read -r event; do ...; done` — the whole
loop shares stdin with that pipe. dump_bandwidth_chaos_diagnostics
(introduced in abbb7be/38631f2b) calls `kubectl debug -i`, which
keeps container stdin open and reads from whatever fd0 it inherits.
With no redirection, that was the *same* pipe carrying the remaining
event JSON lines, so kubectl debug silently consumed them before the
next `read -r event` could see them.

Redirect stdin from /dev/null on both kubectl debug invocations so
they no longer compete with the event loop's piped input. -i is still
needed so --attach defaults to true; only the OS-level fd is isolated.

Signed-off-by: Nana Essilfie-Conduah <nana@swirldslabs.com>
The selector=BN1/direction=from "ingress" attempt (afe10e4) still
showed 0 bytes in the throttled tc band in CI. Root cause: chaos-mesh's
bandwidth action is always egress-only, installed on the *selector*
pod's own interface — direction only changes which IP field (src vs
dst) the ipset filters on, not whether shaping is ingress or egress.
Filtering BN1's own egress packets for source=CN can never match
(BN1's outbound packets are always sourced from BN1 itself), so this
approach was structurally incapable of shaping anything, regardless
of rate or units.

Revert to the original selector=sender/direction=to (the only
combination that installs a real egress qdisc on the traffic's actual
source), but resolve the *target* as BN1's Service ClusterIP via
chaos-mesh's externalTargets field instead of a pod selector. This
matches the packet's real destination address at the point tc/ipset
filters it, before kube-proxy's DNAT ever rewrites it to the pod IP —
the actual root cause identified via the netadmin ipset dump.

- execute_inject_bandwidth: add target.kind=service handling that
  resolves the named Service's ClusterIP via `kubectl get svc -o
  jsonpath` and renders it as externalTargets, replacing the
  pod-selector `target:` block for that case only (inject-latency and
  pod-selector bandwidth targets are unaffected).
- network-bandwidth.yaml.tmpl: replace the static target: block with a
  ${TARGET_BLOCK} placeholder populated by the script, since the two
  target modes render structurally different YAML.
- All 3 bandwidth-lag test tiers: target: { kind: service, name:
  block-node-1 } with direction: to, replacing the ingress attempt.

Signed-off-by: Nana Essilfie-Conduah <nana@swirldslabs.com>
@codecov

codecov Bot commented Aug 26, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.

@@             Coverage Diff              @@
##               main    #3387      +/-   ##
============================================
- Coverage     83.59%   83.55%   -0.04%     
+ Complexity     2581     2578       -3     
============================================
  Files           219      219              
  Lines         11336    11336              
  Branches       1328     1328              
============================================
- Hits           9476     9472       -4     
- Misses         1365     1370       +5     
+ Partials        495      494       -1     

see 2 files with indirect coverage changes

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

dump_bandwidth_chaos_diagnostics (and the netshoot/kubectl-debug plumbing
it needed) was scaffolding added purely to find the Service-ClusterIP-vs-
pod-IP ipset mismatch, now fixed via externalTargets (f1bf1f7) and
documented in chaos-templates/README.md. It added ephemeral-container
overhead to every bandwidth injection with no ongoing value now that the
root cause is known and handled. wait_for_networkchaos_injected is kept —
it's a real hardening (fail fast on injection failure), not exploratory
instrumentation.

Also trims the inline target.kind=service comment down to a pointer at
the new README rather than re-explaining the mechanism inline.

Signed-off-by: Nana Essilfie-Conduah <nana@swirldslabs.com>
Nana-EC added 17 commits August 26, 2026 10:06
…oach

Documents how solo-test-runner.sh wires test YAML events into these
NetworkChaos templates, and the three non-obvious Chaos Mesh bandwidth-
action behaviors that cost most of the debugging effort in PR #3387:
rate units (kbit vs kbps), egress-only tc shaping, and Service-ClusterIP
vs pod-IP targeting (externalTargets). Also documents how to manually
verify a fault is actually shaping traffic via kubectl debug + netshoot,
since AllInjected=True alone doesn't confirm that.

Intent: an engineer picking up this test suite should be able to read
this once and know what to change for a new latency/bandwidth scenario,
without rediscovering these gotchas from scratch.

Signed-off-by: Nana Essilfie-Conduah <nana@swirldslabs.com>
The per-file header comments had accumulated a blow-by-blow debugging
narrative (every failed selector/direction/unit attempt) as the real
root causes were found across several commits. That history now lives
in chaos-templates/README.md as a proper reference; trim each test
file back to a concise design rationale plus a pointer to the README,
so the test files read as current-state documentation rather than a
debugging log.

Also cross-reference finding 005 (CN↔BN reconnect deadlock after severe
bandwidth starvation, agent/proposals/solo-chaos-spike/findings/ —
local, not committed) from both tiers, since it directly affects what
recovery-assertion failures mean at these rates.

Signed-off-by: Nana Essilfie-Conduah <nana@swirldslabs.com>
…tigation

50kbit/s (this tier's previous rate) is tighter than bandwidth-lag's
100kbit/s tier, which was enough to permanently deadlock the CN↔BN1
reconnect after chaos cleared (finding 005 — CN never adjusts its
requested resume block after a BlockNodeOutOfRange rejection, so it
retries a stale block forever). Running stress at 50kbit/s would almost
certainly hit the same deadlock, producing no new signal beyond what
the base tier already showed.

Loosen to 500kbit/s as a provisional value to get a runnable,
non-guaranteed-failure test while the deadlock is investigated
separately, and loosen blocks-diverged/blocks-converged thresholds to
match (previous values assumed a ~66-block spread that this looser cap
won't produce). Not yet validated against a real block-size/timeout
recalculation — retune once finding 005 is understood well enough to
pick a rate deliberately.

severe (200kbit/s) is left unchanged per direction — its fate pending
the same investigation.

Signed-off-by: Nana Essilfie-Conduah <nana@swirldslabs.com>
A second confirmed reconnect-deadlock run (finding 005) plus a closer
read of the CN logs showed the previous rates (50-500kbit/s across all
three tiers) were never actually testing a severity gradient — every
one of them sits ~1000x below the real critical zone, so they all hit
the identical outright per-request-timeout deadlock regardless of the
specific number. Loosening 50kbit/s to 500kbit/s (prior commit) made
no difference for exactly this reason.

Derived two real floors from CN log evidence (block ~2.1MB, ~0.5 blk/s,
CN per-request send timeout ~2.5s):
  - sustained-need floor ~8.5 Mbit/s (block_bytes*8*block_rate)
  - per-request timeout floor ~6.8 Mbit/s (block_bytes*8/timeout_s) —
    below this a single block can't transfer within CN's timeout at
    all, which is what triggers the permanent reconnect deadlock.

Re-tiered around that zone, per direction: bandwidth-lag covers a
minimal constraint, bandwidth-lag-stress a significant but manageable
one, and bandwidth-lag-severe the highest constraint before the
breaking point (not past it):
  - bandwidth-lag:        100kbit/s -> 15mbit/s (~1.5% of a 1 Gbps link)
  - bandwidth-lag-stress: 500kbit/s ->  9mbit/s (~0.9%, just above the
                           sustained-need floor)
  - bandwidth-lag-severe: 200kbit/s ->  7mbit/s (~0.7%, just above the
                           per-request timeout floor — deliberately the
                           tier most likely to brush against finding
                           005's deadlock)

`limit` scaled to ~3MB (roughly one block's worth of queue depth) for
all three tiers to match the new rate scale. Assertion thresholds reset
to conservative starting points since none of these exact rates have
run in CI yet. Documented the full floor derivation in
chaos-templates/README.md so the next person tuning these doesn't have
to rediscover it.

Signed-off-by: Nana Essilfie-Conduah <nana@swirldslabs.com>
…uted topologies

bandwidth-lag's blocks-converged/blocks-increasing/block-rate-floor
assertions failed on both topologies: once a BN's live publisher
connection is torn down under chaos, non-greedy backfill caps its
gap-scan window at its own last stored block, so it never looks past
that point even though its peers (with independent, unthrottled
publishers) are far ahead. Enabling greedy lets the scan window
extend to the peers' max available block instead, so a stalled BN can
catch back up via backfill regardless of whether its live stream ever
recovers.

fan-out-3cn-2bn is left as-is: it already sets greedy: true, but BN2
has no publisher of its own and depends entirely on backfill from
BN1, so there is no independent unthrottled source to backfill from
once BN1's stream dies - running bandwidth-lag against that topology
also doesn't match the test's documented topology requirement
(paired-3).

Signed-off-by: Nana Essilfie-Conduah <nana@swirldslabs.com>
…ss takes effect

execute_load_start read a load-start event's test_class arg, defaulted it,
and logged it, but never exported NLG_TEST_TYPE before invoking
solo-load-generate.sh, which selects its class from
${NLG_TEST_TYPE:-CryptoTransferLoadTest}. Every in-test load-start therefore
always ran CryptoTransferLoadTest regardless of what a test YAML's
load-start.args.test_class asked for.

Signed-off-by: Nana Essilfie-Conduah <nana@swirldslabs.com>
…E export

Extracts execute_load_start from solo-test-runner.sh (same sed/eval approach
as test-topology-decisions.sh, since the runner has top-level arg parsing
that exits under set -u with no --test flag) and mocks solo-load-generate.sh
with a stub that captures the environment it actually received. Confirmed
this fails against the pre-fix code (NLG_TEST_TYPE unset) and passes with
the fix. Wired into task test:scripts so it actually runs.

Signed-off-by: Nana Essilfie-Conduah <nana@swirldslabs.com>
…s takes effect

Same gap as execute_load_start (fixed in 77ae3e2): execute_load_stop read a
load-stop event's test_class arg, defaulted it, and logged it, but never
exported NLG_TEST_TYPE before invoking solo-load-generate.sh stop. Without
this, fixing load-start alone would start a non-default load class correctly
but load-stop would still target CryptoTransferLoadTest and fail to actually
stop it.

Signed-off-by: Nana Essilfie-Conduah <nana@swirldslabs.com>
Extends the existing execute_load_start regression test with the matching
execute_load_stop case, and isolates every case with an explicit unset of
NLG_TEST_TYPE/NLG_ARGS/NLG_MAX_TPS beforehand so a missing export fails
deterministically instead of possibly passing on leaked state from an
earlier case in the same run.

Signed-off-by: Nana Essilfie-Conduah <nana@swirldslabs.com>
Exercises the test_class override now that execute_load_start/
execute_load_stop actually honor it, instead of always defaulting to
CryptoTransferLoadTest regardless of what a test YAML asks for.

Signed-off-by: Nana Essilfie-Conduah <nana@swirldslabs.com>
Derives real average bytes-per-block from files_recent_total_bytes_stored /
files_recent_blocks_written (both already exposed by BlockFileRecentPlugin)
over a sample window, asserted against a min_bytes floor. Gives a direct
number instead of inferring block size from the presence/absence of CN-side
slow-request warnings. Same "assertions run after all events complete"
constraint as block-rate-floor: this measures the post-load-stop/recovery
window, not bytes-per-block during active chaos.

Signed-off-by: Nana Essilfie-Conduah <nana@swirldslabs.com>
Green/red/zero-blocks-written/missing-baseline cases, same fixture-mock
pattern as assert_block_rate_floor's coverage in this file.

Signed-off-by: Nana Essilfie-Conduah <nana@swirldslabs.com>
min_bytes is a sanity floor, not a tuned expectation -- the point is to read
the printed bytes/block value and settle whether a given load (CryptoTransfer
vs HCS) is actually producing blocks big enough to stress the bandwidth cap,
instead of inferring size from the absence of CN-side slow-request warnings.

Signed-off-by: Nana Essilfie-Conduah <nana@swirldslabs.com>
Signed-off-by: Nana Essilfie-Conduah <nana@swirldslabs.com>
Queried files_recent_total_bytes_stored/files_recent_blocks_written directly,
but MetricKey.addCategory(METRICS_CATEGORY) prefixes every metric with
"blocknode_" (BlockNodePlugin.METRICS_CATEGORY = "blocknode") before
exposition -- same reason block-rate-floor queries
blocknode_publisher_highest_block_number_inbound rather than the bare Java
key. Confirmed against CI run 33209880050: the assertion failed with "no
baseline (... unavailable)" because the unprefixed names don't exist on
/metrics.

Also tightens the fixture mock in test-chaos-assertions.sh to gate on the
correct prefixed names instead of matching whatever name it was given --
the previous version would have passed against either name, which is how
this shipped without the test catching it. Verified: reverting just the
query names now makes the fixture test fail (confirmed locally, reverted
after confirming).

Signed-off-by: Nana Essilfie-Conduah <nana@swirldslabs.com>
…oadTest

HCSLoadTest ran for the full chaos window (confirmed via CI timestamps) but
produced no detectable divergence across 3 consecutive runs -- zero CN-side
slow-request/error events at the same 7 Mbit/s cap that reliably trips
double-digit disconnects with CryptoTransferLoadTest. Its individual
transactions are apparently too small in bytes to stress this cap regardless
of submission rate. CryptoTransferLoadTest is the one that's actually been
producing the effect this test needs.

Signed-off-by: Nana Essilfie-Conduah <nana@swirldslabs.com>
…l counter suffix

blocknode_files_recent_blocks_written is a LongCounter; OpenMetrics/Prometheus
exposition appends _total to Counter-type metrics, so the actual exported name
is blocknode_files_recent_blocks_written_total. blocknode_files_recent_total_bytes_stored
is a Gauge and is unaffected. Confirmed against real Grafana metric names.

Signed-off-by: Nana Essilfie-Conduah <nana@swirldslabs.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

feat: add bandwidth-throttle chaos event type to E2E latency tests to reproduce cumulative-lag production failure

1 participant