Skip to content

Commit d1fd2bd

Browse files
authored
refactor(qwp): consolidate the QWP connect-string configuration keys (#66)
1 parent 1cff44c commit d1fd2bd

23 files changed

Lines changed: 785 additions & 493 deletions

CLAUDE.md

Lines changed: 12 additions & 87 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,8 @@ Go client library for QuestDB ingestion. Three transports:
1717

1818
Module path: `github.com/questdb/go-questdb-client/v4` — the `/v4` segment is
1919
load-bearing when importing within this repo. Minimum Go: 1.23 (go.mod pins
20-
`go 1.23` with a `1.24.4` toolchain).
20+
`go 1.23` with no `toolchain` directive; CI's `1.23.x`/`1.24.x` matrix runs
21+
under `GOTOOLCHAIN=local`).
2122

2223
## Commands
2324

@@ -88,92 +89,20 @@ encodes a batch into `qwpSfCursorEngine` via `engineAppendBlocking`; the
8889
`qwpSfSendLoop` goroutine drains it to the WebSocket, parses ACKs, advances
8990
`engineAckedFsn`, and owns reconnect + replay from `engineAckedFsn() + 1`.
9091

91-
**Cursor frames are self-sufficient** — full schema definitions plus the full
92-
symbol dictionary from id 0, every flush. This is what makes
93-
reconnect/replay/orphan-adoption safe across a fresh server connection.
94-
95-
**The wire carries no schema id and no schema mode byte.** A table block is
96-
`table_name, row_count, col_count, inline columns, column data`; the inline
97-
column definitions are the authoritative schema, repeated on every frame. There
98-
is no `nextSchemaId` accumulator on the sender, no per-table `schemaId` field on
99-
the table buffer, no schema-change detection, and no reference mode. (QWP once
100-
carried a mode byte + schema id plus a schema-reference optimisation; it was
101-
removed across the server and all clients.) On egress, the decoder parses the
102-
schema from the first `RESULT_BATCH` of a query (`batch_seq == 0`) into
103-
`qwpQueryDecoder.querySchema` and reuses it for that query's continuation
104-
batches; `qwpEgressIO.dispatcherRun` calls `resetQuerySchema` at the start of
105-
every query so a schema never leaks across query boundaries.
106-
107-
Symbol-dict tracking (`maxSentSymbolId`, `batchMaxSymbolId`) is still in place,
108-
and both fields are load-bearing. The encoder always passes `-1` as the
109-
`maxSentId` arg of `encodeMultiTableWithDeltaDict` to force "full dict from id
110-
0", but `batchMaxSymbolId` is the separate `batchMaxId` arg and bounds the dict
111-
actually written: `writeDeltaDict` emits `globalDict[0..batchMaxSymbolId]`, so
112-
dropping it would silently truncate the symbol dict. `maxSentSymbolId` is the
113-
cross-flush high-water mark that `resetAfterFlush` rewinds `batchMaxSymbolId` to
114-
(never to `-1`), so a later batch reusing only earlier symbols still writes the
115-
full dict its rows reference. Both are also read by tests and external
116-
observers, but that is incidental to their wire role.
117-
118-
`WithInFlightWindow(n)` / `in_flight_window=n` is **retained but a no-op** in
119-
the cursor architecture — backpressure is governed by the engine's segment-ring
120-
+ `engineAppendBlocking` deadline.
121-
122-
### Java-parity QWP knobs (not in connect-string.md)
123-
124-
These connect-string keys are recognised by the Java client
125-
(`Sender.java`) but are not listed in the
126-
[native-client spec](https://github.com/questdb/questdb-enterprise/blob/main/questdb/docs/qwp/connect-string.md).
127-
We accept them for Java-parity portability — a connect string that
128-
works on the Java client must work here. None should ever be
129-
considered for removal without a matching change in Java:
130-
131-
- `gorilla=on|off` — gates the Gorilla timestamp encoding in
132-
`qwp_encoder.go` (FLAG_GORILLA). Default `on`.
133-
- `in_flight_window=N` — see the "retained but a no-op" note above.
134-
135-
`close_timeout=N` (millisecond integer) was a v4.0–v4.5 Go-only key
136-
for the memory-mode close path. The cursor architecture unified
137-
memory and SF onto `close_flush_timeout_millis`, which the spec
138-
also defines. The parser now rejects `close_timeout=` with a
139-
migration hint pointing at `close_flush_timeout_millis`.
140-
`WithCloseTimeout(d)` is retained as a deprecated alias that routes
141-
positive durations through `close_flush_timeout_millis`; new code
142-
should use `WithCloseFlushTimeout` directly.
143-
144-
Flush semantics: `Flush` / `FlushAndGetSequence` **never wait for the server
145-
ACK** — they return once the batch is published into the cursor engine (in-RAM
146-
for memory mode, on-disk for SF) and the send loop delivers + replays it in the
147-
background. This matches the Java spec (`design/qwp-cursor-durability.md`
148-
decision #1: "flush() never waits for ACK; ACKs are async") and is uniform
149-
across both the pending-rows and zero-pending branches and auto-flush — all
150-
route through `enqueueCursor`; explicit `Flush` only additionally surfaces a
151-
latched send-loop error eagerly. (`Flush` was an ACK barrier
152-
through v4.2.0; that contract was dropped when the cursor/SF architecture made
153-
local persistence, not the ACK, the durability guarantee.) `FlushAndGetSequence` returns the
154-
published FSN — the upper bound of any `SenderError.ToFsn` for that batch;
155-
**pair it with `AwaitAckedFsn` for server-ACK confirmation** (the dedicated
156-
primitive now that `Flush` no longer blocks on ACKs).
157-
158-
Orphan-slot adoption (SF mode, `drain_orphans=on`) is implemented in
159-
`qwp_sf_orphan.go` + `qwp_sf_drainer.go` + `qwp_sf_round_walk.go`; drainers run
160-
in dedicated goroutines and are visible via `QwpSender.BackgroundDrainers()`.
92+
**`Flush` / `FlushAndGetSequence` never wait for the server ACK** — they return
93+
once the batch is published to the cursor engine (in-RAM for memory mode,
94+
on-disk for SF); delivery and replay to the server run in the background. For
95+
server-ACK confirmation, pair `FlushAndGetSequence` with `AwaitAckedFsn`.
16196

16297
### Error handling
16398

16499
QWP server rejections surface as `*SenderError` (`sender_error.go` is canonical
165-
for categories + policy enum). Two paths: async callback registered via
166-
`WithErrorHandler`, and producer-side typed error via `errors.As` after `Flush`
167-
/ `FlushAndGetSequence`.
168-
169-
Policy resolution precedence (highest first): `WithErrorPolicyResolver`
170-
`WithErrorPolicy(category, ...)` → connect-string `on_*_error`
171-
`on_server_error` → spec defaults. `PROTOCOL_VIOLATION` and `UNKNOWN` are never
172-
user-configurable — always HALT.
173-
174-
A HALT latches the typed error on the I/O loop; `sendLoopCheckError()` surfaces
175-
it on the next producer call. The sender does not auto-resume — close + rebuild
176-
is the supported recovery (matches Java).
100+
for the categories + policy enum). `PROTOCOL_VIOLATION` and `UNKNOWN` are never
101+
user-configurable — always HALT. A HALT records the typed error on the I/O loop
102+
and surfaces it on the next producer call; the sender does not auto-resume, so
103+
close + rebuild is the supported recovery. Policy precedence, highest first:
104+
`WithErrorPolicyResolver``WithErrorPolicy` → connect-string `on_*_error`
105+
`on_server_error` → spec defaults.
177106

178107
### Connection pooling
179108

@@ -186,10 +115,6 @@ doesn't participate.
186115
QWP unit tests use `httptest.Server` to stand in for the QuestDB WebSocket
187116
endpoint (`newQwpTestServer` in `qwp_sender_test.go`). ILP unit tests are pure.
188117

189-
`*_integration_test.go` files need Docker — they spin up real QuestDB via
190-
testcontainers-go; HTTP/TCP suites sometimes launch haproxy via
191-
`test/haproxy.cfg`.
192-
193118
Cross-language conformance: `interop_test.go` +
194119
`test/interop/questdb-client-test` (submodule) — ILP vectors shared across
195120
QuestDB client libraries.

README.md

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -175,9 +175,9 @@ governed by the engine's segment ring and the append deadline
175175
(`sf_append_deadline_millis` in store-and-forward mode), not by a
176176
fixed in-flight count.
177177

178-
`in_flight_window` / `qdb.WithInFlightWindow(n)` is **retained for
179-
backward compatibility but is a no-op** in this architecture. Connect
180-
strings carrying it still parse; the value is ignored.
178+
There is no in-flight-window knob: the cursor architecture governs
179+
backpressure on its own, so connect strings have no pipeline-depth key
180+
and an unrecognized one is rejected as an unsupported option.
181181

182182
`Flush` and `FlushAndGetSequence` **never wait for the server ACK**.
183183
They return once the batch is published into the cursor engine — in
@@ -577,7 +577,7 @@ the lock releases automatically when the process exits.
577577
|---|---|---|
578578
| `sf_dir` | unset | Group root. Setting it activates SF. |
579579
| `sender_id` | `default` | Per-sender slot name; ASCII letters / digits / `-_.` only. |
580-
| `sf_max_bytes` | 4 MiB | Per-segment file size. |
580+
| `sf_max_segment_bytes` | 4 MiB | Per-segment file size. |
581581
| `sf_max_total_bytes` | 10 GiB | Total cap; producer is backpressured when reached. |
582582
| `sf_durability` | `memory` | Reserved; `flush` / `append` are deferred follow-ups. |
583583
| `sf_append_deadline_millis` | 30000 | How long `At` / `AtNow` block on backpressure before failing. |
@@ -590,7 +590,7 @@ the lock releases automatically when the process exits.
590590
| `max_background_drainers` | 4 | Cap on concurrent orphan drainers. |
591591

592592
The same options are available programmatically:
593-
`WithSfDir`, `WithSenderId`, `WithSfMaxBytes`, `WithSfMaxTotalBytes`,
593+
`WithSfDir`, `WithSenderId`, `WithSfMaxSegmentBytes`, `WithSfMaxTotalBytes`,
594594
`WithReconnectPolicy`, `WithInitialConnectRetry`,
595595
`WithInitialConnectMode`, `WithCloseFlushTimeout`.
596596

0 commit comments

Comments
 (0)