Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
85 commits
Select commit Hold shift + click to select a range
e4d46b1
Extract engine-construction factory from Sender.build()
jovfer Aug 17, 2026
6f3a3cf
Restore shared quarantined counter across construction and connect
jovfer Aug 17, 2026
3a30e4d
Add symbol_dict_reset config knobs
jovfer Aug 17, 2026
dc8a3f2
Arm symbol-dict recycle at flush; add advisory reset API
jovfer Aug 17, 2026
8f90f45
Prove threshold arming survives full-dict degradation
jovfer Aug 17, 2026
59559cf
Translate user-visible FSNs by a recycle epoch base
jovfer Aug 17, 2026
69255bd
Enforce roll precondition and fix a non-discriminating test
jovfer Aug 17, 2026
f9e986f
Recycle the send stack at the empty-backlog barrier
jovfer Aug 17, 2026
a2a1240
Guard the recycle swap against an unrebuildable engine
jovfer Aug 17, 2026
91b2f66
Pin memory-mode recycle behavior
jovfer Aug 17, 2026
903f2f6
Bounded blocking wait for a starved dictionary reset
jovfer Aug 17, 2026
3e68673
Fix round 1: correct starvation-wait javadoc, tighten tests
jovfer Aug 17, 2026
6180156
Pin recycle refusal conditions
jovfer Aug 17, 2026
2841394
Pin recycle behavior under outage and orphan drain
jovfer Aug 17, 2026
efd3e4f
Pin zero-catch-up on the recycle epoch boundary
jovfer Aug 17, 2026
c1a29bb
Fix round 1: correct Pin 4 mechanism attribution
jovfer Aug 17, 2026
debd45d
Pin recycle crash-window recovery
jovfer Aug 18, 2026
b5ba2b2
Fix round 1: pin arm (b) disk image, tighten oracles
jovfer Aug 18, 2026
07ecb27
Recycle heals full-dict degrade; expose recycle metrics
jovfer Aug 18, 2026
631ce88
Fix round 1: volatile metrics, tighten healing oracles
jovfer Aug 18, 2026
555fd48
Point cap error at the reset valve; raise client cap to 2M
jovfer Aug 18, 2026
e22376e
Fix round 1: drop tautological arm-state assertions
jovfer Aug 18, 2026
6dc84e5
Final wave: survive a step-7 connect failure without latching
jovfer Aug 18, 2026
06dcb6d
Fix round 2: recycle awaits a deferred engine close
jovfer Aug 19, 2026
65029ce
Make recycle-read monitoring fields volatile
jovfer Aug 20, 2026
6793928
Pin buffer-through-outage contract for the recycle
jovfer Aug 20, 2026
abed2a8
Defer the recycle reconnect to the I/O loop
jovfer Aug 20, 2026
dbbabf4
Seed the rebuilt loop's ever-connected flag
jovfer Aug 20, 2026
aa6a98f
Await the async post-recycle handshake in tests
jovfer Aug 20, 2026
fb28718
Make awaitAckedFsn recycle-safe; fix step-7 prose
jovfer Aug 20, 2026
d00acf9
Pin step-7 non-latching with a fault-injection test
jovfer Aug 24, 2026
895cc22
Add regression test for the batch-watermark reset guard
jovfer Aug 24, 2026
5986609
Split close()'s drain guard by true dependency
jovfer Aug 24, 2026
77d4ad7
Add re-arm floor to stop recycle thrash
jovfer Aug 24, 2026
16ac394
Commit the recycle swap only after a successful rebuild
jovfer Aug 24, 2026
b0a0126
Fix stale step-number docs left by the recycle reorder
jovfer Aug 24, 2026
dc99042
Make the recycle resumable instead of latching terminal
jovfer Aug 24, 2026
2ae927d
Pin the recycle's slot-heal and breach verdicts
jovfer Aug 24, 2026
8c41635
Never arm the reset on senders that cannot recycle
jovfer Aug 24, 2026
4bb08fb
Fix accessor truthfulness across the recycle window
jovfer Aug 24, 2026
dc6526b
Fix up M9 javadoc overclaim and make hasLoopEverConnected volatile
jovfer Aug 24, 2026
4b3894d
Fix stale doc claims left by the review wave
jovfer Aug 24, 2026
1d869cc
Pin the recycle re-arm floor arithmetic
jovfer Aug 27, 2026
269aa8a
Point comments at the renamed arming test
jovfer Aug 27, 2026
5591741
Hand the live error handler to the rebuild factory
jovfer Aug 27, 2026
e02a52c
Track a recovered engine's deferred close in the recycle
jovfer Aug 27, 2026
9d83a38
Pre-size the fresh dictionary, clamp getAckedFsn
jovfer Aug 27, 2026
2338976
Drop review finding IDs from the recycle tests
jovfer Aug 27, 2026
3ba9fbd
Pin the rebuild-time quarantine notification
jovfer Aug 27, 2026
1e7b73d
Pin getAckedFsn's torn-read clamp and null-engine window
jovfer Aug 27, 2026
c96ff5c
Spend one deferred-close budget per pending close
jovfer Aug 27, 2026
1391b9f
Clear the stale slot-lock latch before the rebuild
jovfer Aug 27, 2026
9969130
Report external FSNs in close-drain diagnostics
jovfer Aug 27, 2026
f83c21d
Bound the recycle threshold and wait knobs
jovfer Aug 27, 2026
66a91d7
Fit the ceiling test's threshold to the new bound
jovfer Aug 27, 2026
421ea94
State the recycle's listener and handler contract
jovfer Aug 27, 2026
9abc3be
Drop the redundant swap counter
jovfer Aug 27, 2026
9fc6060
Count the recycle counters correctly
jovfer Aug 27, 2026
7ef2381
Drop the last mention of the swap counter
jovfer Aug 27, 2026
42d55c6
Tighten the recycle suites and strip narration
jovfer Aug 27, 2026
50de646
Drop the last planning reference from a javadoc
jovfer Aug 27, 2026
f1c737d
Skip visibly when the interrupt loses the race
jovfer Aug 27, 2026
8e76d43
Rebase the abandon WARN and tidy three javadocs
jovfer Aug 27, 2026
a7c78c4
Merge remote-tracking branch 'origin/main' into qwp-dict-recycle-r1
jovfer Sep 8, 2026
f8d0876
Restore the pre-branch 23-arg connect(String,...) overload
jovfer Sep 8, 2026
06f8eda
Make the symbol-dict reset opportunistic by default
jovfer Sep 8, 2026
cd4d644
Pin the interrupt policy in the recycle park loops
jovfer Sep 8, 2026
7cd2201
Restore the interrupt flag on every deferred-close throw path
jovfer Sep 8, 2026
0794216
Re-register the delta baseline on CLOSE_LOOP resume
jovfer Sep 8, 2026
c6cf00d
Close the chunk debt on the resume's Error path
jovfer Sep 8, 2026
9d2de1f
Pin the resume fallback: baseline drop plus debt close
jovfer Sep 8, 2026
1b564c7
Prove the resume group is crash-safe end to end
jovfer Sep 8, 2026
9625a4a
State what the crash-replay test actually proves
jovfer Sep 8, 2026
a87779a
State the recycle trigger point in the three contracts
jovfer Sep 8, 2026
54b5297
Pin the everConnected seed against a post-recycle 401
jovfer Sep 8, 2026
1473b48
Pin FSN epoch-base accumulation across real recycles
jovfer Sep 8, 2026
7aa6dc4
Harden the monitoring surface and close review nits
jovfer Sep 8, 2026
280e379
Default the symbol-dict starvation wait to 2 s
jovfer Sep 9, 2026
259a16b
Snapshot arm (c)'s crash image only after the hot spare is installed
jovfer Sep 9, 2026
dd3e904
Poll the handshake count before pinning it in the refusal tests
jovfer Sep 9, 2026
81d5175
Keep the re-arm floor from being lowered by a manual reset
jovfer Sep 10, 2026
145a2de
Move the loop and engine counters into an adoptable holder
jovfer Sep 10, 2026
f28d9b5
Make the getTotal* counters sender-lifetime across recycles
jovfer Sep 10, 2026
ddb8ff0
Keep the delta baseline equal to the ringed coverage on a resume failure
jovfer Sep 10, 2026
b0497e8
Fold only the engine's own counter on adoption; pin the memory-mode r…
jovfer Sep 10, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ core/CMakeCache.txt
**/.project
**/.settings
**/.classpath
**/.factorypath
**/build
**/CMakeFiles
.envrc
Expand Down
444 changes: 366 additions & 78 deletions core/src/main/java/io/questdb/client/Sender.java

Large diffs are not rendered by default.

Original file line number Diff line number Diff line change
Expand Up @@ -193,15 +193,19 @@ public enum Kind {

/**
* The active wire connection dropped and the reconnect loop is about
* to start. Fired once per outage, before any retry attempts.
* to start. Fired once per outage, before any retry attempts. Not
* fired for the deliberate reconnect a symbol-dictionary recycle
* performs.
*/
DISCONNECTED,

/**
* A reconnect attempt succeeded against the same endpoint that was
* previously active. Fired once per successful reconnect to the same
* endpoint; {@link Kind#FAILED_OVER} is fired instead when the new
* endpoint differs.
* endpoint differs. Also fired — without a preceding
* {@link #DISCONNECTED} — for the reconnect that follows a
* symbol-dictionary recycle, which is deliberate, not an outage.
*/
RECONNECTED,

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,11 @@
* ({@link SenderConnectionEvent.Kind#CONNECTED},
* {@link SenderConnectionEvent.Kind#FAILED_OVER},
* {@link SenderConnectionEvent.Kind#RECONNECTED}) are guaranteed to fire on
* each transition. Failure events ({@code ENDPOINT_ATTEMPT_FAILED},
* each transition. A symbol-dictionary recycle tears the wire connection down
* and reconnects deliberately; that reconnect reports {@code RECONNECTED}
* (or {@code FAILED_OVER}) with no {@code DISCONNECTED} before it, so a
* listener pairing the two must not assume every success follows an outage.
* Failure events ({@code ENDPOINT_ATTEMPT_FAILED},
* {@code ALL_ENDPOINTS_UNREACHABLE}) may be coalesced under inbox pressure.
* The terminal event {@code AUTH_FAILED}
* fires before the producer-thread {@code LineSenderException} is observable on
Expand Down
10 changes: 6 additions & 4 deletions core/src/main/java/io/questdb/client/SenderErrorHandler.java
Original file line number Diff line number Diff line change
Expand Up @@ -33,11 +33,13 @@
*
* <h2>Threading</h2>
* Handlers normally run on a dedicated daemon dispatcher thread, never on the
* I/O thread or the producer thread. One exception: a build()-time quarantine
* I/O thread or the producer thread. One exception: a slot quarantine
* ({@link SenderError.Category#DATA_LOSS}) is dispatched synchronously on the
* thread calling {@code build()} — the async dispatcher belongs to the
* connected sender, which does not exist yet at build time. Handlers must not
* block: for the build-time case, {@code build()} is waiting.
* calling thread — at {@code build()} time because the async dispatcher does
* not exist yet, and during a symbol-dictionary recycle rebuild (inside
* {@code table(...)}, on the producer thread) because a data-loss notice must
* not be dropped under inbox pressure. Handlers must not block or call back
* into the sender: in both cases the caller is waiting.
* Slow handlers cannot stall publishing; if the bounded
* inbox fills up, surplus notifications are dropped (visible via
* {@code QwpWebSocketSender.getDroppedErrorNotifications()}).
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -155,7 +155,11 @@ public int getOrAddSymbol(CharSequence symbol) {
+ ". Rows using already-registered symbol values continue to work. To start a fresh "
+ "dictionary, close this sender and build a new one (with store-and-forward the "
+ "buffered backlog drains first). For unbounded-cardinality data use varchar "
+ "columns instead of symbol");
+ "columns instead of symbol. The automatic dictionary reset "
+ "(symbol_dict_reset, symbol_dict_reset_threshold) and "
+ "Sender.resetSymbolDictionary() avoid this cap, but both act only "
+ "on senders created via Sender.build()/fromConfig(), and the reset "
+ "itself runs at a table() call once the backlog is acknowledged.");
}

// Assign new ID — toString() only for new symbols that must be stored
Expand Down
1,741 changes: 1,608 additions & 133 deletions core/src/main/java/io/questdb/client/cutlass/qwp/client/QwpWebSocketSender.java

Large diffs are not rendered by default.

Original file line number Diff line number Diff line change
Expand Up @@ -1140,7 +1140,8 @@ public void run() {
maxHeadFrameRejections,
poisonMinEscalationWindowMillis,
catchUpCapGapMinEscalationWindowMillis,
CursorWebSocketSendLoop.ReconnectPolicy.ORPHAN);
CursorWebSocketSendLoop.ReconnectPolicy.ORPHAN,
0L);
// Without this the loop's ridden-out reports -- above all
// "credential-unavailable", the one endpoint-policy failure an ORPHAN
// loop retries rather than latching -- are dispatched into a null, and
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
/*+*****************************************************************************
* ___ _ ____ ____
* / _ \ _ _ ___ ___| |_| _ \| __ )
* | | | | | | |/ _ \/ __| __| | | | _ \
* | |_| | |_| | __/\__ \ |_| |_| | |_) |
* \__\_\\__,_|\___||___/\__|____/|____/
*
* Copyright (c) 2014-2019 Appsicle
* Copyright (c) 2019-2026 QuestDB
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*
******************************************************************************/

package io.questdb.client.cutlass.qwp.client.sf.cursor;

import java.util.concurrent.atomic.AtomicLong;

/**
* Sender-lifetime observability counters shared by every
* {@link CursorWebSocketSendLoop} generation and every {@link CursorSendEngine}
* a {@code QwpWebSocketSender} attaches.
* <p>
* A symbol-dictionary recycle rebuilds both the loop and the engine; the
* counters they report must not restart with them, so the sender owns one
* instance for its whole life and hands it to each new loop and engine before
* use ({@code adoptCounters}). A loop or engine built standalone (tests,
* background drainers) starts on a fresh instance of its own, so its getters
* behave exactly as they did when the counters were per-instance fields.
* <p>
* Every field is incremented atomically from whichever I/O thread (or the
* caller thread inside a synchronous {@code start()}) is active, and read by
* any monitor thread; each read is one atomic load, so a value sampled across
* a recycle is never a torn sum.
*/
public final class CursorSendCounters {
/** ACK frames received and applied; {@code CursorWebSocketSendLoop#getTotalAcks()}. */
public final AtomicLong acks = new AtomicLong();
/** Producer appends that hit a full ring and parked; {@code CursorSendEngine#getTotalBackpressureStalls()}. */
public final AtomicLong backpressureStalls = new AtomicLong();
/** Frames re-sent inside post-reconnect catch-up windows. */
public final AtomicLong framesReplayed = new AtomicLong();
/** Binary frames issued to the wire, replays included. */
public final AtomicLong framesSent = new AtomicLong();
/** Reconnect attempts, failed and successful alike. */
public final AtomicLong reconnectAttempts = new AtomicLong();
/** Successful reconnects. */
public final AtomicLong reconnects = new AtomicLong();
/** Non-OK / non-DURABLE_ACK frames received from the server, retriable and terminal. */
public final AtomicLong serverErrors = new AtomicLong();

/**
* Folds every counter of {@code other} into this instance. Used once, when a
* loop or engine that started on its own default instance is handed the
* sender's shared one, so anything counted before adoption is carried
* rather than dropped.
*/
public void addAll(CursorSendCounters other) {
acks.addAndGet(other.acks.get());
backpressureStalls.addAndGet(other.backpressureStalls.get());
framesReplayed.addAndGet(other.framesReplayed.get());
framesSent.addAndGet(other.framesSent.get());
reconnectAttempts.addAndGet(other.reconnectAttempts.get());
reconnects.addAndGet(other.reconnects.get());
serverErrors.addAndGet(other.serverErrors.get());
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -84,12 +84,14 @@ public final class CursorSendEngine implements QuietCloseable {
private static volatile ThreadFactory flockReleaseRetryThreadFactory =
DEFAULT_FLOCK_RELEASE_RETRY_THREAD_FACTORY;
private final long appendDeadlineNanos;
// Number of times appendBlocking observed BACKPRESSURE_NO_SPARE on its first
// ring.appendOrFsn attempt. One increment per blocking-call that had to wait
// for the manager (or for ACKs) — not one per spin-park. Producer-thread
// writer; volatile because the user may sample it from any thread.
private final java.util.concurrent.atomic.AtomicLong backpressureStallCount =
new java.util.concurrent.atomic.AtomicLong();
// Sender-lifetime observability counters; only backpressureStalls is used
// here: one increment per blocking appendBlocking call that had to wait
// for the manager (or for ACKs), not one per spin-park. Producer-thread
// writer; any thread may read it. A fresh instance by default;
// QwpWebSocketSender hands every engine it attaches its own shared
// instance via adoptCounters(), so a symbol-dictionary recycle's rebuilt
// engine keeps counting where the outgoing one stopped.
private CursorSendCounters counters = new CursorSendCounters();
// Constructed before an owned manager acquires its native path scratch, so
// callback allocation failure cannot orphan manager resources. A timed-out
// close can then hand it to either manager path without allocating.
Expand Down Expand Up @@ -923,7 +925,7 @@ public long appendBlocking(long payloadAddr, int payloadLen) {
}
// First miss → record one stall (not one per spin) and start the
// deadline clock.
backpressureStallCount.incrementAndGet();
counters.backpressureStalls.incrementAndGet();
long deadlineNs = System.nanoTime() + appendDeadlineNanos;
while (true) {
long now = System.nanoTime();
Expand All @@ -937,7 +939,7 @@ public long appendBlocking(long payloadAddr, int payloadLen) {
lastBackpressureLogNs = now;
LOG.warn("cursor producer backpressured ({} stalls so far); waiting for I/O or periodic disk sync; "
+ "will throw after {} ms",
backpressureStallCount.get(), appendDeadlineNanos / 1_000_000L);
counters.backpressureStalls.get(), appendDeadlineNanos / 1_000_000L);
}
LockSupport.parkNanos(50_000L); // 50 µs
fsn = ring.appendOrFsn(payloadAddr, payloadLen);
Expand Down Expand Up @@ -1349,6 +1351,11 @@ public SlotLock getSlotLockForTesting() {
return slotLock;
}

@TestOnly
public Runnable getSlotLockReleaseListenerForTesting() {
return slotLockReleaseListener;
}

@TestOnly
public long getSyncIntervalNanosForTesting() {
return syncIntervalNanos;
Expand Down Expand Up @@ -1623,6 +1630,32 @@ public void setSlotLockReleaseListener(Runnable listener) {
}
}

/**
* Replaces this engine's counters with the sender's shared, sender-lifetime
* instance, folding this engine's own backpressure-stall count into it
* (the other counters are the loop's, and an engine handed between
* senders must not carry a previous sender's totals). Producer thread
* only, before the first {@link #appendBlocking} on this engine -- the
* same attach window {@link #setSlotLockReleaseListener} uses.
*/
public void adoptCounters(CursorSendCounters shared) {
if (shared == counters) {
return;
}
shared.backpressureStalls.addAndGet(counters.backpressureStalls.get());
counters = shared;
}

/**
* The engine's current counters holder -- the DEFAULT instance until
* {@link #adoptCounters} replaces it. Test-only seam to observe what
* {@link #adoptCounters} folds and what it leaves alone.
*/
@TestOnly
public CursorSendCounters getCountersForTesting() {
return counters;
}

/**
* Re-arms the shared terminal retry for an engine whose final watermark
* barrier or confirmed flock release is still pending and no longer
Expand Down Expand Up @@ -1749,10 +1782,12 @@ public PersistedSymbolDict getPersistedSymbolDict() {
* Number of times {@link #appendBlocking} hit
* {@link SegmentRing#BACKPRESSURE_NO_SPARE} on its first attempt and
* had to wait for the segment manager (or for ACKs) to free space.
* One increment per blocking-call, not per spin-park. Cumulative.
* One increment per blocking-call, not per spin-park. Cumulative, and
* carried across a symbol-dictionary recycle once the owning sender has
* adopted this engine (see {@link #adoptCounters}).
*/
public long getTotalBackpressureStalls() {
return backpressureStallCount.get();
return counters.backpressureStalls.get();
}

/**
Expand Down
Loading
Loading