Skip to content

Commit 97d39c1

Browse files
committed
docs(design): add QWP client startup/failover behavior and ergonomics notes
1 parent b7ac84d commit 97d39c1

2 files changed

Lines changed: 655 additions & 0 deletions

File tree

Lines changed: 214 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,214 @@
1+
# QWP client startup/failover — ergonomics issues
2+
3+
Tracked sharp edges surfaced while reviewing
4+
[`qwp-client-startup-failover-behavior.md`](./qwp-client-startup-failover-behavior.md).
5+
Each entry is grounded in source. "Candidate" = likely defect worth changing;
6+
"Intended (revisit)" = deliberate contract that may still deserve reconsideration.
7+
8+
Severity legend: **P1** user-visible footgun likely to cause an outage or hang ·
9+
**P2** confusing/surprising but recoverable · **P3** polish.
10+
11+
---
12+
13+
## ERG-1 — `initial_connect_retry` is implicitly promoted to SYNC (P1, Candidate)
14+
15+
**Symptom.** A user sets `reconnect_max_duration_millis` for resilience and,
16+
without setting `initial_connect_retry`, their application now **blocks** on
17+
startup for the entire budget when the server is down.
18+
19+
**Source.** `Sender.java` (~line 1451): if `initialConnectMode == null` and any
20+
`reconnect_*` knob is set, the mode resolves to `SYNC`.
21+
22+
**Why it's bad.** Mode is inferred from an unrelated knob. The "make me more
23+
resilient" action produces a "hang my boot" side effect. The code comment itself
24+
acknowledges the knob "reads as a generic retry budget but the underlying path
25+
only governs reconnects."
26+
27+
**Proposed fix.**
28+
- Make initial-connect mode an explicit, independent choice; stop inferring it.
29+
- If inference must stay for back-compat, log a `WARN` when a `reconnect_*` knob
30+
flips startup to `SYNC`, naming the knob and the resulting blocking behavior.
31+
32+
**Acceptance.** With only `reconnect_max_duration_millis` set and the server
33+
down, `build()` either returns promptly (OFF default) or logs an explicit
34+
warning before blocking. A test asserts the warning / non-blocking default.
35+
36+
---
37+
38+
## ERG-2 — `reconnect_max_duration_millis`: misleading name + inconsistent `0` (P2, Candidate)
39+
40+
**Symptom.** Two confusions:
41+
1. The name implies "reconnect only" but it also bounds the **initial** connect
42+
in SYNC/ASYNC modes.
43+
2. `reconnect_max_duration_millis=0` means **give up immediately**, whereas
44+
`idle_timeout_ms=0` and `max_lifetime_ms=0` in the same config surface mean
45+
**infinite**. There is no infinite-retry mode at all.
46+
47+
**Source.** `CursorWebSocketSendLoop.java:827``deadlineNanos = start + dur*1e6`,
48+
loop `while (now < deadline)`; `0` ⇒ zero iterations. Contrast
49+
`QuestDBBuilder.idleTimeoutMillis/maxLifetimeMillis` (`0 ⇒ Long.MAX_VALUE`).
50+
51+
**Why it's bad.** Same `0` token, opposite semantics depending on the knob;
52+
tolerating a long maintenance window forces magic numbers like `86400000`.
53+
54+
**Proposed fix.**
55+
- Adopt one `0` convention. Recommended: `0 ⇒ infinite`, matching the pool
56+
knobs, which also gives a real infinite-retry mode.
57+
- Consider an alias `connect_retry_budget_ms` that reflects it covers initial +
58+
reconnect; keep the old key as a deprecated alias.
59+
60+
**Acceptance.** Documented, consistent `0` semantics across the config surface;
61+
test covering `=0` behavior and (if added) infinite mode.
62+
63+
---
64+
65+
## ERG-3 — `failover` does not cover startup; queries have no async connect (P2, Candidate)
66+
67+
**Symptom.** Users expect `failover=on` to make startup resilient. It does not —
68+
it only governs reconnect+replay during `execute()` after a connection exists.
69+
Query initial connect is always synchronous and blocking, with no async/lazy
70+
mode (unlike ingest).
71+
72+
**Source.** `QwpQueryClient.connect()` is synchronous; `failover_*` defaults at
73+
`QwpQueryClient.java:139-141`; spec "Query client behavior".
74+
75+
**Why it's bad.** Expectation mismatch on a safety-critical knob; asymmetry
76+
between ingest (3 modes) and query (1 mode) forces two mental models.
77+
78+
**Proposed fix.**
79+
- Document `failover`'s scope prominently (done in the rewrite).
80+
- Evaluate an async/lazy initial-connect mode for the query client to match
81+
ingest, or a unified `initial_connect` setting shared by both sides.
82+
83+
**Acceptance.** Either query supports a documented non-blocking initial-connect
84+
mode, or the docs make the scope unambiguous and the limitation is explicitly
85+
accepted.
86+
87+
---
88+
89+
## ERG-4 — No first-class write-only facade (P2, Candidate)
90+
91+
**Symptom.** A write-only user of `QuestDB` must still supply a query config they
92+
never use **and** remember `query_pool_min=0` to avoid a build-time query
93+
connection.
94+
95+
**Source.** `QuestDBBuilder.build()` hard-requires both `ingestConfig` and
96+
`queryConfig`; no write-only path.
97+
98+
**Why it's bad.** Leaky and error-prone; the doc's own recommendation is "prefer
99+
direct `Sender`," which is an admission the facade is awkward here.
100+
101+
**Proposed fix.**
102+
- Add `QuestDB.builder().ingestConfig(...).writeOnly()` (or a `writeOnly()`
103+
shortcut) that skips the query pool entirely.
104+
- Symmetric `readOnly()` is a natural follow-up.
105+
106+
**Acceptance.** A write-only facade builds with no query config and creates no
107+
query pool; documented and tested.
108+
109+
---
110+
111+
## ERG-5 — A single endpoint's `401`/`403` aborts the whole walk (P2, Intended, revisit)
112+
113+
**Symptom.** One misconfigured endpoint returning `401`/`403` blocks startup
114+
even when other listed endpoints would accept the credentials. Applies to both
115+
ingest and query walks, including at startup.
116+
117+
**Source.** Ingest/query endpoint matrices; `CursorWebSocketSendLoop` treats
118+
`QwpAuthFailedException` as terminal across all endpoints.
119+
120+
**Why it's debatable.** "Fail fast on bad credentials" is reasonable, but it is
121+
asymmetric with how every *transport* failure is tolerated, and surprising
122+
during rolling credential rotation or a single bad node.
123+
124+
**Proposed fix (revisit).**
125+
- Keep terminal-on-auth as the contract, but make it a deliberately documented
126+
contract (done in the rewrite).
127+
- Consider an opt-in (e.g. `auth_failure=continue`) that demotes auth failure to
128+
a per-endpoint skip for heterogeneous fleets.
129+
130+
**Acceptance.** Behavior documented as intentional; decision recorded on whether
131+
an opt-in continue mode is warranted.
132+
133+
---
134+
135+
## ERG-6 — Facade can't reach error handler / connection listener / serverInfoTimeout (P2, Candidate)
136+
137+
**Symptom.** Through the `QuestDB` facade you cannot install a
138+
`SenderErrorHandler` or `SenderConnectionListener` (ingest), nor set
139+
`serverInfoTimeoutMs` (query). The latter has no config key at all.
140+
141+
**Source.** `LineSenderBuilder.errorHandler()/connectionListener()` exist only on
142+
the direct sender builder; `serverInfoTimeoutMs` is a `QwpQueryClient` builder
143+
field with no `ConfigSchema` key (`ConfigSchema.java` EGRESS section).
144+
145+
**Why it's bad.** The facade is the recommended high-level entry point, yet it
146+
cannot configure observability hooks or a documented query timeout.
147+
148+
**Proposed fix.**
149+
- Expose ingest error handler / connection listener on `QuestDBBuilder`
150+
(per-pool or shared).
151+
- Add a `server_info_timeout_ms` config key so it is reachable from any conn
152+
string (and therefore the facade).
153+
154+
**Acceptance.** Both hooks and the timeout are reachable from the facade;
155+
documented in the knob-availability matrix.
156+
157+
---
158+
159+
## ERG-7 — Simplest API has the worst error visibility (P1, Candidate)
160+
161+
**Symptom.** `Sender.fromConfig(cfg)` with `initial_connect_retry=async` swallows
162+
terminal startup failures — they surface only on a later producer call or at
163+
`close()`. The visible path requires switching to `Sender.builder(...)` and
164+
installing a handler.
165+
166+
**Source.** Async terminal `SenderError` delivered to a configured
167+
`SenderErrorHandler`; "even without a handler they are surfaced by later producer
168+
calls or close-time safety net behavior."
169+
170+
**Why it's bad.** The nicest ergonomics and the worst observability are
171+
inversely correlated for the single most important question: "did my writer ever
172+
connect?"
173+
174+
**Proposed fix.**
175+
- Default to a sane error sink (e.g. `WARN`/`ERROR` log on terminal async
176+
failure) even without a registered handler.
177+
- Provide a lightweight status accessor (e.g. `wasEverConnected()` /
178+
`lastError()`) on the public `Sender` surface for poll-based checks.
179+
180+
**Acceptance.** A terminal async failure is observable without installing a
181+
custom handler; documented and tested.
182+
183+
---
184+
185+
## ERG-8 — No client-side TCP connect timeout (P2, Intended, revisit)
186+
187+
**Symptom.** A black-holed host in the `addr` list blocks the endpoint walk
188+
until the OS connect timeout, undercutting the resilience value of listing
189+
multiple endpoints.
190+
191+
**Source.** `auth_timeout_ms` bounds only the post-connect upgrade/auth phase;
192+
no separate application-level TCP connect timeout in the transport.
193+
194+
**Why it's debatable.** It is a transport limitation, but it directly defeats the
195+
multi-endpoint failover use case at startup.
196+
197+
**Proposed fix (revisit).**
198+
- Add a client-side connect timeout so the walk can abandon black-holed hosts
199+
and proceed to the next endpoint.
200+
201+
**Acceptance.** A black-holed first endpoint no longer blocks past a configurable
202+
bound before the walk advances; documented and tested.
203+
204+
---
205+
206+
## Suggested sequencing
207+
208+
1. **ERG-1** and **ERG-7** (both P1) — they cause hangs and silent failures.
209+
2. **ERG-2**, **ERG-4**, **ERG-6** (P2 Candidate) — naming/consistency and
210+
facade completeness.
211+
3. **ERG-3**, **ERG-8** (P2, need design) — async query connect and connect
212+
timeout.
213+
4. **ERG-5** — confirm/record the auth-terminal contract; opt-in continue mode
214+
only if a concrete fleet use case justifies it.

0 commit comments

Comments
 (0)