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 */
4748public 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