@@ -168,6 +168,11 @@ public final class CursorWebSocketSendLoop implements QuietCloseable {
168168 * Throttle "reconnect attempt N failed" WARN logs to one per 5 s.
169169 */
170170 private static final long RECONNECT_LOG_THROTTLE_NANOS = 5_000_000_000L ;
171+ // True when this loop delta-encodes symbol dictionaries: it keeps a reconnect
172+ // catch-up mirror (sentDict*) and re-registers the whole dictionary on a fresh
173+ // server before replaying delta frames. See the sentDict* fields in the mutable
174+ // section for the full mechanism. Gates all the delta-dict state.
175+ private final boolean deltaDictEnabled ;
171176 // Pre-converted to nanos for the comparison in sendDurableAckKeepaliveIfDue.
172177 // Zero or negative disables the keepalive entirely.
173178 private final long durableAckKeepaliveIntervalNanos ;
@@ -211,51 +216,6 @@ public final class CursorWebSocketSendLoop implements QuietCloseable {
211216 private final long reconnectMaxDurationMillis ;
212217 private final WebSocketResponse response = new WebSocketResponse ();
213218 private final ResponseHandler responseHandler = new ResponseHandler ();
214- // Delta symbol dictionary catch-up state (see swapClient). Active in memory
215- // mode, and in disk mode whenever the per-slot persisted dictionary opened --
216- // fresh slots included, not just recovered / orphan-drained ones. On a
217- // recovered / orphan-drained slot the constructor additionally SEEDS sentDict*
218- // from that persisted dictionary; a fresh slot starts with an empty mirror and
219- // grows it as frames are sent.
220- // deltaDictEnabled gates all of it. The loop mirrors, in sentDict*, every
221- // symbol it has ever sent -- the concatenated [len varint][utf8] bytes in
222- // global-id order (sentDictBytes*) plus the count (sentDictCount) -- so that
223- // on reconnect it can re-register the whole dictionary on the fresh server
224- // (which discards its dictionary on every disconnect) before replaying frames
225- // whose deltas start above id 0. All of this is touched only by the I/O thread.
226- // Footprint note: this mirror is a SECOND copy of the dictionary -- the same
227- // symbols the producer's GlobalSymbolDictionary already holds as Java Strings --
228- // kept as native UTF-8 bytes for the reconnect-catch-up capability. So a
229- // memory-mode connection's steady-state dictionary footprint is ~2x the symbol
230- // set. It is bounded by distinct-symbol count (not per-row) and never trimmed
231- // for the connection's lifetime (a reconnect may need the whole dictionary at
232- // any moment), so it cannot be dropped; it is an intentional cost of the feature.
233- private final boolean deltaDictEnabled ;
234- // Cap-gap attempts -- catch-ups that reached a node and found an entry too large
235- // for its batch cap -- since the last SUCCESSFUL catch-up (see
236- // MAX_CATCHUP_CAP_GAP_ATTEMPTS for the full budget accounting). A successful
237- // catch-up resets it (sendDictCatchUp); a transient reconnect neither increments
238- // nor resets it. NOT reset per connection -- it measures the cap-gap episode
239- // across reconnects so a persistent gap eventually latches. I/O-thread-only.
240- private int catchUpCapGapAttempts ;
241- // True once a real ring frame (data or commit) has been sent on the CURRENT
242- // connection, as opposed to only the dictionary catch-up. The catch-up
243- // consumes wire sequences (nextWireSeq), so nextWireSeq > 0 no longer implies
244- // "the head frame was sent": onClose's poison-strike gate and
245- // handleServerRejection's pre-send gate key off THIS instead. Without it, a
246- // transient outage AFTER the catch-up but BEFORE the first data frame (a
247- // flapping LB/middlebox that accepts the upgrade + catch-up then closes) would
248- // be mistaken for a deterministic head-frame rejection and escalate to a
249- // PROTOCOL_VIOLATION terminal -- breaking the store-and-forward "retry a
250- // transient outage forever" contract. Reset per connection in
251- // setWireBaselineWithCatchUp; set in trySendOne after a successful send.
252- private boolean dataFrameSentThisConnection ;
253- private long sentDictBytesAddr ;
254- private int sentDictBytesCapacity ;
255- private int sentDictBytesLen ;
256- private int sentDictCount ;
257- // End position (native address) written by the last readVarintAt() call.
258- private long varintEnd ;
259219 private final CountDownLatch shutdownLatch = new CountDownLatch (1 );
260220 private final AtomicLong totalAcks = new AtomicLong ();
261221 // Counters for observability of the durable-ack path. Both are zero
@@ -279,6 +239,49 @@ public final class CursorWebSocketSendLoop implements QuietCloseable {
279239 // by category. Includes both retriable and terminal outcomes — i.e. every
280240 // server-side rejection observed regardless of how the loop reacted.
281241 private final AtomicLong totalServerErrors = new AtomicLong ();
242+ // Delta symbol dictionary catch-up state (see swapClient, deltaDictEnabled).
243+ // Active in memory mode, and in disk mode whenever the per-slot persisted
244+ // dictionary opened -- fresh slots included, not just recovered / orphan-drained
245+ // ones. On a recovered / orphan-drained slot the constructor additionally SEEDS
246+ // sentDict* from that persisted dictionary; a fresh slot starts with an empty
247+ // mirror and grows it as frames are sent. The loop mirrors, in sentDict*, every
248+ // symbol it has ever sent -- the concatenated [len varint][utf8] bytes in
249+ // global-id order (sentDictBytes*) plus the count (sentDictCount) -- so that on
250+ // reconnect it can re-register the whole dictionary on the fresh server (which
251+ // discards its dictionary on every disconnect) before replaying frames whose
252+ // deltas start above id 0. All of this is touched only by the I/O thread.
253+ // Footprint note: this mirror is a SECOND copy of the dictionary -- the same
254+ // symbols the producer's GlobalSymbolDictionary already holds as Java Strings --
255+ // kept as native UTF-8 bytes for the reconnect-catch-up capability. So a
256+ // memory-mode connection's steady-state dictionary footprint is ~2x the symbol
257+ // set. It is bounded by distinct-symbol count (not per-row) and never trimmed for
258+ // the connection's lifetime (a reconnect may need the whole dictionary at any
259+ // moment), so it cannot be dropped; it is an intentional cost of the feature.
260+ private long sentDictBytesAddr ;
261+ private int sentDictBytesCapacity ;
262+ private int sentDictBytesLen ;
263+ private int sentDictCount ;
264+ // Cap-gap attempts -- catch-ups that reached a node and found an entry too large
265+ // for its batch cap -- since the last SUCCESSFUL catch-up (see
266+ // MAX_CATCHUP_CAP_GAP_ATTEMPTS for the full budget accounting). A successful
267+ // catch-up resets it (sendDictCatchUp); a transient reconnect neither increments
268+ // nor resets it. NOT reset per connection -- it measures the cap-gap episode
269+ // across reconnects so a persistent gap eventually latches. I/O-thread-only.
270+ private int catchUpCapGapAttempts ;
271+ // True once a real ring frame (data or commit) has been sent on the CURRENT
272+ // connection, as opposed to only the dictionary catch-up. The catch-up consumes
273+ // wire sequences (nextWireSeq), so nextWireSeq > 0 no longer implies "the head
274+ // frame was sent": onClose's poison-strike gate and handleServerRejection's
275+ // pre-send gate key off THIS instead. Without it, a transient outage AFTER the
276+ // catch-up but BEFORE the first data frame (a flapping LB/middlebox that accepts
277+ // the upgrade + catch-up then closes) would be mistaken for a deterministic
278+ // head-frame rejection and escalate to a PROTOCOL_VIOLATION terminal -- breaking
279+ // the store-and-forward "retry a transient outage forever" contract. Reset per
280+ // connection in setWireBaselineWithCatchUp; set in trySendOne after a successful
281+ // send.
282+ private boolean dataFrameSentThisConnection ;
283+ // End position (native address) written by the last readVarintAt() call.
284+ private long varintEnd ;
282285 private WebSocketClient client ;
283286 // Optional: when non-null, every server-rejection error (retriable and
284287 // terminal alike) is offered to the dispatcher for async delivery to the user's
0 commit comments