Skip to content

Commit 4ec260f

Browse files
authored
Merge branch 'main' into qwp_query_reset_dict_flag
2 parents 229045a + 84da57d commit 4ec260f

30 files changed

Lines changed: 2930 additions & 1426 deletions

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

Lines changed: 6 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -59,16 +59,15 @@ static QuestDBBuilder builder() {
5959

6060
/**
6161
* Connects with a single configuration string used for both ingest and
62-
* egress. The schema must be {@code http}, {@code https}, {@code ws} or
63-
* {@code wss}; the other half of the deployment is derived by schema
64-
* translation ({@code http}<->{@code ws}, {@code https}<->{@code wss}).
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.
6565
* <p>
6666
* Use {@link #connect(CharSequence, CharSequence)} or {@link #builder()}
67-
* for ingest transports other than HTTP/HTTPS, or when ingest and egress
68-
* use different addresses.
67+
* when ingest and egress use different addresses or credentials.
6968
*
70-
* @param configurationString a Sender- or QwpQueryClient-style config
71-
* string (see {@link Sender#fromConfig} or
69+
* @param configurationString a {@code ws}/{@code wss} config string (see
70+
* {@link Sender#fromConfig} or
7271
* {@link io.questdb.client.cutlass.qwp.client.QwpQueryClient#fromConfig})
7372
* @return a connected QuestDB handle
7473
*/

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

Lines changed: 142 additions & 48 deletions
Original file line numberDiff line numberDiff line change
@@ -24,13 +24,25 @@
2424

2525
package 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;
2830
import 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
*/
3547
public 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

Comments
 (0)