feat(pool): QuestDBClient sender connection pool (Java parity)#76
feat(pool): QuestDBClient sender connection pool (Java parity)#76ideoma wants to merge 28 commits into
Conversation
Adds an elastic, thread-safe pool of ISender instances mirroring the Java client's QuestDB handle: QuestDBClient.Connect / Builder, BorrowSender (+async), thread-affine Sender()/ReleaseSender(), and a housekeeper that reaps idle / over-age senders. Pooled senders return on dispose (flush + give back), not disconnect. - Config keys on SenderOptions (sender_pool_min/max, acquire_timeout_ms, idle_timeout_ms, max_lifetime_ms, housekeeper_interval_ms), JsonIgnore'd so plain senders round-trip unchanged. - SemaphoreSlim capacity gate, create-outside-lock, elastic min/max. - Store-and-forward: per-slot sender_id=<base>-<index>, slot bitmap with lock-release-aware reclaim/retire (unified leak-debt capacity accounting). - Two production gaps closed: QwpSlotLock.IsReleased -> QwpWebSocketSender.IsSlotLockReleased (via IPooledSlotSender seam), and QwpOrphanScanner exclude-managed-slots filtering. - Pooled ws:: senders castable to IQwpWebSocketSender for the full QWP set. - ~80 unit tests; example-sender-pool; CLAUDE.md updated. Egress query pooling and proactive in-range SF startup recovery are not included (documented limitations). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
|
Important Review skippedDraft detected. Please check the settings in the CodeRabbit UI or the ⚙️ Run configurationConfiguration used: defaults Review profile: CHILL Plan: Pro Run ID: You can disable this status message by setting the Use the checkbox below for a quick retry:
✨ Finishing Touches🧪 Generate unit tests (beta)
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. Comment |
… connections (and SF flocks) when the query pool fails to construct
…s flush on a non-thread-safe inner sender
… is rejected case-sensitively at query time
…engine defers its lock release past the 5 s teardown budget
…call, even uncontended
…hreaded tests run on HTTP pools, so AllocateSlotIndex/RetireSlotIndex/SettleLeakDebtLocked (the most race-prone code) are never exercised under contention
…ow a spurious PoolExhausted
…conditionally, so the documented capability probe lies for HTTP/TCP pools
Code review — QuestDBClient connection pool (level 2)Verdict: Approve with minor changes. No critical or data-loss/leak/corruption defect survived verification. The permit/leak-debt accounting, use-after-return guard, and the Cancel-TOCTOU fix are all correct on independent trace. Six review passes (correctness/concurrency, borrowed-handle lifecycle, config/lazy_connect, query pool, tests, adversarial fresh-context) plus a line-by-line read of the core pool files all corroborate a clean core. Findings below are 3 Moderate + Minor. Moderate1. QuestDBClient.Builder().FromConfig("ws::addr=x;query_pool_min=3;")
.LazyConnect().QueryPoolMin(0).Build(); // throws ConfigErrorThe effective 2. Shared pool timing knobs set only in a separate query config are silently dropped — QuestDBClient.Connect("http::addr=x;", "ws::addr=y;acquire_timeout_ms=200;idle_timeout_ms=1000");The query pool silently uses 5000/60000 (ingest defaults), not 200/1000. Either honor these from the query config or reject them there — accept-and-ignore is silent misconfiguration. 3. Cooperative Minor
Test coverage gaps
Verified clean (dismissed after source verification)
All findings are in-diff. 🤖 Generated with Claude Code |
Summary
Adds
QuestDBClient, an elastic, thread-safe pool ofISenderinstances mirroring the Java client'sQuestDBhandle. Construct once, share across threads;BorrowSender()(+BorrowSenderAsync) leases a sender that flushes and returns to the pool on dispose (not disconnect). A background housekeeper reaps idle / over-age senders. On net7.0+ the same handle also pools egress query clients (NewQuery()/ExecuteSqlAsync). Also addslazy_connectfor tolerant startup (start with the server down, buffer writes, read once it's up).Highlights
SenderOptions(sender_pool_min/max,acquire_timeout_ms,idle_timeout_ms,max_lifetime_ms,housekeeper_interval_ms),[JsonIgnore]d so a plain sender round-trips byte-identically.SemaphoreSlimcapacity gate, create-outside-lock, elastic min/max, acquire-timeout (PoolExhausted).IQwpQueryClients —NewQuery()/ExecuteSqlAsync(...), with the client lease held implicitly per execution (no public borrow/release). Addsquery_pool_min/max; the acquire/idle/lifetime/housekeeper knobs are shared with the sender pool and swept by one housekeeper.lazy_connect) — a pool-facade connect-string flag (alsoQuestDBClientBuilder.LazyConnect()) that lets the handle build while the server is down. Thews/wssingest side connects asynchronously (buffering writes meanwhile) and the read pool defaultsquery_pool_min=0so nothing connects eagerly — the read pool stays enabled and a query connects lazily on firstNewQueryonce the server is up.Build()(ResolveLazyConnect) injectsinitial_connect_retry=asyncfor the pooled ws senders when unset, and rejects a conflicting blocking-startup knob up front (IngressError/ConfigError): an explicitinitial_connect_retryother thanasync, or an explicitquery_pool_min > 0(connect string orQueryPoolMin(...)builder call, tracked across ingest + separate-query configs). Lives inQwpConnectStringKeys.Shared, so a plainSenderparses and ignores it. Mirrors java-questdb-clientlazy_connect.sender_id=<base>-<index>, slot bitmap with lock-release-aware reclaim/retire (unified leak-debt capacity accounting).QwpSlotLock.IsReleased→QwpWebSocketSender.IsSlotLockReleased(net-agnosticIPooledSlotSenderseam);QwpOrphanScannerexclude-managed-slots filter.ws::/wss::senders are castable toIQwpWebSocketSender(full QWP column set,Ping, seqTxn watermarks).Out of scope (documented)
Test plan
src/net-questdb-client-tests/Pooling/(sender + query pool: config, borrow/return, exhaustion, reaping, SF slot allocation/reclaim/retire, error-safety) plusPoolOptionsTestsat the test-project root.lazy_connectcoverage:LazyConnectTests(flag parsing, accepted-on-every-scheme,ToStringround-trip,ResolveLazyConnectdefaulting/conflict logic, and facade builds/conflicts throughQuestDBClient.Connect/ builder) plusLazyConnectRecoveryTests— end-to-end down→up→restart against an in-process QWP server whose lifetime the test controls: start the client before the server (writes buffer), bring it up (buffered writes drain), then restart the server mid-stream on the same port (the engine reconnects to the fresh instance and replays only the un-acked frames).DummyQwpServergains a fixed-Portoption (defaults to random, so existing tests are unchanged).🤖 Generated with Claude Code