Skip to content
Open
Show file tree
Hide file tree
Changes from 2 commits
Commits
Show all changes
178 commits
Select commit Hold shift + click to select a range
de86197
Delta-encode the QWP symbol dictionary
glasstiger Jul 9, 2026
3092230
Contain QWP dict catch-up send failures in the reconnect loop
glasstiger Jul 9, 2026
9f3f7bd
Resume the QWP symbol-dict write-ahead from the durable size
glasstiger Jul 9, 2026
09cd706
Guard the oversized catch-up entry terminal
glasstiger Jul 9, 2026
18dd545
Persist the QWP symbol-dict batch in a single write
glasstiger Jul 9, 2026
2a1a2d6
Free the recovery-seeded dict mirror in close()
glasstiger Jul 9, 2026
e130da9
Pin the recovery test on the catch-up frame
glasstiger Jul 9, 2026
2b78b0e
Cover the disk-mode full-dict fallback
glasstiger Jul 9, 2026
282ac09
Truncate the torn tail when reopening the symbol dict
glasstiger Jul 9, 2026
754eb3d
Guard the sent-dict mirror against int overflow
glasstiger Jul 9, 2026
01f251d
Merge branch 'main' into qwp-delta-symbol-dict
bluestreak01 Jul 9, 2026
4f44fb3
Parse the delta header once per frame on send
glasstiger Jul 9, 2026
0f9d88f
Fix stale self-sufficient-frame comments
glasstiger Jul 9, 2026
d89f14f
Make the delta-dict tests deterministic and leak-checked
glasstiger Jul 9, 2026
27aad9e
Cover the split-batch delta contract
glasstiger Jul 9, 2026
034d8f3
Accumulate the tail of a partial-overlap delta into the mirror
glasstiger Jul 9, 2026
41fddb1
Guard against persisted-dict duplication on a failed publish
glasstiger Jul 9, 2026
f9430bc
Merge branch 'main' into qwp-delta-symbol-dict
glasstiger Jul 9, 2026
9602440
Merge branch 'qwp-delta-symbol-dict' of https://github.com/questdb/ja…
glasstiger Jul 9, 2026
0ef96af
Cover the reconnect catch-up ACK alignment
glasstiger Jul 9, 2026
ff79b72
Cover the retriable catch-up send containment
glasstiger Jul 9, 2026
8fd9ad1
Tidy catch-up comments and harden two edges
glasstiger Jul 9, 2026
8fcd835
Fix delta symbol-dict recovery and reconnect bugs
glasstiger Jul 10, 2026
fc8b4ba
Adopt recovered dictionary into the send mirror
glasstiger Jul 10, 2026
8dd7723
Harden delta symbol-dict recovery and NACK gating
glasstiger Jul 10, 2026
8853d8f
Persist symbol delta from frame, skip re-encode
glasstiger Jul 10, 2026
10a125c
Guard catch-up frame overflow, add biting tests
glasstiger Jul 10, 2026
fa50e81
Fix orphan-drain losing symbol-dict mirror seed
glasstiger Jul 10, 2026
444199e
Fix symbol-dict durability doc; add recovery test
glasstiger Jul 10, 2026
7a95653
Discard a surviving symbol dict on fresh start
glasstiger Jul 10, 2026
6d14727
Harden delta symbol-dict send and persist paths
glasstiger Jul 10, 2026
fdd7141
Tidy delta symbol-dict dead code and edges
glasstiger Jul 10, 2026
2fdbbf9
Fix catch-up terminal on a homogeneous batch cap
glasstiger Jul 10, 2026
8cfd7bd
Fix recovery id desync on UTF-8-colliding symbols
glasstiger Jul 10, 2026
0b4ab3f
Harden symbol-dict resource teardown paths
glasstiger Jul 10, 2026
186ae10
Defer catch-up commit; fix stale delta comments
glasstiger Jul 10, 2026
fef7203
Plug ensureConnected leak; consolidate varint helpers
glasstiger Jul 10, 2026
cbaf474
Add a per-entry CRC to the SF symbol dictionary
glasstiger Jul 10, 2026
6cc1307
Document split-flush and dict-getter contracts
glasstiger Jul 10, 2026
c065ad4
Stop split-flush stranding a deferred prefix
glasstiger Jul 10, 2026
40c6a28
Harden persisted symbol-dict edge paths
glasstiger Jul 10, 2026
94e3b1d
Guard the symbol-dict reopen against a >2GB file
glasstiger Jul 11, 2026
6c6dc40
Latch a terminal on sent-dict mirror overflow
glasstiger Jul 11, 2026
ff2e841
Guard torn-dict resume and skip split re-encode
glasstiger Jul 12, 2026
50fc218
Give the catch-up cap gap a settle budget
glasstiger Jul 12, 2026
7880b7a
Cover defensive dict guards and tidy test helpers
glasstiger Jul 13, 2026
2ad8f50
Fail clean on a total symbol-dict tear on resume
glasstiger Jul 13, 2026
76fe4f1
Bound recovered symbol id to committed frames
glasstiger Jul 13, 2026
084f93f
Null loaded dict entries after a failed truncate
glasstiger Jul 13, 2026
8facb8d
Test the catch-up NACK guard at the ack boundary
glasstiger Jul 13, 2026
031777e
Cover the cap-gap budget reset on success
glasstiger Jul 13, 2026
0c0b9dc
Cover file-mode split write-ahead dict persist
glasstiger Jul 13, 2026
bbc01a8
Cover split-preflight and torn-dict guard gaps
glasstiger Jul 14, 2026
39e9e52
Cover the persisted symbol-dict short-write path
glasstiger Jul 14, 2026
265b703
Harden the symbol-dict fd close and split sizing
glasstiger Jul 14, 2026
2fa5cd9
Recover full-dict SF slots without bricking build
glasstiger Jul 14, 2026
c181e4f
Recover slots whose symbol dictionary is damaged
glasstiger Jul 14, 2026
017652f
Keep the producer alive through a torn symbol dictionary
glasstiger Jul 14, 2026
dd48d43
Assert the new cap-gap config key is honored
glasstiger Jul 14, 2026
521b48d
Close the untested gaps around the symbol-dict guards
glasstiger Jul 14, 2026
aeb92b5
Set aside the slots recovery cannot rescue, instead of failing
glasstiger Jul 14, 2026
ff53e1f
Rebuild the dictionary from the frames still on disk
glasstiger Jul 14, 2026
3b73bc8
Pin the producer's recovery seed on the unopenable-dictionary path
glasstiger Jul 14, 2026
98a4ac0
Don't key the cap-gap episode off nanoTime's sign
glasstiger Jul 14, 2026
63dc52a
Free the recovery mirror on a ctor seed throw
glasstiger Jul 15, 2026
7299925
Correct stale and stranded symbol-dict comments
glasstiger Jul 15, 2026
c0c2ce2
Preserve symbol dict on a transient stat fault
glasstiger Jul 15, 2026
0520dc4
Don't quarantine a fully-acked torn-dict slot
glasstiger Jul 15, 2026
8d6a932
Keep foreground cap-gap retries unbounded
glasstiger Jul 15, 2026
84293fa
Reset cap-gap budget across unrelated outages
glasstiger Jul 15, 2026
fe9546f
Prevent orphan adoption during slot quarantine
glasstiger Jul 15, 2026
6328467
Verify delivery after torn-slot recovery
glasstiger Jul 15, 2026
5617d79
Optimize symbol dictionary recovery and catch-up
glasstiger Jul 15, 2026
f3282a4
Harden delta dictionary test correctness
glasstiger Jul 15, 2026
675d036
Avoid re-encoding split QWP frames
glasstiger Jul 15, 2026
f12c934
Clean up QWP delta dictionary tests
glasstiger Jul 15, 2026
78a1d32
Fix Windows QWP client test failures
glasstiger Jul 16, 2026
12a3aec
Fix QWP store-and-forward recovery safety
glasstiger Jul 16, 2026
350e4a3
Restore QWP API compatibility
glasstiger Jul 16, 2026
a341a9b
Optimize QWP symbol dictionary recovery
glasstiger Jul 16, 2026
5e467f3
fix: harden reconnect and test cleanup handling
glasstiger Jul 16, 2026
994554e
fix: preserve QWP store-and-forward retries
glasstiger Jul 17, 2026
20df0eb
fix: harden QWP recovery cleanup
glasstiger Jul 17, 2026
1596dab
Reduce QWP recovery memory and CRC cost
glasstiger Jul 17, 2026
4386ca1
Optimize QWP replay and dictionary catch-up
glasstiger Jul 17, 2026
111f341
Add QWP edge-case coverage
glasstiger Jul 17, 2026
8a1aaf0
Add QWP failure-path coverage
glasstiger Jul 17, 2026
0428fd8
Clean up QWP recovery scaffolding
glasstiger Jul 17, 2026
9cc550f
Surface startup endpoint-policy failures
glasstiger Jul 17, 2026
68aa31a
Pin the quarantine test on the rename failure
glasstiger Jul 17, 2026
5ded812
Cut symbol-dict overhead and fix two blind guards
glasstiger Jul 17, 2026
b279eec
Re-fold recovery at baseline 0 when discarding the dictionary
glasstiger Jul 17, 2026
ded2edc
Close store-and-forward recovery and drainer gaps
glasstiger Jul 17, 2026
16be764
Fix connectWithRetry budget overflow
glasstiger Jul 17, 2026
042c123
Single-pass symbol-dict append; test window remap
glasstiger Jul 17, 2026
54e21f8
Delete dead recovery symbol-walk methods
glasstiger Jul 17, 2026
0a9c3d6
De-reflect cursor tests; de-flake poison timing
glasstiger Jul 18, 2026
75d89b8
Preserve cap-gap episode; minor test/style nits
glasstiger Jul 18, 2026
0a3010f
Fix Windows separator mismatch in SlotLockTest
glasstiger Jul 18, 2026
1699c0a
Snapshot server batch cap once in sendRow
glasstiger Jul 19, 2026
3314d98
Drop redundant symbol-dict catch-up entry index
glasstiger Jul 19, 2026
2257daa
Drop the send loop's unused reconnect budget field
glasstiger Jul 19, 2026
c1587c9
Reclaim logical slot locks on fully-drained close
glasstiger Jul 20, 2026
47b3c9e
Fix inverted async initial-connect retry docs
glasstiger Jul 20, 2026
ece7817
Skip a segment whose mmap fault arrives late
glasstiger Jul 20, 2026
4c10246
Pin catch-up id tiling and fix stale comments
glasstiger Jul 20, 2026
54cb19d
Share the slot-lock dir and stop false quarantines
glasstiger Jul 20, 2026
d76ef16
Fix Windows path split in orphan-tail drainer test
glasstiger Jul 20, 2026
0fe4873
Close four data-integrity holes in the delta symbol dictionary
glasstiger Jul 20, 2026
66bdc0f
Pin the C3 and C4 fixes with regression tests
glasstiger Jul 20, 2026
3f98352
Close the remaining four correctness holes in SF recovery
glasstiger Jul 20, 2026
509b372
Cut three redundant walks out of recovery and the flush path
glasstiger Jul 20, 2026
52a9473
Speed up recovery lookups and the per-symbol varint decode
glasstiger Jul 20, 2026
2ee6e8f
Close two test-gate holes; report the third as not reproducible
glasstiger Jul 20, 2026
f9bb589
Cover three untested-critical rows; explain the two that resist
glasstiger Jul 20, 2026
ba4d458
Fix six moderate findings in the SF dictionary paths
glasstiger Jul 20, 2026
6d007c7
Close test loops, drop dead walkers, fix misleading comments
glasstiger Jul 20, 2026
172cfe6
Fix SF recovery, lock and quarantine safety bugs
glasstiger Jul 21, 2026
db5dc37
Drop the binary-compatibility shims
Jul 28, 2026
f4f9665
Treat a dictionary gap as retriable
Jul 28, 2026
a225734
Model the gap rejection in the test decoder
Jul 28, 2026
1507e9e
Model post-gap connection silence and cap-shrink gaps
Jul 28, 2026
11333a4
Let an mmap fault reach the dictionary degrade
Jul 28, 2026
01288d7
Bound the catch-up when no cap is advertised
Jul 28, 2026
a5b5cc6
Clear the in-flight holder before rethrowing
Jul 28, 2026
19f7536
Remove the legacy-reader barrier
Jul 28, 2026
b795b4b
Make a shared sf_dir opt-in
Jul 28, 2026
6ca6ad2
Add end-to-end permission test for sf_dir_shared
Jul 28, 2026
4d245ff
Finish close() when the last batch is rejected
Jul 28, 2026
3fe1828
Refuse a fresh slot with an unclearable dictionary
Jul 28, 2026
627507c
Cover the dictionary refusal through a real EISDIR
Jul 28, 2026
7494fee
Separate the words in the cap-gap config key
Jul 28, 2026
25d253f
Fail closed when recovery skips a segment
Jul 28, 2026
6018642
Add the missing all-skipped regression test
Jul 28, 2026
af513a9
Quarantine a slot whose construction itself refuses
Jul 28, 2026
ab974c4
Refuse a skip before the gap it can open, not after
Jul 28, 2026
9fa9067
Carry a generation id in the segment header
Jul 28, 2026
84fa7cd
Guard rotation against a closing ring; test generation propagation
Jul 29, 2026
8c05107
Correct comments describing removed server behaviour
Jul 29, 2026
04f67e8
Fix sticky-bit arithmetic in SfDirPermissionsTest javadoc
Jul 29, 2026
0b5a2c4
Quarantine unreplayable slots; fix test gaps
Jul 29, 2026
8813206
Merge origin/main into qwp-delta-symbol-dict
Jul 29, 2026
45ec2fa
fix(sf): refuse a fresh start when a drained-slot leftover survives r…
Jul 29, 2026
c597421
fix(sf): treat a drained-slot leftover unlink failure as operational,…
Jul 29, 2026
7e327b3
test(sf): narrow the leftover-refusal assertion to its own message
Jul 29, 2026
0959b46
refactor(sf): drop the lineage stamp from the persisted symbol dictio…
Jul 30, 2026
0df1335
test(sf): pin the HEADER_SIZE routing boundary in PersistedSymbolDict…
Jul 30, 2026
5fadc42
test(sf): make the HEADER_SIZE boundary test discriminate load from r…
Jul 30, 2026
b5728a5
refactor(sf): drop the recovered-chain lineage agreement check
Jul 30, 2026
d3bff09
docs(sf): fix UnreplayableSlotException javadoc to match live throw s…
Jul 30, 2026
78cd3b6
refactor(sf): shrink the segment header back to 24 bytes, version 1
Jul 30, 2026
fd2d0f1
fix(sf): update stale header-size comments, sharpen a rename-failure
Jul 30, 2026
724fee8
Remove the sf_dir_shared opt-in
Jul 31, 2026
27b5276
Unlink the pid sidecar before the lock file
Jul 31, 2026
3383f29
Latch hasEverConnected after the catch-up succeeds
Jul 31, 2026
8f8ecf9
fix(qwp): refuse symbol registration past the server dictionary cap
Jul 31, 2026
68960c3
test(qwp): pin the row-API contract at the symbol dictionary cap
Jul 31, 2026
88568f7
Fix SegmentSkipQuarantineTest's racy rotation precondition
Jul 31, 2026
164bb9b
Fix alphabetical ordering and javadoc wrap in QwpConstants
Jul 31, 2026
3c7dcc9
Harden the build()-time quarantine path
Jul 31, 2026
4d8702c
Pin the quarantine SenderError dispatch
Jul 31, 2026
0d50275
Fold sendRow's second column walk into nextRow
Jul 31, 2026
0913496
Name reset() in the batch-too-large recovery hint
Jul 31, 2026
df199dd
Create test dictionaries through openClean, not open
Aug 1, 2026
a16d182
Fail loudly on transient symbol-dict recovery faults
Aug 1, 2026
f5faf1c
Align recovery fixtures with the no-fabrication contract
Aug 1, 2026
425eedc
Pin the fail-loud break of the misattribution chain
Aug 1, 2026
6193d10
Route dict existence checks on stat errno
Aug 1, 2026
141515e
Expose durable side-file bytes from the symbol dictionary
Aug 1, 2026
4af3691
Count side-file bytes against the SF disk cap
Aug 1, 2026
091476a
Fix disk-full warn to not promise a remedy trim can't deliver
Aug 1, 2026
e1ac106
Wire the dict side-file gauge into manager registration
Aug 1, 2026
a8e8752
Harden the cap control test and make the dict gauge wait-free
Aug 1, 2026
8be7670
Rebuild the torn-slot fixture through the manager trim and re-enable …
Aug 1, 2026
8328f21
Re-enable the torn-dict guard test on the manager-path fixture
Aug 1, 2026
71856dc
Add an end-to-end DICTIONARY_GAP NACK recycle-and-replay test
Aug 1, 2026
b1d5524
Cover both branches of the close() drain-timeout outage naming
Aug 1, 2026
0b363eb
Polish test comments, license banner, and explicit sync-connect config
Aug 1, 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
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,7 @@
import io.questdb.client.cutlass.qwp.client.sf.cursor.DefaultSenderConnectionListener;
import io.questdb.client.cutlass.qwp.client.sf.cursor.DefaultSenderErrorHandler;
import io.questdb.client.cutlass.qwp.client.sf.cursor.DefaultSenderProgressHandler;
import io.questdb.client.cutlass.qwp.client.sf.cursor.PersistedSymbolDict;
import io.questdb.client.cutlass.qwp.client.sf.cursor.SenderConnectionDispatcher;
import io.questdb.client.cutlass.qwp.client.sf.cursor.SenderErrorDispatcher;
import io.questdb.client.cutlass.qwp.client.sf.cursor.SenderProgressDispatcher;
Expand Down Expand Up @@ -221,6 +222,14 @@ public class QwpWebSocketSender implements Sender {
private CursorSendEngine cursorEngine;
private CursorWebSocketSendLoop cursorSendLoop;
private boolean deferCommit;
// True when the sender emits incremental (delta) symbol dictionaries: each
// message carries only symbol ids not yet sent on the wire, rather than the
// full dictionary from id 0. Enabled only in memory-mode, where a reconnect
// replays from the in-process ring and the I/O thread re-registers the whole
// dictionary via a catch-up frame before replaying. File-mode store-and-forward
// keeps full self-sufficient frames (recovery/orphan-drain can replay mid-stream
// to a fresh server, where a partial delta would leave gaps). Set in setCursorEngine.
private boolean deltaDictEnabled;
// User-supplied observer for background orphan-slot drainer events.
// Volatile: written by setDrainerListener (any thread, before or after
// startOrphanDrainers) and read at pool-creation time. Null -> drainers
Expand Down Expand Up @@ -313,6 +322,12 @@ public class QwpWebSocketSender implements Sender {
// beginRound(true) call. roundSeq=1 is the first round; CONNECTED in the
// first round indicates the initial connect.
private long roundSeq;
// Highest global symbol id the producer has baked into a frame so far, or -1.
// Lifetime-monotonic in delta mode -- it is NOT reset on reconnect, because
// the I/O thread re-registers the full dictionary via a catch-up frame before
// replaying, so the producer's delta baseline stays valid across the wire
// boundary. Used only when deltaDictEnabled; ignored in full-dict mode.
private int sentMaxSymbolId = -1;
// When true, auto-flush sends messages with FLAG_DEFER_COMMIT and only
// explicit flush() triggers the server-side commit. Enables accumulating
// arbitrarily large datasets that exceed the server's recv buffer.
Expand Down Expand Up @@ -2215,6 +2230,18 @@ public void setCursorEngine(CursorSendEngine engine, boolean takeOwnership) {
}
this.cursorEngine = engine;
this.ownsCursorEngine = takeOwnership && engine != null;
// Delta encoding is available in memory-mode (in-process catch-up) and in
// file-mode when the persisted dictionary opened (recovery / orphan-drain
// rebuild the dictionary from it). Otherwise fall back to full self-
// sufficient frames. See CursorSendEngine.isDeltaDictEnabled.
this.deltaDictEnabled = engine != null && engine.isDeltaDictEnabled();
// Recovery: repopulate the producer's global dictionary from the slot's
// persisted dictionary so newly ingested symbols continue from the
// recovered ids (rather than colliding with them at 0), and the delta
// baseline resumes where the crashed session left off.
if (deltaDictEnabled && engine.wasRecoveredFromDisk()) {
seedGlobalDictionaryFromPersisted(engine.getPersistedSymbolDict());
}
}

/**
Expand Down Expand Up @@ -3423,14 +3450,15 @@ private void flushPendingRows(boolean deferCommit) {
}

ensureActiveBufferReady();
// Cursor SF requires every on-disk frame to be self-sufficient:
// recorded frames replay to fresh server connections (orphan-slot
// drainers and post-reconnect replay), so always emit the full
// symbol-dict delta from id=0 and the full column schema inline,
// never a back-reference the target server may not have seen.
// In full-dict mode every frame is self-sufficient: it carries the whole
// symbol dictionary from id 0 so orphan-drain / recovery replay to a fresh
// server never dangles a symbol id. In delta mode (memory-mode only) each
// frame carries only ids above sentMaxSymbolId; a reconnect re-registers
// the dictionary via an I/O-thread catch-up frame before replay, so the
// producer's monotonic baseline stays valid across the wire boundary.
encoder.setDeferCommit(deferCommit);
encoder.beginMessage(tableCount, globalSymbolDictionary,
/*confirmedMaxId=*/ -1, currentBatchMaxSymbolId);
symbolDeltaBaseline(), currentBatchMaxSymbolId);
for (int i = 0, n = keys.size(); i < n; i++) {
CharSequence tableName = keys.getQuick(i);
if (tableName == null) {
Expand All @@ -3456,10 +3484,18 @@ private void flushPendingRows(boolean deferCommit) {
return;
}

// Write-ahead: durably persist this frame's new symbols BEFORE it is
// published, so a recovered/orphan-drained slot can always rebuild the
// dictionary the (non-self-sufficient) delta frame references. No-op in
// memory mode and when the frame introduces no new symbols.
persistNewSymbolsBeforePublish();
activeBuffer.ensureCapacity(messageSize);
activeBuffer.write(buffer.getBufferPtr(), messageSize);
activeBuffer.incrementRowCount();
sealAndSwapBuffer();
// The frame carrying ids up to currentBatchMaxSymbolId is now on the ring;
// advance the delta baseline so the next frame ships only newer ids.
advanceSentMaxSymbolId();

hasDeferredMessages = deferCommit;
if (!deferCommit) {
Expand Down Expand Up @@ -3514,8 +3550,12 @@ private void flushPendingRowsSplit(ObjList<CharSequence> keys, boolean deferComm
boolean deferThis = deferCommit || !isLast;

encoder.setDeferCommit(deferThis);
// Each split frame emits the delta above sentMaxSymbolId; the first
// frame ships the whole batch's new ids and advances the baseline, so
// the remaining frames carry an empty delta and just reference ids the
// first frame already registered.
encoder.beginMessage(1, globalSymbolDictionary,
/*confirmedMaxId=*/ -1, currentBatchMaxSymbolId);
symbolDeltaBaseline(), currentBatchMaxSymbolId);
encoder.addTable(tableBuffer);
int messageSize = encoder.finishMessage();
QwpBufferWriter buffer = encoder.getBuffer();
Expand All @@ -3528,11 +3568,18 @@ private void flushPendingRowsSplit(ObjList<CharSequence> keys, boolean deferComm
.put(", serverMaxBatchSize=").put(serverMaxBatchSize).put(']');
}

// Write-ahead persist before publish (see flushPendingRows). The
// first split frame carries the batch's new symbols; the rest are
// no-ops once the baseline has advanced past them.
persistNewSymbolsBeforePublish();
ensureActiveBufferReady();
activeBuffer.ensureCapacity(messageSize);
activeBuffer.write(buffer.getBufferPtr(), messageSize);
activeBuffer.incrementRowCount();
sealAndSwapBuffer();
// Frame queued: advance so the next split frame's delta starts above
// the ids this one just registered.
advanceSentMaxSymbolId();
}

encoder.setDeferCommit(false);
Expand Down Expand Up @@ -3573,8 +3620,10 @@ private void sendCommitMessage() {
LOG.debug("Sending commit message for deferred batch");
}
encoder.setDeferCommit(false);
// A commit carries no rows and no new symbols; in delta mode its empty
// delta simply starts at the server's current dictionary size.
encoder.beginMessage(0, globalSymbolDictionary,
/*confirmedMaxId=*/ -1, currentBatchMaxSymbolId);
symbolDeltaBaseline(), currentBatchMaxSymbolId);
int messageSize = encoder.finishMessage();
QwpBufferWriter buffer = encoder.getBuffer();
ensureActiveBufferReady();
Expand All @@ -3586,15 +3635,84 @@ private void sendCommitMessage() {
lastCommitBoundaryFsn = cursorEngine.publishedFsn();
}

/**
* Advances the delta baseline once a frame carrying the current batch's
* symbols has been queued onto the ring. No-op in full-dict mode. Only ever
* moves the baseline forward, so a batch that used no new symbols leaves it
* unchanged.
*/
private void advanceSentMaxSymbolId() {
if (deltaDictEnabled && currentBatchMaxSymbolId > sentMaxSymbolId) {
sentMaxSymbolId = currentBatchMaxSymbolId;
}
}

/**
* Appends the symbols this frame introduces ({@code [sentMaxSymbolId+1 ..
* currentBatchMaxSymbolId]}) to the slot's persisted dictionary BEFORE the
* frame is published to the ring. This write-ahead ordering keeps the
* persisted dictionary a superset of every process-crash-recoverable frame's
* references, so recovery and orphan-drain can re-register it on a fresh
* server. Not fsync'd (see PersistedSymbolDict) -- a host crash that tears it
* is caught by the send loop's replay guard. No-op in memory mode (no
* persisted dictionary) and when the frame introduces no new symbols.
*/
private void persistNewSymbolsBeforePublish() {
if (!deltaDictEnabled || cursorEngine == null) {
return;
}
PersistedSymbolDict pd = cursorEngine.getPersistedSymbolDict();
if (pd == null) {
return;
}
int from = sentMaxSymbolId + 1;
int to = currentBatchMaxSymbolId;
if (to < from) {
return;
}
for (int id = from; id <= to; id++) {
pd.appendSymbol(globalSymbolDictionary.getSymbol(id));
}
}

private void resetSymbolDictStateForNewConnection() {
// The new server has an empty symbol dictionary, so the next batch
// must ship a delta starting at id 0. beginMessage() always passes
// confirmedMaxId = -1; resetting the batch watermark here keeps a
// stale value from suppressing re-emission of symbol ids the new
// server has never seen.
// Runs on the foreground (initial) connect only -- NOT on the I/O thread's
// reconnect/failover path. The per-batch watermark is drained state, so
// clearing it here is harmless. sentMaxSymbolId is deliberately left
// untouched: in delta mode the I/O thread re-registers the whole
// dictionary with a catch-up frame on reconnect, so the producer's
// monotonic baseline must survive the wire boundary; resetting it would
// desync the producer from the I/O thread's sent-dictionary count.
currentBatchMaxSymbolId = -1;
}

/**
* On recovery, repopulates the producer's {@link GlobalSymbolDictionary} from
* the slot's persisted dictionary (ids assigned in the same ascending order,
* so they match the recovered frames) and resumes the delta baseline at the
* recovered tip, so newly ingested symbols continue above the recovered ids.
*/
private void seedGlobalDictionaryFromPersisted(PersistedSymbolDict pd) {
if (pd == null || pd.size() == 0) {
return;
}
ObjList<String> symbols = pd.readLoadedSymbols();
for (int i = 0, n = symbols.size(); i < n; i++) {
globalSymbolDictionary.getOrAddSymbol(symbols.getQuick(i));
}
sentMaxSymbolId = globalSymbolDictionary.size() - 1;
}

/**
* The symbol id below which the server already holds every dictionary entry,
* used as {@code confirmedMaxId} when encoding a frame. In delta mode this is
* the producer's monotonic sent watermark; in full-dict mode it is -1 so every
* frame re-ships the dictionary from id 0.
*/
private int symbolDeltaBaseline() {
return deltaDictEnabled ? sentMaxSymbolId : -1;
}

private void rollbackRow() {
if (currentTableBuffer != null) {
currentTableBuffer.cancelCurrentRow();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -110,6 +110,13 @@ public final class CursorSendEngine implements QuietCloseable {
// in the constructor, closed by {@link #close()}. The segment manager
// writes through this on every tick where ackedFsn has advanced.
private final AckWatermark watermark;
// Engine-owned per-slot symbol dictionary file (disk mode only; {@code null}
// in memory mode and if open() failed). Enables delta-encoded SF frames:
// recovery / orphan-drain load it to re-register the dictionary on the fresh
// server before replaying non-self-sufficient frames. Opened in the
// constructor, closed by {@link #close()}. When null in disk mode the engine
// reports delta encoding as unavailable and the sender keeps full-dict frames.
private final PersistedSymbolDict persistedSymbolDict;
// close() is publicly callable from any thread (Sender.close from a user
// thread, JVM shutdown hooks, test cleanup). volatile + synchronized
// close() makes the check-and-set atomic and gives readers a fence.
Expand Down Expand Up @@ -199,6 +206,7 @@ private CursorSendEngine(String sfDir, long segmentSizeBytes, SegmentManager man
// reference instead of orphaning the mmap'd segments + fds.
SegmentRing ringInProgress = null;
AckWatermark watermarkInProgress = null;
PersistedSymbolDict persistedDictInProgress = null;
try {
// Disk mode: try to recover any *.sfa files left behind by a prior
// session before deciding to start fresh. Without this the engine
Expand Down Expand Up @@ -257,6 +265,10 @@ private CursorSendEngine(String sfDir, long segmentSizeBytes, SegmentManager man
// mmap doesn't take down the engine -- we just fall
// back to the bare lowestBase - 1 seed.
watermarkInProgress = AckWatermark.open(sfDir);
// Load the persisted symbol dictionary so delta-encoded frames
// in this recovered slot can be re-registered on the fresh
// server before replay. Null on open failure -> delta disabled.
persistedDictInProgress = PersistedSymbolDict.open(sfDir);
long baseSeed = lowestBase - 1;
long watermarkFsn = watermarkInProgress != null
? watermarkInProgress.read()
Expand Down Expand Up @@ -309,6 +321,10 @@ private CursorSendEngine(String sfDir, long segmentSizeBytes, SegmentManager man
if (!memoryMode) {
AckWatermark.removeOrphan(sfDir);
watermarkInProgress = AckWatermark.open(sfDir);
// Same stale-side-file hygiene for the symbol dictionary: a
// fresh slot starts with an empty dictionary.
PersistedSymbolDict.removeOrphan(sfDir);
persistedDictInProgress = PersistedSymbolDict.open(sfDir);
}
MmapSegment initial;
String initialPath = null;
Expand All @@ -333,10 +349,11 @@ private CursorSendEngine(String sfDir, long segmentSizeBytes, SegmentManager man
manager.start();
}
manager.register(ringInProgress, sfDir, watermarkInProgress);
// All construction succeeded — commit the ring and
// watermark references.
// All construction succeeded — commit the ring, watermark and
// symbol-dictionary references.
this.ring = ringInProgress;
this.watermark = watermarkInProgress;
this.persistedSymbolDict = persistedDictInProgress;
} catch (Throwable t) {
// Stop an owned manager before freeing the ring and watermark it may
// touch, then release the slot lock. Each cleanup is in its own
Expand All @@ -362,6 +379,12 @@ private CursorSendEngine(String sfDir, long segmentSizeBytes, SegmentManager man
} catch (Throwable ignored) {
}
}
if (persistedDictInProgress != null) {
try {
persistedDictInProgress.close();
} catch (Throwable ignored) {
}
}
if (acquiredLock != null) {
try {
acquiredLock.close();
Expand Down Expand Up @@ -541,6 +564,12 @@ public synchronized void close() {
} catch (Throwable ignored) {
}
}
if (persistedSymbolDict != null) {
try {
persistedSymbolDict.close();
} catch (Throwable ignored) {
}
}
if (fullyDrained) {
try {
unlinkAllSegmentFiles(sfDir);
Expand All @@ -550,6 +579,11 @@ public synchronized void close() {
AckWatermark.removeOrphan(sfDir);
} catch (Throwable ignored) {
}
try {
// Slot fully drained: the dictionary has no frames behind it.
PersistedSymbolDict.removeOrphan(sfDir);
} catch (Throwable ignored) {
}
}
} finally {
if (slotLock != null) {
Expand Down Expand Up @@ -586,6 +620,38 @@ public long getTotalBackpressureStalls() {
return backpressureStallCount.get();
}

/**
* True when this engine has no store-and-forward directory: the ring lives
* only in malloc'd memory, so it cannot be recovered after a crash and no
* orphan drainer ever replays it. Only in-process reconnect/failover replays
* its frames, which is what makes send-time symbol-dict catch-up (rather than
* fully self-sufficient frames) safe.
*/
public boolean isMemoryMode() {
return sfDir == null;
}

/**
* Whether the sender may delta-encode symbol dictionaries on this engine.
* Always true in memory mode (the send loop keeps an in-process catch-up
* mirror). In disk mode it requires the persisted dictionary to have opened,
* since delta frames are not self-sufficient and recovery / orphan-drain must
* be able to rebuild the dictionary from disk. When false in disk mode the
* sender falls back to full self-sufficient frames.
*/
public boolean isDeltaDictEnabled() {
return sfDir == null || persistedSymbolDict != null;
}

/**
* The engine's persisted symbol dictionary, or {@code null} in memory mode
* (and in disk mode if it failed to open). The producer appends new symbols
* to it; recovery / orphan-drain read its loaded entries to seed catch-up.
*/
public PersistedSymbolDict getPersistedSymbolDict() {
return persistedSymbolDict;
}

/**
* Pass-through to {@link SegmentRing#nextSealedAfter(MmapSegment)}.
*/
Expand Down
Loading
Loading