Skip to content

feat(core): add local durable acknowledgements to the QWP sender - #98

Open
eugenels wants to merge 4 commits into
mainfrom
nw_adaptive_commit
Open

feat(core): add local durable acknowledgements to the QWP sender#98
eugenels wants to merge 4 commits into
mainfrom
nw_adaptive_commit

Conversation

@eugenels

@eugenels eugenels commented Sep 11, 2026

Copy link
Copy Markdown

The sender can request durable acknowledgements by tier instead of a single
boolean. request_durable_ack accepts on, off, local, replicated and
local,replicated.

  • local: the server emits STATUS_LOCAL_DURABLE_ACK (0x0E) frames once
    commits are fdatasync-durable on its disk. The sender trims its
    store-and-forward copy on them. Local durability survives power loss, not
    the loss of the server's disk.
  • replicated: unchanged shipped behaviour — the server emits
    STATUS_DURABLE_ACK frames once commits reach the object store, and the
    sender trims on them.
  • local,replicated: both streams; the sender trims on the replicated ack
    (the strongest requested guarantee) and receives local acks as early
    progress signals, exposed per table via
    CursorWebSocketSendLoop.getLocalDurableTableWatermark.

on keeps its shipped meaning as the legacy alias for the replicated tier: it
still travels as the literal true header value and expects the historical
enabled confirmation, so nothing changes on the wire for existing
configurations.

The grant is all-or-nothing. The server echoes the granted set verbatim (or
enabled for a legacy request); a partial or missing echo counts as a denial
and fails the connect with the existing mismatch error. This is a behaviour
change worth noting: requesting a tier the server cannot serve now fails
loudly at connect instead of silently proceeding without durable acks —
including explicit tokens sent to servers that predate them.

Binary compatibility is preserved: the nine public
QwpWebSocketSender.connect overloads that took the boolean flag remain as
delegating overloads (verified by ExportedApiCompatibilityTest), and
Sender.LineSenderBuilder.requestDurableAck(boolean) is unchanged. A new
requestDurableAck(CharSequence) overload accepts the tier tokens.

Server support for the local tier lands via questdb/questdb#7411 (branch
nw_adaptive_commit), which carries end-to-end tests driving this client —
including power-loss crash proofs that the local ack never covers a
transaction the server's disk does not hold. The Enterprise tandem of that
branch is questdb/questdb-enterprise#1147; composing the local and
replicated tiers on an Enterprise server is follow-up work there.

Test plan:

  • unit coverage for the tier parsing, header/echo round-trip and trim
    routing (CursorWebSocketSendLoopDurableAckTest and siblings)
  • WsSenderConfigHonoredTest covers the new config values
  • ExportedApiCompatibilityTest verifies the restored boolean overloads
  • full client suite green (3456 tests)
  • server-side integration and crash tests exercise this client from the
    questdb/questdb branch

The sender can now request durable acks by tier set instead of a single
boolean. request_durable_ack accepts on, off, local, replicated and
local,replicated; "on" keeps its shipped meaning as the legacy alias
for the replicated tier and still travels as the literal "true" header
value with the historical "enabled" confirmation, so nothing changes on
the wire for existing configurations.

DurableAckTiers encodes the set as a bitmask and owns the header token
and confirmation-token mapping. WebSocketClient sends the requested
token and validates that the server echoes the granted set verbatim --
the grant is all-or-nothing, a partial or foreign echo counts as a
denial and fails the connect with the existing mismatch error.

WebSocketResponse recognizes the new STATUS_LOCAL_DURABLE_ACK (0x0E)
frame, which shares the STATUS_DURABLE_ACK payload layout. The send
loop trims its store-and-forward copy on the strongest requested
tier's ack: local-only requests trim on the local ack through the same
watermark and drain path; requests including the replicated tier keep
trimming on STATUS_DURABLE_ACK and record local acks as progress
signals in a separate watermark map.

The loop exposes getTotalLocalDurableAcks() and the sender exposes its
send loop to tests so server-side integration tests can assert the ack
and trim counters end to end.
ExportedApiCompatibilityTest rightly rejected retyping the public
QwpWebSocketSender.connect overloads' durable-ack parameter from
boolean to the DurableAckTiers bitmask: callers compiled against
earlier releases would break with NoSuchMethodError. The nine
pre-branch boolean signatures return as delegating overloads that map
true to the legacy "true" request (replicated tier, "enabled"
confirmation) -- the shipped meaning of the opt-in.

WsSenderConfigHonoredTest now expects the config-token string the
snapshot reports ("on", "local", "replicated", "local,replicated")
and covers the new values. The JVM-error cleanup test follows the
renamed durableAckTiers field in its reflective setup.
Review found localDurableTableWatermarks written but never readable,
making the promised both-tier progress signal unreachable. The loop
now exposes getLocalDurableTableWatermark(tableName) alongside the
other ack counters.
The tier feature landed with only mechanical test edits (false ->
DurableAckTiers.NONE signature updates) and four dead test hooks:
cursorSendLoopForTest, localDurableAck, getTotalLocalDurableAcks and
getLocalDurableTableWatermark had no callers. This commit adds the
missing behavioral coverage and puts every hook to use:

- DurableAckTiersTest covers parseConfigValue/configValue round trips,
  the request and confirmation wire tokens, the rejected spellings,
  and the isTrimOnLocalAck trim-trigger predicate.
- CursorWebSocketSendLoopDurableAckTest gains local-tier tests:
  local-only trim on STATUS_LOCAL_DURABLE_ACK, progress-only local
  acks with both tiers requested, monotonic local watermarks, the
  stray-frame warn path, watermark reset on reconnect, and the
  replicated-ack-in-local-mode subsumption path.
- WebSocketResponseLocalDurableAckTest round-trips the
  STATUS_LOCAL_DURABLE_ACK wire format through writeTo/readFrom,
  isStructurallyValid, truncation rejection, and status naming.
- TestWebSocketServer now captures the X-QWP-Request-Durable-Ack
  header and echoes the granted set (the legacy "true" request still
  confirms as "enabled"), with a fixed-grant override for mismatch
  tests. Integration tests drive local and local,replicated flows
  through the real handshake, pin the exact request token per
  configured set, assert denial when the server grants a different
  set, and pin the boolean connect overload's legacy mapping.
- DurableAckIntegrationTest.awaitOks counted one OK per row, but the
  server emits one OK per batch, so the helper always spun its full
  5s deadline and returned silently. awaitOkBatches counts batches
  and fails loudly on timeout; the suite drops from ~20s to under 1s.

Comment cleanup in the same area: the compat-overload block,
DurableAckTiers javadoc and Sender.requestDurableAck(boolean) now
describe what the code does instead of narrating the retyping and
release history ("was retyped", "shipped meaning").
@mtopolnik

Copy link
Copy Markdown
Contributor

[PR Coverage check]

😍 pass : 106 / 147 (72.11%)

file detail

path covered line new line coverage
🔵 io/questdb/client/cutlass/qwp/client/QwpWebSocketSender.java 7 40 17.50%
🔵 io/questdb/client/Sender.java 11 19 57.89%
🔵 io/questdb/client/cutlass/qwp/client/sf/cursor/BackgroundDrainer.java 2 2 100.00%
🔵 io/questdb/client/cutlass/http/client/WebSocketClient.java 11 11 100.00%
🔵 io/questdb/client/cutlass/qwp/client/DurableAckTiers.java 35 35 100.00%
🔵 io/questdb/client/cutlass/qwp/client/sf/cursor/CursorWebSocketSendLoop.java 24 24 100.00%
🔵 io/questdb/client/cutlass/qwp/client/WebSocketResponse.java 16 16 100.00%

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants