TENT Failover#
TENT hides transfer failures from the application by recovering inside the data path. This document describes how the recovery works, which knobs control it, and how it is tested.
The design has two layers:
Cross-transport failover in
TransferEngineImpl. When a transport fails a task at the completion stage, the engine moves that task to the next available transport (for example RDMA → TCP). Submit-stage failures are not retried today; see Known Gaps.Intra-RDMA rail recovery in
RailMonitor. When a specific (local NIC, remote NIC) rail keeps failing, the monitor pauses it with exponential cooldown; a successful transfer or the cooldown expiry brings it back.
Application code submits a batch and polls getTransferStatus. It never sees a FAILED task as long as any healthy path remains and the failover budget is not exhausted.
Fault Model#
TENT focuses on three kinds of transient faults:
Fault |
Surface |
Recovery action |
|---|---|---|
Work request completion error (WC error) |
RDMA worker sees a bad completion |
Rail-level |
QP / endpoint failure |
|
Not retried today: task surfaces as |
Peer disconnect mid-transfer |
|
Cross-transport failover |
Permanent or application-visible errors (invalid arguments, out-of-memory, segment not found) are not retried; they are returned to the caller as-is.
Architecture#
+-------------------------+
submitTransfer | TransferEngineImpl |
---------------> classify by TransportType
| submitTransferTasks |----failure----+
+-------------------------+ |
| v
| resubmitTransferTask
| (bump priority, pick next
| transport, resubmit)
v
+---------------------------+
| RdmaTransport / workers |
| +---------------------+ |
| | RailMonitor | |
| | per-rail state | |
| | cooldown / recover | |
| +---------------------+ |
+---------------------------+
Each request is owned by one
TaskInfo.typenames the transport currently executing the task;xport_priorityis the index into the ranked fallback list;failover_countcaps how many times we may re-resolve the transport.The ranked fallback list comes from
getTransportType(req, priority). Priority 0 yields the best available transport; increasing priority walks down the list;UNSPECmeans no transport left.RDMA rail state lives in
RailMonitor. Its lifecycle is independent of the task-level state machine: a rail can be paused while tasks keep flowing on other rails.
State Machine#
Cross-transport failover#
resubmitTransferTask is the single entry point that promotes a failing task to the next transport:
++task.failover_count
if failover_count > max_failover_attempts -> return error (exhausted)
task.xport_priority++
type = resolveTransport(task.request, task.xport_priority)
if type == UNSPEC -> return error (no transport)
transport_list_[type]->submitTransferTasks(...)
It has two callers, one per recoverable failure surface:
Completion-stage failure.
getTransferStatus(batch_id, task_id, status)and the batch-form overload callresubmitTransferTaskonce perFAILEDcompletion. On success the task is re-markedPENDINGso the aggregated batch status does not latch toFAILEDbecause of a task that is actually retrying.Exhaustion. When the budget is hit,
resubmitTransferTasksets the returned status toInvalidEntry("Failover limit exceeded, all transports exhausted"). Callers leavetask.typeunchanged; the task then reportsFAILEDthrough the normal status flow.
Submit-stage failures (submitTransferTasks returning non-OK) are not retried today. They mark the task as UNSPEC, and getTransferStatus short-circuits to FAILED. See Known Gaps for why.
RDMA rail recovery#
Inside RdmaTransport, each completion drives the rail monitor:
Bad completion →
rail.markFailed(local_nic, remote_nic)Good completion →
rail.markRecovered(local_nic, remote_nic)
markFailed bumps error_count inside error_window_. Once the count hits error_threshold_ the rail is paused until now + cooldown_; the cooldown doubles on every repeat failure up to kMaxCooldown (300 s).
markRecovered clears the error count, un-pauses the rail, and resets the exponential-backoff memory so the next failure cycle starts from the initial cooldown. A fast path returns without work when the rail is already healthy, which is the common case on the completion hot path.
available(local, remote) is the gate every work request passes through before posting. If the cooldown has expired, available itself resets all backoff state (error count, resume time, cooldown) and logs Rail recovered: ... (cooldown expired). Otherwise it returns false and the scheduler picks another rail via findBestRemoteDevice.
This produces two independent recovery signals — cooldown expiry and live success — so a flaky rail does not stall forever if no other rail is posted to, and a recovered rail returns to service at the first good completion instead of waiting for the full cooldown.
Configuration#
All knobs live in the top-level transfer-engine.json. Defaults are safe for production; tune only if you have evidence.
Key |
Default |
Meaning |
|---|---|---|
|
|
Controls whether |
|
|
Upper bound on |
|
|
Number of failures inside |
|
|
Sliding window for counting rail errors. A failure older than the window resets |
|
|
Initial cooldown after tripping. Doubles on each repeat failure, capped at 300 s. |
The RDMA keys are read by RailMonitor::load. Example:
{
"enable_auto_failover_on_poll": true,
"max_failover_attempts": 3,
"transports": {
"rdma": {
"rail_error_threshold": 3,
"rail_error_window_secs": 10,
"rail_cooldown_secs": 30
}
}
}
Observability#
Metric#
tent_transport_failover_total is a counter incremented once per successful transport switch inside resubmitTransferTask. A non-zero rate means the engine is actively recovering; a sudden jump usually points at a single bad link or flaky peer.
The counter is only built when TENT is compiled with -DTENT_METRICS_ENABLED=ON (see metrics.md). Without that flag the macro is a no-op.
Log keywords#
Keyword |
Interpretation |
|---|---|
|
A task has successfully switched transports. |
|
Task exhausted its budget and will surface |
|
|
|
Cooldown elapsed and the rail is back in service. |
|
Live success on a previously paused rail brought it back early. |
Testing#
Real hardware faults are hard to stage, so failover is tested by driving the real TransferEngineImpl with FakeTransport backends and a fault-injecting decorator. The engine is unmodified: a completion-stage FAILED looks like a WC error or a dropped peer, and resubmitTransferTask runs as it would in production.
The harness — why a fake Transport is enough, how fakes are swapped in, and what this can and cannot prove — is in TENT Testing. That page is the mechanism; this section only notes what failover uses it for:
Completion-stage
FAILEDon the primary must resubmit on the next available transport.Exhausting
max_failover_attempts(including0and1) must surfaceFAILEDand must not touch a transport beyond the budget.failover_countis per-task: one failing request must not spend another request’s budget.With
enable_auto_failover_on_poll=false, status polling is observational;progressBatch/waitTransferCompletion/transferSyncstill recover.
Submit-stage failures are intentionally not covered here; see Known Gaps.
Known Gaps#
Submit-stage failures do not trigger failover. When
submitTransferTasksreturns non-OK, every task in that call is markedUNSPECand surfaces asFAILED. A naive retry loop here is unsafe for two reasons:Merged requests. When
merge_requestsis enabled (default),task_id_list[type]contains both the real merged task and its derived aliases. Resubmitting per task-id re-posts one logical transfer multiple times on the fallback transport, breaking the deduplication the merge pass established.Partial enqueue. Some transports (for example
ShmTransport::submitTransferTasks,NVLinkTransport::submitTransferTasks) enqueue or start work for earlier requests inrequest_listbefore returning an error on a later one. The return status alone does not tell us which tasks partially succeeded, so a blanket resubmit would duplicate already-started transfers. A safe submit-stage recovery needs either (a) a transport-level “atomic submit” capability flag plus per-task skip of derived ids, or (b) per-request status returned fromsubmitTransferTasks. Neither exists today.
markRecovered(and cooldown expiry inavailable) clears the exponential-backoff memory entirely. A rail that flaps repeatedly therefore does not accumulate a growing cooldown across recovery cycles. If this becomes a problem the fix is to decay rather than reset.Cross-transport failover is driven purely by return status; there is no latency-based “this transport is healthy but too slow, try another” signal. That belongs to the scheduler, not this document.
Runtime-layer failover is covered by FakeTransport tests in the
tent-cicuda-offlegs. DMA integrity, real WC errors, and staging under NVLink still need hardware runners; see TENT Testing.