@@ -729,7 +729,9 @@ public int getTimeout() {
729729 // build() time. 0 or negative is a documented "disable" value, so
730730 // a Long.MIN_VALUE sentinel keeps it distinguishable from "unset".
731731 private static final long DURABLE_ACK_KEEPALIVE_NOT_SET = Long .MIN_VALUE ;
732+ private long authTimeoutMillis = QwpWebSocketSender .DEFAULT_AUTH_TIMEOUT_MS ;
732733 private long durableAckKeepaliveIntervalMillis = DURABLE_ACK_KEEPALIVE_NOT_SET ;
734+ private boolean gorillaEnabled = true ;
733735 // Drives the initial-connect strategy. OFF is fail-fast (default).
734736 // SYNC retries on the user thread up to the reconnect cap. ASYNC
735737 // returns immediately and lets the I/O thread retry in the
@@ -1029,8 +1031,8 @@ public Sender build() {
10291031 }
10301032
10311033 if (protocol == PROTOCOL_WEBSOCKET ) {
1032- if (hosts .size () != 1 || ports .size () != 1 ) {
1033- throw new LineSenderException ("only a single address ( host:port) is supported for WebSocket transport " );
1034+ if (hosts .size () < 1 || ports .size () != hosts . size () ) {
1035+ throw new LineSenderException ("WebSocket transport requires at least one host:port pair " );
10341036 }
10351037
10361038 int actualAutoFlushRows = autoFlushRows == PARAMETER_NOT_SET_EXPLICITLY ? DEFAULT_WS_AUTO_FLUSH_ROWS : autoFlushRows ;
@@ -1134,11 +1136,15 @@ public Sender build() {
11341136 int actualErrorInboxCapacity = errorInboxCapacity != PARAMETER_NOT_SET_EXPLICITLY
11351137 ? errorInboxCapacity
11361138 : io .questdb .client .cutlass .qwp .client .sf .cursor .SenderErrorDispatcher .DEFAULT_CAPACITY ;
1139+ java .util .List <QwpWebSocketSender .Endpoint > wsEndpoints =
1140+ new java .util .ArrayList <>(hosts .size ());
1141+ for (int i = 0 , n = hosts .size (); i < n ; i ++) {
1142+ wsEndpoints .add (new QwpWebSocketSender .Endpoint (hosts .getQuick (i ), ports .getQuick (i )));
1143+ }
11371144 QwpWebSocketSender connected ;
11381145 try {
11391146 connected = QwpWebSocketSender .connect (
1140- hosts .getQuick (0 ),
1141- ports .getQuick (0 ),
1147+ wsEndpoints ,
11421148 wsTlsConfig ,
11431149 actualAutoFlushRows ,
11441150 actualAutoFlushBytes ,
@@ -1155,7 +1161,8 @@ public Sender build() {
11551161 initialConnectMode ,
11561162 errorHandler ,
11571163 actualErrorInboxCapacity ,
1158- actualDurableAckKeepaliveIntervalMillis
1164+ actualDurableAckKeepaliveIntervalMillis ,
1165+ authTimeoutMillis
11591166 );
11601167 } catch (Throwable t ) {
11611168 // connect() failed before ownership of cursorEngine
@@ -1167,6 +1174,7 @@ public Sender build() {
11671174 }
11681175 throw t ;
11691176 }
1177+ connected .setGorillaEnabled (gorillaEnabled );
11701178 // connect() succeeded — `connected` now owns cursorEngine
11711179 // via setCursorEngine(engine, true). From here on, ANY
11721180 // failure must close `connected` (which closes the engine
@@ -1971,6 +1979,30 @@ public LineSenderBuilder durableAckKeepaliveIntervalMillis(long millis) {
19711979 return this ;
19721980 }
19731981
1982+ /**
1983+ * Per-endpoint timeout on the WebSocket upgrade response read. Default
1984+ * {@value QwpWebSocketSender#DEFAULT_AUTH_TIMEOUT_MS} ms.
1985+ */
1986+ public LineSenderBuilder authTimeoutMillis (long millis ) {
1987+ if (protocol != PARAMETER_NOT_SET_EXPLICITLY && protocol != PROTOCOL_WEBSOCKET ) {
1988+ throw new LineSenderException (
1989+ "auth_timeout is only supported for WebSocket transport" );
1990+ }
1991+ if (millis <= 0L ) {
1992+ throw new LineSenderException ("auth_timeout must be > 0: " ).put (millis );
1993+ }
1994+ this .authTimeoutMillis = millis ;
1995+ return this ;
1996+ }
1997+
1998+ public LineSenderBuilder gorilla (boolean enabled ) {
1999+ if (protocol != PARAMETER_NOT_SET_EXPLICITLY && protocol != PROTOCOL_WEBSOCKET ) {
2000+ throw new LineSenderException ("gorilla is only supported for WebSocket transport" );
2001+ }
2002+ this .gorillaEnabled = enabled ;
2003+ return this ;
2004+ }
2005+
19742006 /**
19752007 * Per-outage cap on the cursor I/O loop's reconnect retry budget.
19762008 * Once a wire failure occurs, the loop retries with exponential
@@ -2666,6 +2698,24 @@ private LineSenderBuilder fromConfig(CharSequence configurationString) {
26662698 }
26672699 pos = getValue (configurationString , pos , sink , "close_flush_timeout_millis" );
26682700 closeFlushTimeoutMillis (parseLongValue (sink , "close_flush_timeout_millis" ));
2701+ } else if (Chars .equals ("auth_timeout" , sink )) {
2702+ if (protocol != PROTOCOL_WEBSOCKET ) {
2703+ throw new LineSenderException ("auth_timeout is only supported for WebSocket transport" );
2704+ }
2705+ pos = getValue (configurationString , pos , sink , "auth_timeout" );
2706+ authTimeoutMillis (parseLongValue (sink , "auth_timeout" ));
2707+ } else if (Chars .equals ("gorilla" , sink )) {
2708+ if (protocol != PROTOCOL_WEBSOCKET ) {
2709+ throw new LineSenderException ("gorilla is only supported for WebSocket transport" );
2710+ }
2711+ pos = getValue (configurationString , pos , sink , "gorilla" );
2712+ if (Chars .equals ("on" , sink ) || Chars .equals ("true" , sink )) {
2713+ gorilla (true );
2714+ } else if (Chars .equals ("off" , sink ) || Chars .equals ("false" , sink )) {
2715+ gorilla (false );
2716+ } else {
2717+ throw new LineSenderException ("invalid gorilla [value=" ).put (sink ).put (", allowed=[on, off]]" );
2718+ }
26692719 } else if (Chars .equals ("durable_ack_keepalive_interval_millis" , sink )) {
26702720 if (protocol != PROTOCOL_WEBSOCKET ) {
26712721 throw new LineSenderException (
0 commit comments