2424
2525package io .questdb .client ;
2626
27- import io .questdb .client .impl .ConfigStringTranslator ;
27+ import io .questdb .client .cutlass .qwp .client .QwpQueryClient ;
28+ import io .questdb .client .impl .ConfigString ;
29+ import io .questdb .client .impl .ConfigView ;
2830import io .questdb .client .impl .QuestDBImpl ;
31+ import org .jetbrains .annotations .TestOnly ;
32+
33+ import java .util .function .IntConsumer ;
34+ import java .util .function .LongConsumer ;
2935
3036/**
3137 * Builder for {@link QuestDB}. Most callers use {@link QuestDB#connect(CharSequence)};
3238 * this builder is for pool sizing, idle/lifetime knobs, acquire timeout,
3339 * and the case where ingest and egress configs differ.
40+ * <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.
3446 */
3547public final class QuestDBBuilder {
3648
@@ -41,16 +53,21 @@ public final class QuestDBBuilder {
4153 static final int DEFAULT_POOL_MAX = 4 ;
4254 static final int DEFAULT_POOL_MIN = 1 ;
4355
44- private long acquireTimeoutMillis = DEFAULT_ACQUIRE_TIMEOUT_MILLIS ;
45- private long housekeeperIntervalMillis = DEFAULT_HOUSEKEEPER_INTERVAL_MILLIS ;
46- private long idleTimeoutMillis = DEFAULT_IDLE_TIMEOUT_MILLIS ;
56+ // Every valid pool value is >= 0, so -1 unambiguously marks "not set
57+ // explicitly". The public pool setters are the only writers of these
58+ // fields, so field != UNSET is exactly the "set explicitly" bit.
59+ private static final int UNSET = -1 ;
60+
61+ private long acquireTimeoutMillis = UNSET ;
62+ private long housekeeperIntervalMillis = UNSET ;
63+ private long idleTimeoutMillis = UNSET ;
4764 private String ingestConfig ;
48- private long maxLifetimeMillis = DEFAULT_MAX_LIFETIME_MILLIS ;
65+ private long maxLifetimeMillis = UNSET ;
4966 private String queryConfig ;
50- private int queryPoolMax = DEFAULT_POOL_MAX ;
51- private int queryPoolMin = DEFAULT_POOL_MIN ;
52- private int senderPoolMax = DEFAULT_POOL_MAX ;
53- private int senderPoolMin = DEFAULT_POOL_MIN ;
67+ private int queryPoolMax = UNSET ;
68+ private int queryPoolMin = UNSET ;
69+ private int senderPoolMax = UNSET ;
70+ private int senderPoolMin = UNSET ;
5471
5572 QuestDBBuilder () {
5673 }
@@ -69,7 +86,9 @@ public QuestDBBuilder acquireTimeoutMillis(long millis) {
6986 }
7087
7188 /**
72- * Builds the {@link QuestDB} handle. Eagerly creates {@code min}
89+ * Builds the {@link QuestDB} handle. Validates both connect strings up
90+ * front -- so a malformed config fails here even when both pools have
91+ * {@code min == 0} and nothing connects -- then eagerly creates {@code min}
7392 * connections in each pool; further slots are allocated lazily up to
7493 * {@code max} when load demands and reaped back to {@code min} when
7594 * idle.
@@ -88,6 +107,30 @@ public QuestDB build() {
88107 if (queryConfig == null ) {
89108 throw new IllegalStateException ("query configuration is required; call fromConfig() or queryConfig()" );
90109 }
110+ ConfigString ingestCs = ConfigString .parse (ingestConfig );
111+ ConfigString queryCs = ConfigString .parse (queryConfig );
112+ ConfigView ingestView = new ConfigView (ingestCs );
113+ ConfigView queryView = new ConfigView (queryCs );
114+ // Validate both connect strings exactly as the pools will, but without
115+ // connecting. The ingest string runs the full Sender parse plus
116+ // validateParameters -- ingress value keys are registry-STRING, so only
117+ // the real parse validates their values. The egress string runs the
118+ // typed validateConfig. A malformed config therefore fails here even
119+ // when a pool min is 0 and nothing connects.
120+ Sender .LineSenderBuilder .validateWsConfigString (ingestConfig );
121+ QwpQueryClient .validateConfig (queryView , "wss" .equals (queryCs .schema ()));
122+
123+ // A view carries no side; getInt/getLong read any key, so the ingest
124+ // and query views also serve the POOL reads.
125+ resolvePoolInt (senderPoolMin , "sender_pool_min" , ingestView , queryView , DEFAULT_POOL_MIN , this ::senderPoolMin );
126+ resolvePoolInt (senderPoolMax , "sender_pool_max" , ingestView , queryView , DEFAULT_POOL_MAX , this ::senderPoolMax );
127+ resolvePoolInt (queryPoolMin , "query_pool_min" , ingestView , queryView , DEFAULT_POOL_MIN , this ::queryPoolMin );
128+ resolvePoolInt (queryPoolMax , "query_pool_max" , ingestView , queryView , DEFAULT_POOL_MAX , this ::queryPoolMax );
129+ resolvePoolLong (acquireTimeoutMillis , "acquire_timeout_ms" , ingestView , queryView , DEFAULT_ACQUIRE_TIMEOUT_MILLIS , this ::acquireTimeoutMillis );
130+ resolvePoolLong (idleTimeoutMillis , "idle_timeout_ms" , ingestView , queryView , DEFAULT_IDLE_TIMEOUT_MILLIS , this ::idleTimeoutMillis );
131+ resolvePoolLong (maxLifetimeMillis , "max_lifetime_ms" , ingestView , queryView , DEFAULT_MAX_LIFETIME_MILLIS , this ::maxLifetimeMillis );
132+ resolvePoolLong (housekeeperIntervalMillis , "housekeeper_interval_ms" , ingestView , queryView , DEFAULT_HOUSEKEEPER_INTERVAL_MILLIS , this ::housekeeperIntervalMillis );
133+
91134 return new QuestDBImpl (
92135 ingestConfig ,
93136 queryConfig ,
@@ -103,42 +146,14 @@ public QuestDB build() {
103146 }
104147
105148 /**
106- * Sets a single unified configuration string used to derive both the
107- * ingest and the egress config. Schema must be {@code http}, {@code https},
108- * {@code ws} or {@code wss}; the other half is derived by schema
109- * translation.
149+ * Sets a single configuration string used for both ingest and egress. The
150+ * schema must be {@code ws} or {@code wss}.
110151 */
111152 public QuestDBBuilder fromConfig (CharSequence configurationString ) {
112- ConfigStringTranslator .Bundle bundle = ConfigStringTranslator .deriveBothSides (configurationString );
113- this .ingestConfig = bundle .ingestConfig ;
114- this .queryConfig = bundle .queryConfig ;
115- ConfigStringTranslator .PoolConfig pc = bundle .poolConfig ;
116- // Apply pool keys carried in the string. Explicit builder calls AFTER
117- // fromConfig() will overwrite these -- last write wins.
118- if (pc .senderPoolMin != ConfigStringTranslator .PoolConfig .UNSET ) {
119- senderPoolMin (pc .senderPoolMin );
120- }
121- if (pc .senderPoolMax != ConfigStringTranslator .PoolConfig .UNSET ) {
122- senderPoolMax (pc .senderPoolMax );
123- }
124- if (pc .queryPoolMin != ConfigStringTranslator .PoolConfig .UNSET ) {
125- queryPoolMin (pc .queryPoolMin );
126- }
127- if (pc .queryPoolMax != ConfigStringTranslator .PoolConfig .UNSET ) {
128- queryPoolMax (pc .queryPoolMax );
129- }
130- if (pc .acquireTimeoutMillis != ConfigStringTranslator .PoolConfig .UNSET ) {
131- acquireTimeoutMillis (pc .acquireTimeoutMillis );
132- }
133- if (pc .idleTimeoutMillis != ConfigStringTranslator .PoolConfig .UNSET ) {
134- idleTimeoutMillis (pc .idleTimeoutMillis );
135- }
136- if (pc .maxLifetimeMillis != ConfigStringTranslator .PoolConfig .UNSET ) {
137- maxLifetimeMillis (pc .maxLifetimeMillis );
138- }
139- if (pc .housekeeperIntervalMillis != ConfigStringTranslator .PoolConfig .UNSET ) {
140- housekeeperIntervalMillis (pc .housekeeperIntervalMillis );
141- }
153+ requireWebSocketSchema (configurationString , "connection" );
154+ String s = configurationString .toString ();
155+ this .ingestConfig = s ;
156+ this .queryConfig = s ;
142157 return this ;
143158 }
144159
@@ -169,9 +184,11 @@ public QuestDBBuilder idleTimeoutMillis(long millis) {
169184 }
170185
171186 /**
172- * Sets the ingest-side configuration in {@link Sender#fromConfig} format.
187+ * Sets the ingest-side configuration. The schema must be {@code ws} or
188+ * {@code wss}.
173189 */
174190 public QuestDBBuilder ingestConfig (CharSequence configurationString ) {
191+ requireWebSocketSchema (configurationString , "ingest" );
175192 this .ingestConfig = configurationString .toString ();
176193 return this ;
177194 }
@@ -190,11 +207,11 @@ public QuestDBBuilder maxLifetimeMillis(long millis) {
190207 }
191208
192209 /**
193- * Sets the query-side configuration in
194- * {@link io.questdb.client.cutlass.qwp.client.QwpQueryClient#fromConfig}
195- * format.
210+ * Sets the query-side configuration. The schema must be {@code ws} or
211+ * {@code wss}.
196212 */
197213 public QuestDBBuilder queryConfig (CharSequence configurationString ) {
214+ requireWebSocketSchema (configurationString , "query" );
198215 this .queryConfig = configurationString .toString ();
199216 return this ;
200217 }
@@ -272,4 +289,81 @@ public QuestDBBuilder senderPoolSize(int size) {
272289 this .senderPoolMax = size ;
273290 return this ;
274291 }
292+
293+ /**
294+ * Snapshot of the resolved pool config, keyed by connect-string key name.
295+ * Valid after {@link #build()} has run pool-key resolution. Drives the
296+ * per-key "honored" guard test.
297+ */
298+ @ TestOnly
299+ public java .util .Map <String , Object > poolConfigSnapshotForTest () {
300+ java .util .Map <String , Object > m = new java .util .HashMap <>();
301+ m .put ("sender_pool_min" , senderPoolMin );
302+ m .put ("sender_pool_max" , senderPoolMax );
303+ m .put ("query_pool_min" , queryPoolMin );
304+ m .put ("query_pool_max" , queryPoolMax );
305+ m .put ("acquire_timeout_ms" , acquireTimeoutMillis );
306+ m .put ("idle_timeout_ms" , idleTimeoutMillis );
307+ m .put ("max_lifetime_ms" , maxLifetimeMillis );
308+ m .put ("housekeeper_interval_ms" , housekeeperIntervalMillis );
309+ return m ;
310+ }
311+
312+ private static void requireWebSocketSchema (CharSequence config , String role ) {
313+ String schema = ConfigString .parse (config ).schema ();
314+ if (!"ws" .equals (schema ) && !"wss" .equals (schema )) {
315+ throw new IllegalArgumentException (
316+ role + " configuration must use the ws or wss schema; got: " + schema );
317+ }
318+ }
319+
320+ private void resolvePoolInt (int current , String key , ConfigView ingest , ConfigView query , int dflt , IntConsumer setter ) {
321+ if (current != UNSET ) {
322+ return ; // explicit builder call wins; skip the conflict check
323+ }
324+ boolean inIngest = ingest .has (key );
325+ boolean inQuery = query .has (key );
326+ int value ;
327+ if (inIngest && inQuery ) {
328+ int vi = ingest .getInt (key , UNSET );
329+ int vq = query .getInt (key , UNSET );
330+ if (vi != vq ) {
331+ throw new IllegalArgumentException (
332+ "conflicting pool config: " + key + " (ingest=" + vi + ", query=" + vq + ")" );
333+ }
334+ value = vi ;
335+ } else if (inIngest ) {
336+ value = ingest .getInt (key , UNSET );
337+ } else if (inQuery ) {
338+ value = query .getInt (key , UNSET );
339+ } else {
340+ value = dflt ;
341+ }
342+ setter .accept (value );
343+ }
344+
345+ private void resolvePoolLong (long current , String key , ConfigView ingest , ConfigView query , long dflt , LongConsumer setter ) {
346+ if (current != UNSET ) {
347+ return ; // explicit builder call wins; skip the conflict check
348+ }
349+ boolean inIngest = ingest .has (key );
350+ boolean inQuery = query .has (key );
351+ long value ;
352+ if (inIngest && inQuery ) {
353+ long vi = ingest .getLong (key , UNSET );
354+ long vq = query .getLong (key , UNSET );
355+ if (vi != vq ) {
356+ throw new IllegalArgumentException (
357+ "conflicting pool config: " + key + " (ingest=" + vi + ", query=" + vq + ")" );
358+ }
359+ value = vi ;
360+ } else if (inIngest ) {
361+ value = ingest .getLong (key , UNSET );
362+ } else if (inQuery ) {
363+ value = query .getLong (key , UNSET );
364+ } else {
365+ value = dflt ;
366+ }
367+ setter .accept (value );
368+ }
275369}
0 commit comments