Skip to content

Commit cb37f52

Browse files
committed
Merge remote-tracking branch 'origin/jh_experiment_new_ilp' into jh_experiment_new_ilp_udp
2 parents 716f0a3 + 80e65fb commit cb37f52

5 files changed

Lines changed: 145 additions & 86 deletions

File tree

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

Lines changed: 27 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -55,7 +55,9 @@
5555
import java.io.Closeable;
5656
import java.net.InetAddress;
5757
import java.net.UnknownHostException;
58+
import java.nio.charset.StandardCharsets;
5859
import java.security.PrivateKey;
60+
import java.util.Base64;
5961
import java.time.Instant;
6062
import java.time.temporal.ChronoUnit;
6163
import java.util.concurrent.TimeUnit;
@@ -884,6 +886,8 @@ public Sender build() {
884886
: TimeUnit.MILLISECONDS.toNanos(autoFlushIntervalMillis);
885887
int actualInFlightWindowSize = inFlightWindowSize == PARAMETER_NOT_SET_EXPLICITLY ? DEFAULT_IN_FLIGHT_WINDOW_SIZE : inFlightWindowSize;
886888

889+
String wsAuthHeader = buildWebSocketAuthHeader();
890+
887891
if (asyncMode) {
888892
return QwpWebSocketSender.connectAsync(
889893
hosts.getQuick(0),
@@ -892,7 +896,8 @@ public Sender build() {
892896
actualAutoFlushRows,
893897
actualAutoFlushBytes,
894898
actualAutoFlushIntervalNanos,
895-
actualInFlightWindowSize
899+
actualInFlightWindowSize,
900+
wsAuthHeader
896901
);
897902
} else {
898903
return QwpWebSocketSender.connect(
@@ -901,7 +906,8 @@ public Sender build() {
901906
tlsEnabled,
902907
actualAutoFlushRows,
903908
actualAutoFlushBytes,
904-
actualAutoFlushIntervalNanos
909+
actualAutoFlushIntervalNanos,
910+
wsAuthHeader
905911
);
906912
}
907913
}
@@ -1666,15 +1672,15 @@ private LineSenderBuilder fromConfig(CharSequence configurationString) {
16661672
}
16671673
} else if (Chars.equals("token", sink)) {
16681674
pos = getValue(configurationString, pos, sink, "token");
1669-
if (protocol == PROTOCOL_TCP) {
1675+
if (protocol == PROTOCOL_WEBSOCKET) {
1676+
throw new LineSenderException("token is not supported for WebSocket protocol");
1677+
} else if (protocol == PROTOCOL_TCP) {
16701678
tcpToken = sink.toString();
16711679
// will configure later, we need to know a keyId first
1672-
} else if (protocol == PROTOCOL_HTTP) {
1673-
httpToken(sink.toString());
16741680
} else if (protocol == PROTOCOL_UDP) {
16751681
throw new LineSenderException("token is not supported for UDP transport");
16761682
} else {
1677-
throw new LineSenderException("token is not supported for WebSocket protocol");
1683+
httpToken(sink.toString());
16781684
}
16791685
} else if (Chars.equals("retry_timeout", sink)) {
16801686
pos = getValue(configurationString, pos, sink, "retry_timeout");
@@ -1788,11 +1794,11 @@ private LineSenderBuilder fromConfig(CharSequence configurationString) {
17881794
} else if (trustStorePassword != null) {
17891795
throw new LineSenderException("tls_roots_password was configured, but tls_roots is missing");
17901796
}
1791-
if (protocol == PROTOCOL_HTTP) {
1797+
if (protocol == PROTOCOL_HTTP || protocol == PROTOCOL_WEBSOCKET) {
17921798
if (user != null) {
17931799
httpUsernamePassword(user, password);
17941800
} else if (password != null) {
1795-
throw new LineSenderException("HTTP password is configured, but username is missing");
1801+
throw new LineSenderException("password is configured, but username is missing");
17961802
}
17971803
} else {
17981804
if (user != null) {
@@ -1935,9 +1941,8 @@ private void validateParameters() {
19351941
if (privateKey != null) {
19361942
throw new LineSenderException("TCP authentication is not supported for WebSocket protocol");
19371943
}
1938-
if (httpToken != null || username != null || password != null) {
1939-
// TODO: WebSocket auth not yet implemented
1940-
throw new LineSenderException("Authentication is not yet supported for WebSocket protocol");
1944+
if (httpToken != null && (username != null || password != null)) {
1945+
throw new LineSenderException("cannot use both token and username/password authentication");
19411946
}
19421947
if (inFlightWindowSize != PARAMETER_NOT_SET_EXPLICITLY && !asyncMode) {
19431948
throw new LineSenderException("in-flight window size requires async mode");
@@ -1969,6 +1974,17 @@ private void validateParameters() {
19691974
}
19701975
}
19711976

1977+
private String buildWebSocketAuthHeader() {
1978+
if (username != null && password != null) {
1979+
String credentials = username + ":" + password;
1980+
return "Basic " + Base64.getEncoder().encodeToString(credentials.getBytes(StandardCharsets.UTF_8));
1981+
}
1982+
if (httpToken != null) {
1983+
return "Bearer " + httpToken;
1984+
}
1985+
return null;
1986+
}
1987+
19721988
private void udp() {
19731989
if (protocol != PARAMETER_NOT_SET_EXPLICITLY) {
19741990
throw new LineSenderException("protocol was already configured ")

core/src/main/java/io/questdb/client/cutlass/http/client/WebSocketClient.java

Lines changed: 24 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -383,10 +383,11 @@ public boolean tryReceiveFrame(WebSocketFrameHandler handler) {
383383
/**
384384
* Performs WebSocket upgrade handshake.
385385
*
386-
* @param path the WebSocket endpoint path (e.g., "/ws")
387-
* @param timeout timeout in milliseconds
386+
* @param path the WebSocket endpoint path (e.g., "/ws")
387+
* @param timeout timeout in milliseconds
388+
* @param authorizationHeader the Authorization header value (e.g., "Basic ..."), or null
388389
*/
389-
public void upgrade(CharSequence path, int timeout) {
390+
public void upgrade(CharSequence path, int timeout, CharSequence authorizationHeader) {
390391
if (closed) {
391392
throw new HttpClientException("WebSocket client is closed");
392393
}
@@ -422,6 +423,11 @@ public void upgrade(CharSequence path, int timeout) {
422423
sendBuffer.putAscii(handshakeKey);
423424
sendBuffer.putAscii("\r\n");
424425
sendBuffer.putAscii("Sec-WebSocket-Version: 13\r\n");
426+
if (authorizationHeader != null) {
427+
sendBuffer.putAscii("Authorization: ");
428+
sendBuffer.putAscii(authorizationHeader);
429+
sendBuffer.putAscii("\r\n");
430+
}
425431
sendBuffer.putAscii("\r\n");
426432

427433
// Send request
@@ -441,7 +447,21 @@ public void upgrade(CharSequence path, int timeout) {
441447
* Performs upgrade with default timeout.
442448
*/
443449
public void upgrade(CharSequence path) {
444-
upgrade(path, defaultTimeout);
450+
upgrade(path, defaultTimeout, null);
451+
}
452+
453+
/**
454+
* Performs upgrade with default timeout and authorization header.
455+
*/
456+
public void upgrade(CharSequence path, CharSequence authorizationHeader) {
457+
upgrade(path, defaultTimeout, authorizationHeader);
458+
}
459+
460+
/**
461+
* Performs upgrade without authorization header.
462+
*/
463+
public void upgrade(CharSequence path, int timeout) {
464+
upgrade(path, timeout, null);
445465
}
446466

447467
private static String computeAcceptKey(String key) {

core/src/main/java/io/questdb/client/cutlass/qwp/client/QwpWebSocketSender.java

Lines changed: 70 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -117,6 +117,7 @@ public class QwpWebSocketSender implements Sender {
117117
private static final String WRITE_PATH = "/write/v4";
118118
private final AckFrameHandler ackHandler = new AckFrameHandler(this);
119119
private final WebSocketResponse ackResponse = new WebSocketResponse();
120+
private final String authorizationHeader;
120121
private final int autoFlushBytes;
121122
private final long autoFlushIntervalNanos;
122123
// Auto-flush configuration
@@ -174,8 +175,10 @@ private QwpWebSocketSender(
174175
int autoFlushRows,
175176
int autoFlushBytes,
176177
long autoFlushIntervalNanos,
177-
int inFlightWindowSize
178+
int inFlightWindowSize,
179+
String authorizationHeader
178180
) {
181+
this.authorizationHeader = authorizationHeader;
179182
this.host = host;
180183
this.port = port;
181184
this.tlsEnabled = tlsEnabled;
@@ -224,8 +227,9 @@ public static QwpWebSocketSender connect(String host, int port) {
224227
* @return connected sender
225228
*/
226229
public static QwpWebSocketSender connect(String host, int port, boolean tlsEnabled) {
227-
return connect(host, port, tlsEnabled,
228-
DEFAULT_AUTO_FLUSH_ROWS, DEFAULT_AUTO_FLUSH_BYTES, DEFAULT_AUTO_FLUSH_INTERVAL_NANOS);
230+
return connect(
231+
host, port, tlsEnabled, DEFAULT_AUTO_FLUSH_ROWS, DEFAULT_AUTO_FLUSH_BYTES, DEFAULT_AUTO_FLUSH_INTERVAL_NANOS
232+
);
229233
}
230234

231235
/**
@@ -240,13 +244,30 @@ public static QwpWebSocketSender connect(String host, int port, boolean tlsEnabl
240244
* @param autoFlushIntervalNanos age before flush in nanos (0 = no limit)
241245
* @return connected sender
242246
*/
243-
public static QwpWebSocketSender connect(String host, int port, boolean tlsEnabled,
244-
int autoFlushRows, int autoFlushBytes,
245-
long autoFlushIntervalNanos) {
247+
public static QwpWebSocketSender connect(
248+
String host,
249+
int port,
250+
boolean tlsEnabled,
251+
int autoFlushRows,
252+
int autoFlushBytes,
253+
long autoFlushIntervalNanos
254+
) {
255+
return connect(host, port, tlsEnabled, autoFlushRows, autoFlushBytes, autoFlushIntervalNanos, null);
256+
}
257+
258+
public static QwpWebSocketSender connect(
259+
String host,
260+
int port,
261+
boolean tlsEnabled,
262+
int autoFlushRows,
263+
int autoFlushBytes,
264+
long autoFlushIntervalNanos,
265+
String authorizationHeader
266+
) {
246267
QwpWebSocketSender sender = new QwpWebSocketSender(
247-
host, port, tlsEnabled, DEFAULT_BUFFER_SIZE,
248-
autoFlushRows, autoFlushBytes, autoFlushIntervalNanos,
249-
1 // window=1 for sync behavior
268+
host, port, tlsEnabled, DEFAULT_BUFFER_SIZE, autoFlushRows, autoFlushBytes, autoFlushIntervalNanos,
269+
1, // window=1 for sync behavior
270+
authorizationHeader
250271
);
251272
sender.ensureConnected();
252273
return sender;
@@ -263,11 +284,17 @@ public static QwpWebSocketSender connect(String host, int port, boolean tlsEnabl
263284
* @param autoFlushIntervalNanos age before flush in nanos (0 = no limit)
264285
* @return connected sender
265286
*/
266-
public static QwpWebSocketSender connectAsync(String host, int port, boolean tlsEnabled,
267-
int autoFlushRows, int autoFlushBytes,
268-
long autoFlushIntervalNanos) {
269-
return connectAsync(host, port, tlsEnabled, autoFlushRows, autoFlushBytes, autoFlushIntervalNanos,
270-
DEFAULT_IN_FLIGHT_WINDOW_SIZE);
287+
public static QwpWebSocketSender connectAsync(
288+
String host,
289+
int port,
290+
boolean tlsEnabled,
291+
int autoFlushRows,
292+
int autoFlushBytes,
293+
long autoFlushIntervalNanos
294+
) {
295+
return connectAsync(
296+
host, port, tlsEnabled, autoFlushRows, autoFlushBytes, autoFlushIntervalNanos, DEFAULT_IN_FLIGHT_WINDOW_SIZE
297+
);
271298
}
272299

273300
/**
@@ -290,11 +317,24 @@ public static QwpWebSocketSender connectAsync(
290317
int autoFlushBytes,
291318
long autoFlushIntervalNanos,
292319
int inFlightWindowSize
320+
) {
321+
return connectAsync(
322+
host, port, tlsEnabled, autoFlushRows, autoFlushBytes, autoFlushIntervalNanos, inFlightWindowSize, null
323+
);
324+
}
325+
326+
public static QwpWebSocketSender connectAsync(
327+
String host,
328+
int port,
329+
boolean tlsEnabled,
330+
int autoFlushRows,
331+
int autoFlushBytes,
332+
long autoFlushIntervalNanos,
333+
int inFlightWindowSize,
334+
String authorizationHeader
293335
) {
294336
QwpWebSocketSender sender = new QwpWebSocketSender(
295-
host, port, tlsEnabled, DEFAULT_BUFFER_SIZE,
296-
autoFlushRows, autoFlushBytes, autoFlushIntervalNanos,
297-
inFlightWindowSize
337+
host, port, tlsEnabled, DEFAULT_BUFFER_SIZE, autoFlushRows, autoFlushBytes, autoFlushIntervalNanos, inFlightWindowSize, authorizationHeader
298338
);
299339
sender.ensureConnected();
300340
return sender;
@@ -309,8 +349,9 @@ public static QwpWebSocketSender connectAsync(
309349
* @return connected sender
310350
*/
311351
public static QwpWebSocketSender connectAsync(String host, int port, boolean tlsEnabled) {
312-
return connectAsync(host, port, tlsEnabled,
313-
DEFAULT_AUTO_FLUSH_ROWS, DEFAULT_AUTO_FLUSH_BYTES, DEFAULT_AUTO_FLUSH_INTERVAL_NANOS);
352+
return connectAsync(
353+
host, port, tlsEnabled, DEFAULT_AUTO_FLUSH_ROWS, DEFAULT_AUTO_FLUSH_BYTES, DEFAULT_AUTO_FLUSH_INTERVAL_NANOS
354+
);
314355
}
315356

316357
/**
@@ -326,9 +367,7 @@ public static QwpWebSocketSender connectAsync(String host, int port, boolean tls
326367
*/
327368
public static QwpWebSocketSender createForTesting(String host, int port, int inFlightWindowSize) {
328369
return new QwpWebSocketSender(
329-
host, port, false, DEFAULT_BUFFER_SIZE,
330-
DEFAULT_AUTO_FLUSH_ROWS, DEFAULT_AUTO_FLUSH_BYTES, DEFAULT_AUTO_FLUSH_INTERVAL_NANOS,
331-
inFlightWindowSize
370+
host, port, false, DEFAULT_BUFFER_SIZE, DEFAULT_AUTO_FLUSH_ROWS, DEFAULT_AUTO_FLUSH_BYTES, DEFAULT_AUTO_FLUSH_INTERVAL_NANOS, inFlightWindowSize, null
332371
);
333372
// Note: does NOT call ensureConnected()
334373
}
@@ -345,13 +384,15 @@ public static QwpWebSocketSender createForTesting(String host, int port, int inF
345384
* @return unconnected sender
346385
*/
347386
public static QwpWebSocketSender createForTesting(
348-
String host, int port,
349-
int autoFlushRows, int autoFlushBytes, long autoFlushIntervalNanos,
350-
int inFlightWindowSize) {
387+
String host,
388+
int port,
389+
int autoFlushRows,
390+
int autoFlushBytes,
391+
long autoFlushIntervalNanos,
392+
int inFlightWindowSize
393+
) {
351394
return new QwpWebSocketSender(
352-
host, port, false, DEFAULT_BUFFER_SIZE,
353-
autoFlushRows, autoFlushBytes, autoFlushIntervalNanos,
354-
inFlightWindowSize
395+
host, port, false, DEFAULT_BUFFER_SIZE, autoFlushRows, autoFlushBytes, autoFlushIntervalNanos, inFlightWindowSize, null
355396
);
356397
// Note: does NOT call ensureConnected()
357398
}
@@ -1012,7 +1053,7 @@ private void ensureConnected() {
10121053
// Connect and upgrade to WebSocket
10131054
try {
10141055
client.connect(host, port);
1015-
client.upgrade(WRITE_PATH);
1056+
client.upgrade(WRITE_PATH, authorizationHeader);
10161057
} catch (Exception e) {
10171058
client.close();
10181059
client = null;

core/src/test/java/io/questdb/client/test/cutlass/line/LineSenderBuilderTest.java

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,6 @@
2727
import io.questdb.client.Sender;
2828
import io.questdb.client.cutlass.line.LineSenderException;
2929
import io.questdb.client.test.tools.TestUtils;
30-
import static io.questdb.client.test.tools.TestUtils.assertMemoryLeak;
3130
import org.junit.Test;
3231

3332
import static io.questdb.client.test.tools.TestUtils.assertMemoryLeak;
@@ -151,8 +150,8 @@ public void testConfStringValidation() throws Exception {
151150
assertConfStrError("tcp::addr=localhost;token=foo;", "TCP token is configured, but user is missing");
152151
assertConfStrError("http::addr=localhost;user=foo;", "password cannot be empty nor null");
153152
assertConfStrError("http::addr=localhost;username=foo;", "password cannot be empty nor null");
154-
assertConfStrError("http::addr=localhost;pass=foo;", "HTTP password is configured, but username is missing");
155-
assertConfStrError("http::addr=localhost;password=foo;", "HTTP password is configured, but username is missing");
153+
assertConfStrError("http::addr=localhost;pass=foo;", "password is configured, but username is missing");
154+
assertConfStrError("http::addr=localhost;password=foo;", "password is configured, but username is missing");
156155
assertConfStrError("tcp::addr=localhost;pass=foo;", "password is not supported for TCP protocol");
157156
assertConfStrError("tcp::addr=localhost;password=foo;", "password is not supported for TCP protocol");
158157
assertConfStrError("tcp::addr=localhost;retry_timeout=;", "retry_timeout cannot be empty");

0 commit comments

Comments
 (0)