[TENT] Half-Open rail state machine + admit/isAvailable split - #1
Closed
Colors-111 wants to merge 2 commits into
Closed
Colors-111 wants to merge 2 commits into
Colors-111 wants to merge 2 commits into
Conversation
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 subscribe to this conversation on GitHub.
Already have an account?
Sign in.
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.
Description
Half-Open rail state machine + admit/isAvailable split
Follow-up to PR kvcache-ai#3946 (defect A + log rate-limiting). kvcache-ai#3946 capped the burst-escalation defect (cooldown 30s vs 300s) and rate-limited the failure-storm logs. A reviewer asked to split the planned Half-Open / Open-phase probing into this follow-up with one prerequisite:
This PR does exactly that: splits
available()into a pure predicateisAvailable()+ a mutatingadmit()with per-rail in-flight tracking, and adds Open-phase probing + Half-Open.State machine: Closed → Open → Half-Open → Closed/Open
!paused(),half_open=false,probe_in_flight=false): healthy.isAvailable()=true,admit()=true(no mutation). Normal transfers; completion is the fast-pathmarkRecoveredno-op.paused(),now < resume_time,half_open=false):isAvailable()=false.admit()arms ONE exploratory probe perprobe_interval_(setsprobe_in_flight+last_probe_time). Probe success →markRecovered→ Closed (defect B: transient faults recover in seconds, not the full cooldown). Probe failure →markFailedno-op (was paused, defect A), flag cleared.paused(),now >= resume_time,half_open=false):isAvailable()=false.admit()transitions to Half-Open and arms one trial. If an Open probe is still in flight at expiry,admit()returns false until it resolves.paused(),half_open=true,probe_in_flight=true):isAvailable()=false,admit()=false(one trial only). Trial success →markRecovered→ Closed, backoff reset to 0. Trial failure →markFailedescalatescooldown *= 2(cap 300s) and re-arms → Open.resume_timestays armed through Half-Open sopaused()stays true (not mistaken for Closed and flooded). Escalation happens only on a Half-Open trial failure; clock expiry alone never escalates. This removes the "expiry fully reopens" path that slammed a still-dead peer with every slice and re-triggered the storm at 30s/60s/120s.The split
available()had two jobs conflated: a query (is this rail usable?) and an admit (route a transfer through it, possibly arming a probe). It was called from:updateBestMapping(rail_monitor.cpp) — pure query, but the old expiry branch mutated state AND calledupdateBestMapping, recursing.selectOptimalDevice/selectFallbackDevice(workers.cpp) — admit; on true the slice is posted.Split:
bool isAvailable(int, int) const— pure predicate (!paused()). No mutation, noupdateBestMapping→ recursion gone. Used byupdateBestMappingand any query.bool admit(int, int)— the only mutating admit. Closed → true (no-op). Open → one probe perprobe_interval_(in-flight gated). expired-Open → Half-Open trial. Half-Open → false (trial in flight).void cancelProbe(int, int)— clears an armed probe/trial when a slice selected the rail but never reached the wire (see below).In-flight tracking (the reviewer's bug)
admit()armsprobe_in_flightat selection time, but selection ≠ posting. If a slice arms a probe then never reaches the wire, the flag is orphaned and the rail can never probe again. Three paths cause this; all are handled:selectFallbackDevice): reordered the GDR check beforeadmit, so a probe is never armed on a GDR-excluded pair. (selectOptimalDevicealready short-circuits GDR beforeadmit.)asyncPostSend): no endpoint obtained →cancelProbebeforesubmitFromTick.asyncPostSend):submitSlicesrejected the slice pre-wire →cancelProbebeforesubmitFromTick.cancelProbeclearsprobe_in_flightand revertshalf_opento expired-Open so the nextadmitre-arms. It is a no-op when no probe is in flight (normal transfers), so callers invoke it unconditionally on the re-queue path. Single-threaded per-worker ownership (eachWorkerContext::rails[machine_id]is oneRailMonitoron one worker thread) makes aboolsufficient: at most one probe/trial per rail, and the completion path (markFailed/markRecovered) clears the same flag the slice armed.What stays out of scope
unmount_expired_mem_segment/ stable segment names, not RailMonitor backoff. Half-Open only stops expiry from re-flooding the dead port; once a fresh replica is reachable, a trial succeeds within oneprobe_interval_.Verification
tent/tests/rail_monitor_test.cpp):BurstFailuresDoNotEscalateCooldown(probing disabled): 8×markFailed;admit()=true at 1.5s (cooldown stayed 1s → trial), false if escalated to 256s.TrialFailureEscalatesCooldown(probing disabled): trial fail → 1→2s, re-arm; 2s cooldown expires → trial.TrialSuccessResetsBackoff(probing disabled): trial success → Closed; next pause uses 1s, not 2s.ExpiryAdmitsOneTrialNotFullReopen(probing disabled): expiryadmit()=true (trial), nextadmit()=false (in-flight).InFlightProbeBlocksSecondAdmit(defaultprobe_interval=1s):admit()=true (probe armed),admit()=false (in-flight),markRecoveredclears → new probe arms. This is the test the reviewer said was missing.CancelProbeRevertsArmedTrial: trial armed →cancelProbe→ re-admits a fresh trial; no-op on a Closed rail.available()→isAvailable()(query semantics unchanged);CooldownDoesNotCarryOverAfterRecoveryadapted (final expiry assertion →admit()with probing disabled).admit()=false while an expired rail'sadmit()=true — this discriminates cooldown duration.InFlightProbeBlocksSecondAdmituses the default 1s to exercise in-flight directly (not to hide it).probe_intervaltests recovery.Files Changed
tent/include/tent/transport/rdma/rail_monitor.h—RailStategainslast_probe_time,half_open,probe_in_flight;available()→isAvailable()/admit()/cancelProbe(); newprobe_interval_+kCfgProbeIntervalSecs.tent/src/transport/rdma/rail_monitor.cpp—isAvailable/admit/cancelProbe;markFailed/markRecoveredin-flight + Half-Open handling;updateBestMapping→isAvailable;load()reads the probe key.tent/src/transport/rdma/workers.cpp—selectOptimalDevice/selectFallbackDeviceavailable()→admit(); GDR check moved beforeadmitin fallback;cancelProbeon the two pre-wire re-queue paths.tent/tests/rail_monitor_test.cpp— 10 renames, 2 adapted, 4 new tests.Configuration
transports/rdma/rail_error_thresholdtransports/rdma/rail_error_window_secstransports/rdma/rail_cooldown_secstransports/rdma/rail_probe_interval_secsModule
mooncake-transfer-engine)mooncake-store)mooncake-conductor)mooncake-reshard)mooncake-ep)mooncake-pg)mooncake-integration)mooncake-p2p-store)mooncake-wheel)mooncake-common)mooncake-rl)Type of Change
How Has This Been Tested?
Test commands:
# Example: bash scripts/run_ci_test.shTest results:
Checklist
./scripts/code_format.shAI Assistance Disclosure