Skip to content

feat(qwp): add deferred commit for transactional ingestion#7144

Merged
bluestreak01 merged 15 commits into
masterfrom
vi_fix_send
May 26, 2026
Merged

feat(qwp): add deferred commit for transactional ingestion#7144
bluestreak01 merged 15 commits into
masterfrom
vi_fix_send

Conversation

@bluestreak01
Copy link
Copy Markdown
Member

@bluestreak01 bluestreak01 commented May 23, 2026

Fixes #7143

Tandem with questdb/java-questdb-client#32.

Summary

Adds FLAG_DEFER_COMMIT (0x01) to the QWP ingress protocol. When a
client sets this flag on a message, the server appends rows to WAL
writers but skips the commit. The commit fires on the next message
without the flag. This allows clients to split a logical batch across
multiple messages that individually fit within the server's recv buffer,
removing the hard coupling between http.recv.buffer.size and maximum
transaction size.

Server changes

  • QwpConstants: add FLAG_DEFER_COMMIT = 0x01.
  • QwpIngressProcessorState: add isDeferCommit() to read the flag
    from the message header, and clearMessageState() to reset
    per-message parsing state without rolling back WAL rows.
  • QwpIngressUpgradeProcessor.handleBinaryMessage(): skip commit()
    when FLAG_DEFER_COMMIT is set; call clearMessageState() instead
    of clear() in the finally block so accumulated WAL rows survive.
    On error, the full clear() still fires to roll back all deferred
    rows.

Client changes (java-questdb-client submodule)

  • QwpConstants: add FLAG_DEFER_COMMIT = 0x01.
  • QwpWebSocketEncoder: add setDeferCommit() to set/clear the flag.
  • QwpBufferWriter / all implementations: add patchByte().
  • QwpWebSocketSender.flushPendingRows(boolean deferCommit): when
    deferCommit is true, the encoded message carries FLAG_DEFER_COMMIT.
    When the batch exceeds serverMaxBatchSize, flushPendingRowsSplit()
    sends each table as a separate deferred message with the last one
    committing (or deferring, if the caller requested defer).
  • QwpWebSocketSender.sendCommitMessage(): sends an empty QWP message
    (0 table blocks, no defer flag) to trigger the server-side commit
    when all pending rows were already sent via deferred auto-flushes.
  • Sender builder: add transaction=on|off connect-string key and
    transactional(boolean) builder method. When enabled, auto-flush
    sends with FLAG_DEFER_COMMIT and only explicit flush() triggers
    the commit.

Protocol spec update

  • Updated the flags byte table (bit 0 is now FLAG_DEFER_COMMIT).
  • Added "Deferred commit" section covering wire semantics, error
    handling, backward compatibility, and implementation recommendations.
  • Updated OK response docs to mention tableCount=0 for deferred
    messages.

Benchmark

  • Added QwpEquitiesL1Benchmark: 10M rows of equities L1 market data
    (500 symbols, 15 exchanges, bid/ask/last prices, volumes) ingested
    with transaction=on — all rows stream via deferred auto-flushes and
    commit once at the end.

Backward compatibility

Old servers ignore the unknown flag bit (it occupies a previously
reserved position). Each message commits independently — more commits
than intended, but no data loss. The feature degrades gracefully.

Error handling

  • Parse error or schema mismatch on a deferred message: the server
    calls clear(), rolling back all accumulated WAL rows from the
    entire deferred group. The client must re-send the full sequence.
  • Client disconnect mid-sequence: close() frees tudCache, rolling
    back uncommitted WAL rows. The client's store-and-forward replays
    all unacknowledged messages on reconnect.

Test plan

  • cd java-questdb-client && mvn -pl core test -- full client suite
    passes
  • mvn -pl core compile -DskipTests -- server compiles
  • Full server test suite (CI)
  • E2E: run QwpEquitiesL1Benchmark against a local server, verify
    10M rows committed atomically on flush()

Pulls in java-questdb-client #32 (Fix microbatch buffer stuck after
cursor append failure) and updates every questdb.client.version
property in the build to 1.3.1-SNAPSHOT so the parent build, the
local-client profile, and downstream artifact metadata all reference
the same release that carries the fix.

Without the bump the parent would keep linking against the published
1.3.0 jar and the QWP WebSocket sender would still leave its
microbatch buffer stuck in SENDING after a synchronous
CursorSendEngine.appendBlocking() failure, surfacing as a 30 s
"Timeout waiting for buffer to be recycled" on the next flush.

Fixes #7143

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@bluestreak01 bluestreak01 added the Bug Incorrect or unexpected behavior label May 23, 2026
@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented May 23, 2026

Important

Review skipped

Auto reviews are disabled on this repository. Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 47cfa77a-2ff6-4ff8-8ffe-d9f11437b219

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch vi_fix_send

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@bluestreak01 bluestreak01 added ILP Issues or changes relating to Influx Line Protocol Java Improvements that update Java code labels May 23, 2026
bluestreak01 and others added 2 commits May 24, 2026 00:30
Picks up submodule commit ae09d47 "Wake segment-manager worker on
register", which closes a pre-existing race in SegmentManager:
register-after-start could leave the worker parked for the full
poll interval when the worker took `lock` first. Unrelated to the
microbatch-buffer fix this branch is built around -- the race
surfaced on this PR's CI run only because of mac-other + JaCoCo
scheduling jitter, but it has been latent in SF code since
commit edbdc3a (feat(ilp): QWiP store-and-forward client buffer).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@bluestreak01 bluestreak01 changed the title fix(qwp): release microbatch buffer when cursor append fails fix(qwp): fix ingestion stalls after append failures and on sender startup May 24, 2026
@bluestreak01 bluestreak01 changed the title fix(qwp): fix ingestion stalls after append failures and on sender startup feat(qwp): add deferred commit for transactional ingestion May 25, 2026
bluestreak01 and others added 6 commits May 26, 2026 15:35
Large uncommitted WAL transactions from deferred QWP batches
can cause memory pressure when the WAL apply job sorts them.
Add a configurable per-table limit (qwp.max.uncommitted.rows,
default 1M) that forces a mid-batch commit when exceeded.
The existing cumulative ACK protocol handles this transparently.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Add three tests covering gaps in deferred-commit E2E coverage:

- testDeferredCommitMultipleCycles: two complete defer-commit
  cycles on the same connection, verifying state is clean
  between cycles
- testDeferredCommitSchemaMismatchRollsBack: schema mismatch
  mid-deferred-sequence rolls back all accumulated WAL rows
  across tables, not just the current message
- testDeferredCommitSymbolDictContinuity: SYMBOL columns with
  delta dictionary encoding work correctly across deferred
  message boundaries

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Points to 081d3ae which re-adds the public
setDeferCommit API needed by QwpSenderE2ETest.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Add the new qwp.max.uncommitted.rows property to the expected
output in testShowParameters. Bump the client submodule to pick
up the close() fix that respects the deferCommit flag, which
fixes testDeferredCommitConnectionDropRollsBack.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@mtopolnik
Copy link
Copy Markdown
Contributor

[PR Coverage check]

😍 pass : 49 / 64 (76.56%)

file detail

path covered line new line coverage
🔵 io/questdb/cutlass/qwp/server/QwpUdpReceiver.java 0 1 00.00%
🔵 io/questdb/cutlass/qwp/server/QwpTudCache.java 13 25 52.00%
🔵 io/questdb/PropServerConfiguration.java 3 5 60.00%
🔵 io/questdb/cutlass/qwp/server/QwpIngressProcessorState.java 18 18 100.00%
🔵 io/questdb/cutlass/qwp/server/QwpIngressUpgradeProcessor.java 13 13 100.00%
🔵 io/questdb/PropertyKey.java 1 1 100.00%
🔵 io/questdb/cutlass/http/DefaultHttpServerConfiguration.java 1 1 100.00%

@bluestreak01 bluestreak01 merged commit bceeedc into master May 26, 2026
51 checks passed
@bluestreak01 bluestreak01 deleted the vi_fix_send branch May 26, 2026 22:58
nwoolmer added a commit that referenced this pull request May 27, 2026
The previous run failed only on mac-other with
QwpUdpMalformedTest.testTooShortDatagram saying
"table does not exist [table=short_test]". The test passed 44/44
locally on linux and passed in PR 7148's mac-other run after the
same #7144 deferred-commit change was already in master. The
QwpUdpReceiver.close() does call tudCache.commitAllBestEffort()
before returning, and the test then drainWalQueue() before the
assertion, so the failure looks like a mac-only timing flake.
Retrigger CI to confirm before chasing the upstream test as a
real regression.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Bug Incorrect or unexpected behavior ILP Issues or changes relating to Influx Line Protocol Java Improvements that update Java code

Projects

None yet

Development

Successfully merging this pull request may close these issues.

QWP WS: cursor append failure leaves microbatch buffer stuck in SENDING

2 participants