Skip to content

fix(#790): raise default FIXP keep-alive interval so reattach can survive a real disconnection - #792

Open
pedrosakuma wants to merge 6 commits into
mainfrom
fix/790-chaos-drill-conformance-timers
Open

pedrosakuma wants to merge 6 commits into
mainfrom
fix/790-chaos-drill-conformance-timers

Conversation

@pedrosakuma

@pedrosakuma pedrosakuma commented Sep 3, 2026

Copy link
Copy Markdown
Owner

Summary

Fixes the 100%-reproducible network-partition chaos-drill failure (#790) by raising the default FIXP KeepAliveIntervalMs from 1000ms to 15000ms, plus two related workflow/compose corrections found along the way.

Root cause

The venue (B3MatchingPlatform) honors the client-negotiated keepAliveInterval, not its own bridge config, and terminates a FIXP session as idle after ~3x that negotiated interval of silence. ExchangeOptions.KeepAliveIntervalMs (trading-host) defaulted to the FIXP spec floor of 1000ms, giving a ~3-second venue-side terminate threshold in every environment. Any disconnection longer than ~3s — including the chaos drill's 10s network-partition scenario, or a realistic production network blip — always got evicted server-side, forcing UNNEGOTIATED → fresh Negotiate → Renegotiated → in-flight PendingNew mutations reclassified Ambiguous (manual reconciliation required).

This confirms @pedrosakuma's original read: disconnection duration alone should never force a session roll — FIXP's Establish-reuse/Reattach should absorb a brief blip. The defect wasn't in the reconnect logic; it was a keepalive interval configured far too aggressively for reattach to ever get a chance to work.

Changes

  • backend/src/B3.Trading.Infrastructure/ExchangeOptions.cs: default KeepAliveIntervalMs 1000 → 15000 (comment explains the venue-side ~3x terminate math).
  • .github/workflows/chaos-drill.yml: stop stacking docker-compose.real-conformance.yml (a different conformance spec's fast-timer bridge profile: 500ms heartbeat/idle) onto the chaos drill's real stack — unrelated to the root cause but a real hazard for this workflow regardless.
  • docker/docker-compose.chaos.yml (new): minimal overlay granting alice's AllowShortSell/AllowSelfTrade risk opt-outs (previously an incidental side effect of real-conformance.yml) so the drill's same-user Buy+Sell self-cross assertion still passes now that real-conformance.yml is no longer stacked.

Validation

Live-dispatched network-partition scenario runs on this branch (no local Docker in this environment):

  • Before either fix: reproduced the original failure (KeepaliveIntervalLapsedUNNEGOTIATEDAmbiguous, orders stuck).
  • After compose-only fixes (before the KeepAliveIntervalMs bump): still failed — same signature, proving the compose changes alone weren't the root cause.
  • After the KeepAliveIntervalMs bump: PASS x2 — runs 33749020448 and 33749424888. Both baseline and post-recovery trades filled cleanly through the partition/reconnect cycle.

Closes #790

Protocol evidence

  • No, this PR does not change cross-repo wire assumptions.
  • Yes, and merged upstream evidence plus official schema details are listed below.
  • Upstream issues, RFCs, or proposals are not the only evidence for implementation, or this PR has no cross-repo wire dependency.
  • Participant-side / product requirements do not prescribe venue messages, templates, fields, or enums that do not exist in the official schema and merged upstream implementation.

This PR only changes our own client-side config default (ExchangeOptions.KeepAliveIntervalMs) within the already-existing, unchanged FIXP negotiation/clamp behavior on the venue side (EstablishValidator.cs in B3MatchingPlatform, read directly to determine the correct default — not modified, and no new template/field/enum is introduced or assumed).

PedroTravi and others added 3 commits September 3, 2026 11:05
The network-partition scenario holds a 10s partition expecting FIXP's
Establish-reuse reattach to recover the session (per spec, a brief
blip should never need a peer-forced session roll). But chaos-drill.yml
stacked docker-compose.real-conformance.yml on top of the real stack,
which swaps matching-platform's bridge config to the fast conformance
profile (heartbeatIntervalMs=idleTimeoutMs=500ms) used by the
SessionRoll conformance spec to force a roll in under a second. With
that profile mounted, the venue always reaps the session (~1.5s) long
before a 10s partition heals, guaranteeing a Renegotiated roll (and the
resulting Ambiguous, permanently-unretried outbound mutations) on every
single run — this is a timer/scenario-duration mismatch in the CI
workflow, not a FIXP or reconnect-logic defect.

Drop the real-conformance.yml overlay from every compose invocation in
this workflow so trading-host boots against the base stack's
production-like bridge (docker/real/exchange-simulator.bridge.json,
heartbeat/idle=30s), which comfortably outlasts the 10s partition and
lets the scenario exercise genuine reattach recovery.

Refs #790.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
The drill's assert_post_recovery_trade() crosses Buy+Sell from the same
end-client (alice) at the same price, which the default-on
self_trade_prevention (and, for the Sell leg, AllowShortSell) gate
rejects. docker-compose.real-conformance.yml used to grant this same
opt-out (for the unrelated ReferencePriceLiveSpec) as a side effect of
an overlay that also swapped in fast conformance FIXP timers — removed
in the previous commit precisely because that timer swap caused #790.

Add a minimal docker-compose.chaos.yml overlay that grants only the
risk opt-out alice needs, without touching matching-platform's
bridge/timers, so trading-host keeps the production-like keepalive
values needed for the network-partition scenario to exercise a genuine
FIXP reattach.

Refs #790.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
The venue honors the client-negotiated keepAliveInterval (not its own
bridge default) and terminates the session as idle after ~3x that
interval of silence. The FIXP spec floor of 1000ms produced a ~3s
terminate threshold, far shorter than a realistic reconnect window,
forcing a spurious session roll (Renegotiate -> Ambiguous mutation
reclassification) on any brief disconnection instead of a clean
Reattach. 15s gives ~45s of tolerance, comfortably surviving the chaos
drill's 10s network-partition scenario and realistic real-world blips
while still detecting genuinely dead sessions promptly.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
PedroTravi and others added 3 commits September 5, 2026 03:24
…indows for new KeepAliveIntervalMs default

The venue terminates a FIXP session as idle after ~3x the client-
negotiated KeepAliveIntervalMs of inbound silence, NOT
SuspendedTimeoutMs (5 min) as these tests' names imply -- these tests
were (accidentally) exercising the keepalive-lapse boundary because
the pre-#792 default (1000ms) made that boundary far tighter (~3s)
than the venue's actual SuspendedTimeoutMs mechanism. After #792
raised the default to 15000ms (~45s threshold), the prior
PastWindowDisconnect=5000ms constant no longer exceeded the venue's
idle-terminate threshold, so PastSuspendedTimeout_Renegotiates_SurvivingOrderFlaggedStale
timed out waiting for a roll that never happened (session simply
reattached, sessionVerId stayed at 1) -- confirmed by the real-stack CI
run on PR #792. The stuck (never-rolled) order then broke that test's
cleanup path (which assumed a rolled/Ambiguous order needing
VenueAbsent resolution), leaving a stray resting order that cascaded
into failures on unrelated later specs sharing the same real-stack
sandbox.

Bump WithinWindowDisconnect 1000ms -> 5000ms and PastWindowDisconnect
5000ms -> 50000ms so both stay correctly positioned relative to the
new ~45s venue-side threshold.

Refs #790

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
The CI run on b27e676 showed WithinSuspendedTimeout_Reattaches_OrderSurvivesNoStaleFlag
newly failing at cleanup after bumping WithinWindowDisconnect 1000ms
-> 5000ms. That change was speculative and unnecessary -- 1000ms was
already comfortably under both the old (~3s) and new (~45s) venue
idle-terminate threshold, so there was no reason to touch it. Revert
to isolate the fix to the one constant that actually needed
recalibrating (PastWindowDisconnect) and narrow the diff for the next
CI iteration.

Refs #790

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…vered

Per code review: disconnectStartedUtc was captured before awaiting
docker.DisconnectMatchingAsync(), which spawns several sequential
docker subprocesses (docker availability check, network inspect,
network disconnect) before the partition takes effect. Timing the
WithinWindowDisconnect/PastWindowDisconnect budget from before that
setup latency understates the real elapsed idle time the venue
observes, eroding the margin against its ~3x KeepAliveIntervalMs
idle-terminate threshold (45s post-#792) -- risking exactly the flake
(session reattaches instead of rolling) this fix targets. Move the
timestamp capture to after DisconnectMatchingAsync() returns.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.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.

ci: "Chaos drill" network-partition scenario fails 100% of the time — post-recovery orders never reach Filled

2 participants