Skip to content

Commit 2cef2df

Browse files
committed
feat(facade): single cluster config for the QuestDB handle
Collapse the dual ingest/query config surface on the QuestDB facade into a single configuration string for the whole cluster. A QuestDB cluster is one logical target reached over QWP for both ingest and query, so one ws/wss string -- listing every node in a single `addr` server list -- now drives both the sender and query pools. - QuestDBBuilder: drop ingestConfig()/queryConfig(); fromConfig() sets the one cluster config. Remove the cross-side pool-key conflict resolution (no two strings to reconcile) -- resolvePoolInt/Long read one ConfigView. build() validates the single string with both the ingest and egress validators; each side applies the keys it owns and ignores the rest. - QuestDB: remove the connect(ingest, query) overload; connect(config) and builder() now document the one-config/server-list model. - QuestDBImpl is unchanged: the builder passes the one config to both pool slots, preserving the white-box reflection seam. Tests: TestWebSocketServer now serves both pools from one config like a real node -- SERVER_INFO is emitted only on the egress /read path (the ingest /write ACK stream would choke on it), plus a setRejectReadUpgrade() toggle to fail just the query upgrade. Rewrote QuestDBBuilderTest and updated the facade callback/recovery/write-only tests and the examples accordingly.
1 parent a494675 commit 2cef2df

8 files changed

Lines changed: 245 additions & 435 deletions

File tree

core/src/main/java/io/questdb/client/QuestDB.java

Lines changed: 12 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -38,10 +38,10 @@
3838
* and the {@link Completion} associated with each query is a field on that
3939
* cached handle.
4040
* <p>
41-
* Configuration: use {@link #connect(CharSequence)} when the same address list
42-
* and credentials serve both ingest and egress -- the most common case.
43-
* Use {@link #connect(CharSequence, CharSequence)} or {@link #builder()} when
44-
* ingest and egress endpoints differ.
41+
* Configuration: one {@code ws}/{@code wss} string describes the whole cluster
42+
* (a single {@code addr} server list) and both the ingest and query pools
43+
* connect across it. Use {@link #connect(CharSequence)} for the common case, or
44+
* {@link #builder()} for pool sizing, the ingest callbacks, and write-only mode.
4545
* <p>
4646
* Thread safety: instances are safe to share. {@link #borrowSender()} and
4747
* {@link #query()} may be called concurrently from any thread; the pool
@@ -51,20 +51,21 @@ public interface QuestDB extends Closeable {
5151

5252
/**
5353
* Builder for advanced configuration (pool sizes, acquisition timeouts,
54-
* differing ingest/egress configs).
54+
* ingest callbacks, write-only mode).
5555
*/
5656
static QuestDBBuilder builder() {
5757
return new QuestDBBuilder();
5858
}
5959

6060
/**
61-
* Connects with a single configuration string used for both ingest and
62-
* egress. The schema must be {@code ws} or {@code wss}: QuestDB ingests and
63-
* queries over QWP (the QuestDB WebSocket protocol), so one string
64-
* configures both clients.
61+
* Connects with a single configuration string for the whole QuestDB cluster,
62+
* used for both ingest and egress. The schema must be {@code ws} or
63+
* {@code wss}: QuestDB ingests and queries over QWP (the QuestDB WebSocket
64+
* protocol), so one string configures both clients. List every cluster node
65+
* in a single {@code addr} server list and both pools connect across it.
6566
* <p>
66-
* Use {@link #connect(CharSequence, CharSequence)} or {@link #builder()}
67-
* when ingest and egress use different addresses or credentials.
67+
* Use {@link #builder()} for pool sizing, the ingest callbacks, and
68+
* write-only mode.
6869
*
6970
* @param configurationString a {@code ws}/{@code wss} config string (see
7071
* {@link Sender#fromConfig} or
@@ -75,22 +76,6 @@ static QuestDB connect(CharSequence configurationString) {
7576
return builder().fromConfig(configurationString).build();
7677
}
7778

78-
/**
79-
* Connects with explicit ingest and egress configuration strings.
80-
*
81-
* @param ingestConfigurationString config for the {@link Sender} pool
82-
* ({@link Sender#fromConfig} format)
83-
* @param queryConfigurationString config for the query pool
84-
* ({@link io.questdb.client.cutlass.qwp.client.QwpQueryClient#fromConfig} format)
85-
* @return a connected QuestDB handle
86-
*/
87-
static QuestDB connect(CharSequence ingestConfigurationString, CharSequence queryConfigurationString) {
88-
return builder()
89-
.ingestConfig(ingestConfigurationString)
90-
.queryConfig(queryConfigurationString)
91-
.build();
92-
}
93-
9479
/**
9580
* Borrows a {@link Sender} from the pool. The caller MUST call
9681
* {@link Sender#close()} on the returned instance to release it back to

core/src/main/java/io/questdb/client/QuestDBBuilder.java

Lines changed: 56 additions & 119 deletions
Original file line numberDiff line numberDiff line change
@@ -35,14 +35,15 @@
3535

3636
/**
3737
* Builder for {@link QuestDB}. Most callers use {@link QuestDB#connect(CharSequence)};
38-
* this builder is for pool sizing, idle/lifetime knobs, acquire timeout,
39-
* and the case where ingest and egress configs differ.
38+
* this builder adds pool sizing, idle/lifetime knobs, the acquire timeout, the
39+
* ingest callbacks, and write-only mode.
4040
* <p>
41-
* Both configs must use the {@code ws} or {@code wss} schema (QWP over
42-
* WebSocket). A pool key (e.g. {@code sender_pool_min}) may be carried in the
43-
* connect string or set with an explicit builder call; an explicit call always
44-
* wins. When both connect strings carry the same pool key with different values,
45-
* {@link #build()} fails.
41+
* One configuration string describes the whole QuestDB cluster (see
42+
* {@link #fromConfig}): list every node in a single {@code addr} server list and
43+
* both the ingest and query pools connect across it. The schema must be
44+
* {@code ws} or {@code wss} (QWP over WebSocket). A pool key (e.g.
45+
* {@code sender_pool_min}) may be carried in the connect string or set with an
46+
* explicit builder call; an explicit call always wins.
4647
*/
4748
public final class QuestDBBuilder {
4849

@@ -64,10 +65,9 @@ public final class QuestDBBuilder {
6465
private SenderConnectionListener connectionListener;
6566
private SenderErrorHandler errorHandler;
6667
private long housekeeperIntervalMillis = UNSET;
68+
private String config;
6769
private long idleTimeoutMillis = UNSET;
68-
private String ingestConfig;
6970
private long maxLifetimeMillis = UNSET;
70-
private String queryConfig;
7171
private int queryPoolMax = UNSET;
7272
private int queryPoolMin = UNSET;
7373
private int senderPoolMax = UNSET;
@@ -157,42 +157,36 @@ public QuestDBBuilder errorHandler(SenderErrorHandler handler) {
157157
* and is delivered once the server acks; until then it stays preserved.
158158
*/
159159
public QuestDB build() {
160-
if (ingestConfig == null) {
161-
throw new IllegalStateException("ingest configuration is required; call fromConfig() or ingestConfig()");
160+
if (config == null) {
161+
throw new IllegalStateException("configuration is required; call fromConfig()");
162162
}
163163
if (writeOnly) {
164164
return buildWriteOnly();
165165
}
166-
if (queryConfig == null) {
167-
throw new IllegalStateException("query configuration is required; call fromConfig() or queryConfig()");
168-
}
169-
ConfigString ingestCs = ConfigString.parse(ingestConfig);
170-
ConfigString queryCs = ConfigString.parse(queryConfig);
171-
ConfigView ingestView = new ConfigView(ingestCs);
172-
ConfigView queryView = new ConfigView(queryCs);
173-
// Validate both connect strings exactly as the pools will, but without
174-
// connecting. The ingest string runs the full Sender parse plus
175-
// validateParameters -- ingress value keys are registry-STRING, so only
176-
// the real parse validates their values. The egress string runs the
177-
// typed validateConfig. A malformed config therefore fails here even
178-
// when a pool min is 0 and nothing connects.
179-
Sender.LineSenderBuilder.validateWsConfigString(ingestConfig);
180-
QwpQueryClient.validateConfig(queryView, "wss".equals(queryCs.schema()));
181-
182-
// A view carries no side; getInt/getLong read any key, so the ingest
183-
// and query views also serve the POOL reads.
184-
resolvePoolInt(senderPoolMin, "sender_pool_min", ingestView, queryView, DEFAULT_POOL_MIN, this::senderPoolMin);
185-
resolvePoolInt(senderPoolMax, "sender_pool_max", ingestView, queryView, DEFAULT_POOL_MAX, this::senderPoolMax);
186-
resolvePoolInt(queryPoolMin, "query_pool_min", ingestView, queryView, DEFAULT_POOL_MIN, this::queryPoolMin);
187-
resolvePoolInt(queryPoolMax, "query_pool_max", ingestView, queryView, DEFAULT_POOL_MAX, this::queryPoolMax);
188-
resolvePoolLong(acquireTimeoutMillis, "acquire_timeout_ms", ingestView, queryView, DEFAULT_ACQUIRE_TIMEOUT_MILLIS, this::acquireTimeoutMillis);
189-
resolvePoolLong(idleTimeoutMillis, "idle_timeout_ms", ingestView, queryView, DEFAULT_IDLE_TIMEOUT_MILLIS, this::idleTimeoutMillis);
190-
resolvePoolLong(maxLifetimeMillis, "max_lifetime_ms", ingestView, queryView, DEFAULT_MAX_LIFETIME_MILLIS, this::maxLifetimeMillis);
191-
resolvePoolLong(housekeeperIntervalMillis, "housekeeper_interval_ms", ingestView, queryView, DEFAULT_HOUSEKEEPER_INTERVAL_MILLIS, this::housekeeperIntervalMillis);
166+
ConfigString cs = ConfigString.parse(config);
167+
ConfigView view = new ConfigView(cs);
168+
// Validate the single cluster config exactly as both pools will, but
169+
// without connecting: the full Sender parse plus validateParameters
170+
// (ingress value keys are registry-STRING, so only the real parse
171+
// validates their values), then the typed egress validateConfig. Each
172+
// side applies the keys it owns and silently ignores the rest, so one
173+
// string drives both. A malformed config therefore fails here even when
174+
// a pool min is 0 and nothing connects.
175+
Sender.LineSenderBuilder.validateWsConfigString(config);
176+
QwpQueryClient.validateConfig(view, "wss".equals(cs.schema()));
177+
178+
resolvePoolInt(senderPoolMin, "sender_pool_min", view, DEFAULT_POOL_MIN, this::senderPoolMin);
179+
resolvePoolInt(senderPoolMax, "sender_pool_max", view, DEFAULT_POOL_MAX, this::senderPoolMax);
180+
resolvePoolInt(queryPoolMin, "query_pool_min", view, DEFAULT_POOL_MIN, this::queryPoolMin);
181+
resolvePoolInt(queryPoolMax, "query_pool_max", view, DEFAULT_POOL_MAX, this::queryPoolMax);
182+
resolvePoolLong(acquireTimeoutMillis, "acquire_timeout_ms", view, DEFAULT_ACQUIRE_TIMEOUT_MILLIS, this::acquireTimeoutMillis);
183+
resolvePoolLong(idleTimeoutMillis, "idle_timeout_ms", view, DEFAULT_IDLE_TIMEOUT_MILLIS, this::idleTimeoutMillis);
184+
resolvePoolLong(maxLifetimeMillis, "max_lifetime_ms", view, DEFAULT_MAX_LIFETIME_MILLIS, this::maxLifetimeMillis);
185+
resolvePoolLong(housekeeperIntervalMillis, "housekeeper_interval_ms", view, DEFAULT_HOUSEKEEPER_INTERVAL_MILLIS, this::housekeeperIntervalMillis);
192186

193187
return new QuestDBImpl(
194-
ingestConfig,
195-
queryConfig,
188+
config,
189+
config,
196190
senderPoolMin,
197191
senderPoolMax,
198192
queryPoolMin,
@@ -210,19 +204,17 @@ public QuestDB build() {
210204
// knobs from the ingest config alone and constructs a QuestDBImpl with no
211205
// query pool. Never touches the read side, so a down server cannot fail it.
212206
private QuestDB buildWriteOnly() {
213-
ConfigString ingestCs = ConfigString.parse(ingestConfig);
214-
ConfigView ingestView = new ConfigView(ingestCs);
215-
Sender.LineSenderBuilder.validateWsConfigString(ingestConfig);
216-
// Only the ingest view applies; pass it for both sides so the existing
217-
// resolvers (which cross-check ingest vs query) read a single source.
218-
resolvePoolInt(senderPoolMin, "sender_pool_min", ingestView, ingestView, DEFAULT_POOL_MIN, this::senderPoolMin);
219-
resolvePoolInt(senderPoolMax, "sender_pool_max", ingestView, ingestView, DEFAULT_POOL_MAX, this::senderPoolMax);
220-
resolvePoolLong(acquireTimeoutMillis, "acquire_timeout_ms", ingestView, ingestView, DEFAULT_ACQUIRE_TIMEOUT_MILLIS, this::acquireTimeoutMillis);
221-
resolvePoolLong(idleTimeoutMillis, "idle_timeout_ms", ingestView, ingestView, DEFAULT_IDLE_TIMEOUT_MILLIS, this::idleTimeoutMillis);
222-
resolvePoolLong(maxLifetimeMillis, "max_lifetime_ms", ingestView, ingestView, DEFAULT_MAX_LIFETIME_MILLIS, this::maxLifetimeMillis);
223-
resolvePoolLong(housekeeperIntervalMillis, "housekeeper_interval_ms", ingestView, ingestView, DEFAULT_HOUSEKEEPER_INTERVAL_MILLIS, this::housekeeperIntervalMillis);
207+
ConfigString cs = ConfigString.parse(config);
208+
ConfigView view = new ConfigView(cs);
209+
Sender.LineSenderBuilder.validateWsConfigString(config);
210+
resolvePoolInt(senderPoolMin, "sender_pool_min", view, DEFAULT_POOL_MIN, this::senderPoolMin);
211+
resolvePoolInt(senderPoolMax, "sender_pool_max", view, DEFAULT_POOL_MAX, this::senderPoolMax);
212+
resolvePoolLong(acquireTimeoutMillis, "acquire_timeout_ms", view, DEFAULT_ACQUIRE_TIMEOUT_MILLIS, this::acquireTimeoutMillis);
213+
resolvePoolLong(idleTimeoutMillis, "idle_timeout_ms", view, DEFAULT_IDLE_TIMEOUT_MILLIS, this::idleTimeoutMillis);
214+
resolvePoolLong(maxLifetimeMillis, "max_lifetime_ms", view, DEFAULT_MAX_LIFETIME_MILLIS, this::maxLifetimeMillis);
215+
resolvePoolLong(housekeeperIntervalMillis, "housekeeper_interval_ms", view, DEFAULT_HOUSEKEEPER_INTERVAL_MILLIS, this::housekeeperIntervalMillis);
224216
return new QuestDBImpl(
225-
ingestConfig,
217+
config,
226218
senderPoolMin,
227219
senderPoolMax,
228220
acquireTimeoutMillis,
@@ -235,14 +227,15 @@ private QuestDB buildWriteOnly() {
235227
}
236228

237229
/**
238-
* Sets a single configuration string used for both ingest and egress. The
239-
* schema must be {@code ws} or {@code wss}.
230+
* Sets the single configuration string for the whole QuestDB cluster --
231+
* used for both ingest and egress. List every cluster node in one
232+
* {@code addr} (comma-separated, or by repeating the key); the ingest and
233+
* query pools each connect across that one server list. The schema must be
234+
* {@code ws} or {@code wss}.
240235
*/
241236
public QuestDBBuilder fromConfig(CharSequence configurationString) {
242-
requireWebSocketSchema(configurationString, "connection");
243-
String s = configurationString.toString();
244-
this.ingestConfig = s;
245-
this.queryConfig = s;
237+
requireWebSocketSchema(configurationString, "cluster");
238+
this.config = configurationString.toString();
246239
return this;
247240
}
248241

@@ -272,16 +265,6 @@ public QuestDBBuilder idleTimeoutMillis(long millis) {
272265
return this;
273266
}
274267

275-
/**
276-
* Sets the ingest-side configuration. The schema must be {@code ws} or
277-
* {@code wss}.
278-
*/
279-
public QuestDBBuilder ingestConfig(CharSequence configurationString) {
280-
requireWebSocketSchema(configurationString, "ingest");
281-
this.ingestConfig = configurationString.toString();
282-
return this;
283-
}
284-
285268
/**
286269
* Maximum age of a pooled connection before the housekeeper recycles it
287270
* (next time it is idle). Useful for picking up DNS / load-balancer
@@ -295,16 +278,6 @@ public QuestDBBuilder maxLifetimeMillis(long millis) {
295278
return this;
296279
}
297280

298-
/**
299-
* Sets the query-side configuration. The schema must be {@code ws} or
300-
* {@code wss}.
301-
*/
302-
public QuestDBBuilder queryConfig(CharSequence configurationString) {
303-
requireWebSocketSchema(configurationString, "query");
304-
this.queryConfig = configurationString.toString();
305-
return this;
306-
}
307-
308281
/**
309282
* Maximum query-pool size. Defaults to 4.
310283
*/
@@ -406,53 +379,17 @@ private static void requireWebSocketSchema(CharSequence config, String role) {
406379
}
407380
}
408381

409-
private void resolvePoolInt(int current, String key, ConfigView ingest, ConfigView query, int dflt, IntConsumer setter) {
382+
private void resolvePoolInt(int current, String key, ConfigView view, int dflt, IntConsumer setter) {
410383
if (current != UNSET) {
411-
return; // explicit builder call wins; skip the conflict check
412-
}
413-
boolean inIngest = ingest.has(key);
414-
boolean inQuery = query.has(key);
415-
int value;
416-
if (inIngest && inQuery) {
417-
int vi = ingest.getInt(key, UNSET);
418-
int vq = query.getInt(key, UNSET);
419-
if (vi != vq) {
420-
throw new IllegalArgumentException(
421-
"conflicting pool config: " + key + " (ingest=" + vi + ", query=" + vq + ")");
422-
}
423-
value = vi;
424-
} else if (inIngest) {
425-
value = ingest.getInt(key, UNSET);
426-
} else if (inQuery) {
427-
value = query.getInt(key, UNSET);
428-
} else {
429-
value = dflt;
384+
return; // explicit builder call wins
430385
}
431-
setter.accept(value);
386+
setter.accept(view.has(key) ? view.getInt(key, UNSET) : dflt);
432387
}
433388

434-
private void resolvePoolLong(long current, String key, ConfigView ingest, ConfigView query, long dflt, LongConsumer setter) {
389+
private void resolvePoolLong(long current, String key, ConfigView view, long dflt, LongConsumer setter) {
435390
if (current != UNSET) {
436-
return; // explicit builder call wins; skip the conflict check
437-
}
438-
boolean inIngest = ingest.has(key);
439-
boolean inQuery = query.has(key);
440-
long value;
441-
if (inIngest && inQuery) {
442-
long vi = ingest.getLong(key, UNSET);
443-
long vq = query.getLong(key, UNSET);
444-
if (vi != vq) {
445-
throw new IllegalArgumentException(
446-
"conflicting pool config: " + key + " (ingest=" + vi + ", query=" + vq + ")");
447-
}
448-
value = vi;
449-
} else if (inIngest) {
450-
value = ingest.getLong(key, UNSET);
451-
} else if (inQuery) {
452-
value = query.getLong(key, UNSET);
453-
} else {
454-
value = dflt;
391+
return; // explicit builder call wins
455392
}
456-
setter.accept(value);
393+
setter.accept(view.has(key) ? view.getLong(key, UNSET) : dflt);
457394
}
458395
}

0 commit comments

Comments
 (0)