@@ -172,9 +172,9 @@ public class QwpWebSocketSender implements Sender {
172172 private final ReentrantLock connectWalkLock = new ReentrantLock ();
173173 private final QwpHostHealthTracker hostTracker ;
174174 // Per-table encoded body byte counts captured during flushPendingRows' combined
175- // encode, reused by flushPendingRowsSplit to size each split frame arithmetically
176- // instead of re- encoding the batch a second time. Cleared and repopulated on every
177- // flush; only consumed on the (exceptional) split path. Reused to stay zero-GC.
175+ // encode. flushPendingRowsSplit uses them both for preflight sizing and to walk
176+ // the staged body slices without encoding the batch a second time. Cleared and
177+ // repopulated on every flush; reused to stay zero-GC.
178178 private final IntList splitFrameBodyBytes = new IntList ();
179179 private final CharSequenceObjHashMap <QwpTableBuffer > tableBuffers ;
180180 // null means plain text (no TLS)
@@ -3497,15 +3497,13 @@ private void flushPendingRows(boolean deferCommit) {
34973497 encoder .setDeferCommit (deferCommit );
34983498 encoder .beginMessage (tableCount , globalSymbolDictionary ,
34993499 symbolDeltaBaseline (), currentBatchMaxSymbolId );
3500- // Record each table's encoded body size (position delta across addTable) so
3501- // the split path can size its per-table frames arithmetically rather than
3502- // re-encoding the whole batch. Body encoding is context-free (a table's bytes
3503- // don't depend on its siblings or the delta section), so the size a table
3504- // takes here equals the size it takes in its own split frame. Only consumed
3505- // when the batch overflows the cap; the capture is a couple of int ops per
3506- // table on the common path.
3500+ // Record each table's encoded body size (position delta across addTable).
3501+ // When the batch needs splitting, these lengths delimit immutable body
3502+ // slices in the combined encoder buffer for direct frame assembly. The
3503+ // capture is a couple of int ops per table on the common path.
35073504 splitFrameBodyBytes .clear ();
3508- int bodyStart = encoder .getBuffer ().getPosition ();
3505+ int combinedBodyStart = encoder .getBuffer ().getPosition ();
3506+ int bodyStart = combinedBodyStart ;
35093507 for (int i = 0 , n = keys .size (); i < n ; i ++) {
35103508 CharSequence tableName = keys .getQuick (i );
35113509 if (tableName == null ) {
@@ -3538,10 +3536,9 @@ private void flushPendingRows(boolean deferCommit) {
35383536 // the next flush picks up the new cap.
35393537 int cap = serverMaxBatchSize ;
35403538 if (cap > 0 && messageSize > cap ) {
3541- // The combined frame's delta-entry bytes are byte-identical to the first
3542- // split frame's (same baseline + batch max), so capture the length now
3543- // for the arithmetic frame-sizing in flushPendingRowsSplit.
3544- flushPendingRowsSplit (keys , deferCommit , encoder .getDeltaEntriesLen (), cap );
3539+ // Keep the completed combined frame staged in the encoder while the
3540+ // split path copies its delta entries and table-body slices.
3541+ flushPendingRowsSplit (keys , deferCommit , combinedBodyStart , cap );
35453542 return ;
35463543 }
35473544
@@ -3593,7 +3590,12 @@ private void flushPendingRows(boolean deferCommit) {
35933590 * carry FLAG_DEFER_COMMIT. When false, only the
35943591 * last message omits the flag.
35953592 */
3596- private void flushPendingRowsSplit (ObjList <CharSequence > keys , boolean deferCommit , int combinedDeltaEntriesLen , int cap ) {
3593+ private void flushPendingRowsSplit (
3594+ ObjList <CharSequence > keys ,
3595+ boolean deferCommit ,
3596+ int combinedBodyStart ,
3597+ int cap
3598+ ) {
35973599 if (LOG .isDebugEnabled ()) {
35983600 LOG .debug ("Splitting flush across multiple messages [serverMaxBatchSize={}, defer={}]" , cap , deferCommit );
35993601 }
@@ -3609,14 +3611,10 @@ private void flushPendingRowsSplit(ObjList<CharSequence> keys, boolean deferComm
36093611 // front makes the split all-or-nothing: either every frame fits and all
36103612 // publish, or none publish and we throw with nothing stranded.
36113613 //
3612- // Each split frame's size is derived ARITHMETICALLY from the combined encode
3613- // flushPendingRows already performed -- header + the two delta-section varints
3614- // + the delta entries (byte-identical to the combined frame's when this frame
3615- // carries them) + the table's own body bytes (captured in splitFrameBodyBytes,
3616- // context-free so identical here and in its solo frame) -- rather than
3617- // re-encoding every table a second time. simBaseline mirrors the publish loop's
3618- // baseline advance (advanceSentMaxSymbolId), so each size equals the frame the
3619- // publish loop will build; this pass mutates no delta/persist state.
3614+ // Each split frame's size is derived from the combined encode flushPendingRows
3615+ // already performed. simBaseline mirrors the publish loop's baseline advance
3616+ // (advanceSentMaxSymbolId), so each size equals the frame the staged-slice
3617+ // assembler will build; this pass mutates no delta/persist state.
36203618 int nonEmptyCount = 0 ;
36213619 int simBaseline = symbolDeltaBaseline ();
36223620 int bodyIdx = 0 ;
@@ -3630,13 +3628,8 @@ private void flushPendingRowsSplit(ObjList<CharSequence> keys, boolean deferComm
36303628 continue ;
36313629 }
36323630 nonEmptyCount ++;
3633- int deltaStart = simBaseline + 1 ;
3634- int deltaCount = Math .max (0 , currentBatchMaxSymbolId - simBaseline );
3635- int messageSize = QwpConstants .HEADER_SIZE
3636- + NativeBufferWriter .varintSize (deltaStart )
3637- + NativeBufferWriter .varintSize (deltaCount )
3638- + (deltaCount > 0 ? combinedDeltaEntriesLen : 0 )
3639- + splitFrameBodyBytes .getQuick (bodyIdx );
3631+ int messageSize = encoder .getSplitMessageSize (
3632+ splitFrameBodyBytes .getQuick (bodyIdx ), simBaseline , currentBatchMaxSymbolId );
36403633 bodyIdx ++;
36413634 if (messageSize > cap ) {
36423635 resetTableBuffersAfterFlush (keys );
@@ -3653,6 +3646,8 @@ private void flushPendingRowsSplit(ObjList<CharSequence> keys, boolean deferComm
36533646 }
36543647
36553648 int sent = 0 ;
3649+ bodyIdx = 0 ;
3650+ int tableBodyOffset = combinedBodyStart ;
36563651 for (int i = 0 , n = keys .size (); i < n ; i ++) {
36573652 CharSequence tableName = keys .getQuick (i );
36583653 if (tableName == null ) {
@@ -3667,35 +3662,38 @@ private void flushPendingRowsSplit(ObjList<CharSequence> keys, boolean deferComm
36673662 boolean isLast = (sent == nonEmptyCount );
36683663 boolean deferThis = deferCommit || !isLast ;
36693664
3670- encoder .setDeferCommit (deferThis );
3671- // Each split frame emits the delta above sentMaxSymbolId; the first
3672- // frame ships the whole batch's new ids and advances the baseline, so
3673- // the remaining frames carry an empty delta and just reference ids the
3674- // first frame already registered.
3675- encoder .beginMessage (1 , globalSymbolDictionary ,
3676- symbolDeltaBaseline (), currentBatchMaxSymbolId );
3677- encoder .addTable (tableBuffer );
3678- int messageSize = encoder .finishMessage ();
3679- QwpBufferWriter buffer = encoder .getBuffer ();
3665+ int tableBodyLength = splitFrameBodyBytes .getQuick (bodyIdx ++);
3666+ // Persist before touching activeBuffer. If the write-ahead fails, the
3667+ // caller can retry with both the source rows and active microbatch
3668+ // unchanged. The first frame carries the batch's new symbols; later
3669+ // frames are no-ops once the baseline has advanced.
3670+ persistNewSymbolsBeforePublish ();
3671+ ensureActiveBufferReady ();
3672+ // The combined encoder buffer remains immutable for the whole split.
3673+ // Assemble this frame directly into the active microbatch: patched
3674+ // header + staged delta bytes + the staged table-body slice. No row or
3675+ // column is encoded a second time.
3676+ int messageSize = encoder .copySplitMessage (
3677+ activeBuffer ,
3678+ tableBodyOffset ,
3679+ tableBodyLength ,
3680+ deferThis ,
3681+ symbolDeltaBaseline (),
3682+ currentBatchMaxSymbolId
3683+ );
3684+ tableBodyOffset += tableBodyLength ;
36803685 // The pre-flight pass above already verified every split frame fits the
36813686 // cap, so none can be found oversized here -- which is what keeps this
36823687 // loop from publishing (and stranding) a deferred prefix before an
36833688 // oversized table. Both passes size against the SAME snapshot cap, so a
36843689 // mid-flush failover cannot make them disagree; the assert therefore only
36853690 // catches a genuine divergence between the pre-flight arithmetic and the
3686- // real encode (a future bug), not a cap race. It deliberately does NOT
3691+ // staged assembler (a future bug), not a cap race. It deliberately does NOT
36873692 // reset+throw here, because by this point a prefix may already be on the ring.
36883693 assert messageSize <= cap
36893694 : "split frame exceeded serverMaxBatchSize after pre-flight [table=" + tableName
36903695 + ", messageSize=" + messageSize + ", serverMaxBatchSize=" + cap + ']' ;
36913696
3692- // Write-ahead persist before publish (see flushPendingRows). The
3693- // first split frame carries the batch's new symbols; the rest are
3694- // no-ops once the baseline has advanced past them.
3695- persistNewSymbolsBeforePublish ();
3696- ensureActiveBufferReady ();
3697- activeBuffer .ensureCapacity (messageSize );
3698- activeBuffer .write (buffer .getBufferPtr (), messageSize );
36993697 activeBuffer .incrementRowCount ();
37003698 sealAndSwapBuffer ();
37013699 // Frame queued: advance so the next split frame's delta starts above
0 commit comments