Skip to content

Commit 8d6a932

Browse files
committed
Keep foreground cap-gap retries unbounded
Separate symbol-dictionary catch-up cap-gap policy by loop owner. Foreground senders now retry until a compatible node returns, while orphan drainers retain bounded quarantine behavior. Preserve existing constructor signatures and add unit and integration coverage for retries beyond the orphan budget and recovery after the server cap is restored.
1 parent 0520dc4 commit 8d6a932

6 files changed

Lines changed: 197 additions & 131 deletions

File tree

core/src/main/java/io/questdb/client/Sender.java

Lines changed: 5 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1760,21 +1760,19 @@ public Sender build() {
17601760

17611761
/**
17621762
* Minimum wall-clock time (millis) a symbol-dictionary catch-up CAP GAP must
1763-
* persist before the sender gives up on it and fails.
1763+
* persist before an orphan store-and-forward drainer quarantines its slot.
17641764
* <p>
17651765
* A cap gap means a symbol already accepted by one node is too large to
17661766
* re-register on the node the sender just failed over to, because that node
17671767
* advertises a smaller maximum batch size. On a homogeneous cluster this cannot
17681768
* happen; it takes a heterogeneous or mid-roll cluster, or an operator lowering
17691769
* the cap below existing data.
17701770
* <p>
1771-
* The sender retries such a gap across reconnects rather than failing on sight,
1772-
* because the larger-cap node may simply be away -- a rolling restart is the most
1773-
* likely reason a failover happened at all. It gives up only once the gap has BOTH
1774-
* recurred many times AND persisted for this long, so an ordinary cluster
1775-
* operation cannot bring down a live producer. Raise it for a cluster whose
1771+
* A foreground sender retries such a gap indefinitely because the larger-cap node
1772+
* may simply be away. An orphan drainer may quarantine only once the gap has BOTH
1773+
* recurred many times AND persisted for this long. Raise it for a cluster whose
17761774
* rolling restarts take longer than the 5-minute default; set it to {@code 0} to
1777-
* fail as soon as the retry count is exhausted.
1775+
* quarantine an orphan slot as soon as the retry count is exhausted.
17781776
* <p>
17791777
* WebSocket transport only.
17801778
*/

core/src/main/java/io/questdb/client/cutlass/qwp/client/QwpWebSocketSender.java

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -314,9 +314,9 @@ public class QwpWebSocketSender implements Sender {
314314
// maxFrameRejections (connect-string key poison_min_escalation_window_millis).
315315
private long poisonMinEscalationWindowMillis =
316316
CursorWebSocketSendLoop.DEFAULT_POISON_MIN_ESCALATION_WINDOW_MILLIS;
317-
// Minimum wall-clock dwell a symbol-dict catch-up cap gap must persist before the
318-
// send loop latches a terminal (connect-string key
319-
// catchup_cap_gap_min_escalation_window_millis). See
317+
// Minimum wall-clock dwell a symbol-dict catch-up cap gap must persist before an
318+
// orphan drainer may quarantine its slot (connect-string key
319+
// catchup_cap_gap_min_escalation_window_millis). Foreground senders retry forever. See
320320
// CursorWebSocketSendLoop.DEFAULT_CATCHUP_CAP_GAP_MIN_ESCALATION_WINDOW_MILLIS.
321321
private long catchUpCapGapMinEscalationWindowMillis =
322322
CursorWebSocketSendLoop.DEFAULT_CATCHUP_CAP_GAP_MIN_ESCALATION_WINDOW_MILLIS;
@@ -3358,7 +3358,8 @@ private void ensureConnected() {
33583358
durableAckKeepaliveIntervalMillis,
33593359
maxFrameRejections,
33603360
poisonMinEscalationWindowMillis,
3361-
catchUpCapGapMinEscalationWindowMillis);
3361+
catchUpCapGapMinEscalationWindowMillis,
3362+
CursorWebSocketSendLoop.CatchUpCapGapPolicy.RETRY_FOREVER);
33623363
// Plug the async-delivery sink before start() so the I/O thread
33633364
// never observes a null dispatcher between recordFatal and
33643365
// notification — the test for null in dispatchError handles

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

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -133,9 +133,9 @@ public final class BackgroundDrainer implements Runnable {
133133
// drain loop; mirrors the owner sender's poison_min_escalation_window_millis.
134134
private final long poisonMinEscalationWindowMillis;
135135
// Minimum wall-clock dwell a symbol-dict catch-up cap gap must persist before this
136-
// drainer's send loop latches a terminal. Same knob the foreground sender uses; the
137-
// orphan drainer honours it too so a rolling restart cannot quarantine an otherwise
138-
// drainable slot on a strike count alone.
136+
// orphan drainer's send loop latches a terminal. Foreground senders retry forever;
137+
// this bounded policy is permitted only so a persistent orphan slot can be
138+
// quarantined for operator intervention.
139139
private final long catchUpCapGapMinEscalationWindowMillis;
140140

141141
public BackgroundDrainer(
@@ -584,7 +584,8 @@ public void run() {
584584
durableAckKeepaliveIntervalMillis,
585585
maxHeadFrameRejections,
586586
poisonMinEscalationWindowMillis,
587-
catchUpCapGapMinEscalationWindowMillis);
587+
catchUpCapGapMinEscalationWindowMillis,
588+
CursorWebSocketSendLoop.CatchUpCapGapPolicy.TERMINAL_AFTER_SETTLE_BUDGET);
588589
loop.start();
589590

590591
while (!stopRequestedOrInterrupted()) {

0 commit comments

Comments
 (0)