Skip to content

Commit fe0148a

Browse files
committed
docs(qwp): align reconnect-budget docs with Invariant B; mark dead field
Documentation-only cleanup after the Invariant B fix (no behavior change): - CursorWebSocketSendLoop: correct the class + fail() javadoc that still said the reconnect loop is "time-capped at reconnect_max_duration_millis". It now retries indefinitely (capped exponential backoff); only auth/upgrade rejects or SF exhaustion are terminal. - CursorWebSocketSendLoop: comment the now-dead reconnectMaxDurationMillis field -- retained for constructor symmetry / caller API, but no longer read by connectLoop. The budget still bounds the blocking (non-lazy) initial connect via connectWithRetry, which takes it as an explicit argument. - BackgroundDrainer: clarify that the wall-clock budget + 16-attempt cap apply ONLY to a genuine durable-ack capability gap; a transient all-replica failover window is retried indefinitely and never quarantined.
1 parent d0a794a commit fe0148a

2 files changed

Lines changed: 32 additions & 20 deletions

File tree

core/src/main/java/io/questdb/client/cutlass/qwp/client/sf/cursor/BackgroundDrainer.java

Lines changed: 16 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -52,22 +52,28 @@
5252
* <li>Close everything in reverse order; release the lock.</li>
5353
* </ol>
5454
* <p>
55-
* On terminal failure (auth-rejection on reconnect, reconnect-budget
56-
* exhaustion, recovery error), the drainer drops a
57-
* {@link OrphanScanner#FAILED_SENTINEL_NAME} sentinel into the slot
58-
* before exiting. Future scans skip the slot until an operator clears
59-
* the sentinel — bounded automatic retry, then human-in-the-loop.
55+
* On terminal failure (auth-rejection on reconnect, a cluster-wide durable-ack
56+
* capability gap that exhausts its settle budget, recovery error), the drainer
57+
* drops a {@link OrphanScanner#FAILED_SENTINEL_NAME} sentinel into the slot
58+
* before exiting. Future scans skip the slot until an operator clears the
59+
* sentinel — bounded automatic retry, then human-in-the-loop. A transient
60+
* all-replica failover window is NOT terminal: it is retried indefinitely
61+
* (Invariant B), never quarantined on a wall-clock budget or attempt cap.
6062
*/
6163
public final class BackgroundDrainer implements Runnable {
6264

6365
/**
6466
* Cap on consecutive {@link QwpDurableAckMismatchException} attempts at
6567
* initial connect before the drainer escalates to a {@code .failed}
66-
* sentinel. The wall-clock budget {@code reconnectMaxDurationMillis}
67-
* also caps the same loop; whichever is hit first triggers escalation.
68-
* 16 attempts gives the cluster room to settle through a rolling
69-
* upgrade (each attempt walks every endpoint internally) without
70-
* letting a genuine cluster-wide misconfig hang the drainer forever.
68+
* sentinel. Applies ONLY to a genuine cluster-wide durable-ack capability
69+
* gap (a server that upgrades but does not advertise durable ack); a
70+
* transient all-replica failover window (role reject) is retried
71+
* indefinitely and is never subject to this cap (Invariant B). The
72+
* wall-clock budget {@code reconnectMaxDurationMillis} also caps this
73+
* capability-gap loop; whichever is hit first triggers escalation. 16
74+
* attempts gives the cluster room to settle through a rolling upgrade
75+
* (each attempt walks every endpoint internally) without letting a genuine
76+
* cluster-wide misconfig hang the drainer forever.
7177
*/
7278
public static final int DEFAULT_MAX_DURABLE_ACK_MISMATCH_ATTEMPTS = 16;
7379
private static final Logger LOG = LoggerFactory.getLogger(BackgroundDrainer.class);

core/src/main/java/io/questdb/client/cutlass/qwp/client/sf/cursor/CursorWebSocketSendLoop.java

Lines changed: 16 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -61,10 +61,11 @@
6161
* cumulative wire sequence {@code N}, calls
6262
* {@code engine.acknowledge(fsnAtZero + N)} so the segment manager
6363
* can trim fully-acked segments.</li>
64-
* <li>On wire failure, runs the configured reconnect policy: backoff
65-
* with jitter up to {@code reconnect_max_duration_millis}, with
66-
* auth-style failures (401/403/non-101 upgrade reject) treated as
67-
* terminal. On reconnect success, repositions the cursor at
64+
* <li>On wire failure, runs the configured reconnect policy: capped
65+
* exponential backoff with jitter, retried indefinitely (Invariant B --
66+
* a store-and-forward drainer never gives up on a wall-clock budget),
67+
* with only auth-style failures (401/403/non-101 upgrade reject) treated
68+
* as terminal. On reconnect success, repositions the cursor at
6869
* {@code ackedFsn+1} and replays.</li>
6970
* </ol>
7071
* No locks on the steady-state path. The producer thread (user) writes
@@ -140,6 +141,11 @@ public final class CursorWebSocketSendLoop implements QuietCloseable {
140141
private final ReconnectFactory reconnectFactory;
141142
private final long reconnectInitialBackoffMillis;
142143
private final long reconnectMaxBackoffMillis;
144+
// Retained for constructor symmetry and passed in by callers, but NOT
145+
// consulted by the background loop: Invariant B removed the wall-clock
146+
// give-up from connectLoop. The budget still bounds the blocking (non-lazy)
147+
// initial connect via QwpWebSocketSender -> connectWithRetry, which takes it
148+
// as an explicit argument rather than reading this field.
143149
private final long reconnectMaxDurationMillis;
144150
private final WebSocketResponse response = new WebSocketResponse();
145151
private final ResponseHandler responseHandler = new ResponseHandler();
@@ -1019,12 +1025,12 @@ private void enqueuePendingOk(long wireSeq) {
10191025

10201026
/**
10211027
* Surface a wire failure. With reconnect plumbing wired (factory +
1022-
* listener both non-null), enters the per-outage retry loop:
1023-
* exponential backoff with jitter, time-capped at
1024-
* {@code reconnectMaxDurationMillis}, terminal on auth/upgrade
1025-
* rejections (so the budget isn't burned on errors that won't fix
1026-
* themselves). On the first successful reconnect within the budget,
1027-
* the I/O loop resumes with reset wire state and replays from
1028+
* listener both non-null), enters the per-outage retry loop: capped
1029+
* exponential backoff with jitter, retried for as long as the loop is
1030+
* running -- there is NO wall-clock give-up (Invariant B: a store-and-
1031+
* forward drainer only terminates on SF exhaustion or a genuinely non-
1032+
* retriable auth/upgrade reject). On the first successful reconnect the
1033+
* I/O loop resumes with reset wire state and replays from
10281034
* {@code engine.ackedFsn() + 1}.
10291035
* <p>
10301036
* Without reconnect plumbing, the failure is immediately terminal

0 commit comments

Comments
 (0)