@@ -138,7 +138,7 @@ public class QwpWebSocketSender implements Sender {
138138 // Flow control
139139 private InFlightWindow inFlightWindow ;
140140 private int maxSentSchemaId = -1 ;
141- // Track highest symbol ID sent to server (for delta encoding)
141+ // Track the highest symbol ID sent to server (for delta encoding)
142142 // Once sent over TCP, server is guaranteed to receive it (or connection dies)
143143 private int maxSentSymbolId = -1 ;
144144 // Batch sequence counter (must match server's messageSequence)
@@ -147,7 +147,14 @@ public class QwpWebSocketSender implements Sender {
147147 // Async mode: pending row tracking
148148 private long pendingBytes ;
149149 private int pendingRowCount ;
150+ // Highest client sequence durably uploaded, tracked when durable-ack opt-in is enabled
151+ // and the server emits STATUS_DURABLE_ACK frames.
152+ private long highestDurableSequence = -1 ;
153+ // Opt-in: request server-side STATUS_DURABLE_ACK frames after WAL reaches object store.
154+ // Must be set before the first send; has no effect once the WebSocket upgrade has completed.
155+ private boolean requestDurableAck ;
150156 private boolean sawBinaryAck ;
157+ private boolean sawPong ;
151158 private WebSocketSendQueue sendQueue ;
152159
153160 private QwpWebSocketSender (
@@ -290,6 +297,33 @@ public static QwpWebSocketSender connect(
290297 return sender ;
291298 }
292299
300+ public static QwpWebSocketSender connect (
301+ String host ,
302+ int port ,
303+ ClientTlsConfiguration tlsConfig ,
304+ int autoFlushRows ,
305+ int autoFlushBytes ,
306+ long autoFlushIntervalNanos ,
307+ int inFlightWindowSize ,
308+ String authorizationHeader ,
309+ int maxSchemasPerConnection ,
310+ boolean requestDurableAck
311+ ) {
312+ QwpWebSocketSender sender = new QwpWebSocketSender (
313+ host , port , tlsConfig ,
314+ autoFlushRows , autoFlushBytes , autoFlushIntervalNanos ,
315+ inFlightWindowSize , authorizationHeader , maxSchemasPerConnection
316+ );
317+ try {
318+ sender .setRequestDurableAck (requestDurableAck );
319+ sender .ensureConnected ();
320+ } catch (Throwable t ) {
321+ sender .close ();
322+ throw t ;
323+ }
324+ return sender ;
325+ }
326+
293327 /**
294328 * Creates a sender without connecting. For testing only.
295329 * <p>
@@ -302,10 +336,14 @@ public static QwpWebSocketSender connect(
302336 * @return unconnected sender
303337 */
304338 public static QwpWebSocketSender createForTesting (String host , int port , int inFlightWindowSize ) {
339+ return createForTesting (host , port , inFlightWindowSize , null );
340+ }
341+
342+ public static QwpWebSocketSender createForTesting (String host , int port , int inFlightWindowSize , String authorizationHeader ) {
305343 return new QwpWebSocketSender (
306344 host , port , null ,
307345 DEFAULT_AUTO_FLUSH_ROWS , DEFAULT_AUTO_FLUSH_BYTES , DEFAULT_AUTO_FLUSH_INTERVAL_NANOS ,
308- inFlightWindowSize , null , DEFAULT_MAX_SCHEMAS_PER_CONNECTION
346+ inFlightWindowSize , authorizationHeader , DEFAULT_MAX_SCHEMAS_PER_CONNECTION
309347 );
310348 }
311349
@@ -779,6 +817,27 @@ public int getAutoFlushRows() {
779817 return autoFlushRows ;
780818 }
781819
820+ /**
821+ * Returns the highest client batch sequence acknowledged by the server
822+ * (committed to WAL), or -1 if no ACK has been received yet.
823+ */
824+ public long getHighestAckedSequence () {
825+ return inFlightWindow != null ? inFlightWindow .getHighestAckedSequence () : -1 ;
826+ }
827+
828+ /**
829+ * Returns the highest client batch sequence that the server has reported
830+ * as durably persisted to the object store via a STATUS_DURABLE_ACK frame,
831+ * or -1 if no durable ACK has been observed yet on this connection.
832+ * <p>
833+ * Only meaningful when the connection was opened with
834+ * {@link #setRequestDurableAck(boolean)} = true on a server where primary
835+ * replication is enabled; otherwise remains -1.
836+ */
837+ public long getHighestDurableSequence () {
838+ return sendQueue != null ? sendQueue .getHighestDurableSequence () : highestDurableSequence ;
839+ }
840+
782841 /**
783842 * Returns the max symbol ID sent to the server.
784843 * Once sent over TCP, server is guaranteed to receive it (or connection dies).
@@ -960,6 +1019,27 @@ public QwpWebSocketSender longColumn(CharSequence columnName, long value) {
9601019 return this ;
9611020 }
9621021
1022+ /**
1023+ * Sends a WebSocket PING and reads frames until the PONG comes back,
1024+ * processing any STATUS_DURABLE_ACK or STATUS_OK frames along the way.
1025+ * After this method returns, {@link #getHighestDurableSequence()} reflects
1026+ * the latest durable watermark reported by the server.
1027+ * <p>
1028+ * In async mode the PING is queued for the I/O thread; this method
1029+ * returns once the I/O thread has sent it and processed the response.
1030+ *
1031+ * @throws LineSenderException if the connection is closed or the ping times out
1032+ */
1033+ public void ping () {
1034+ checkNotClosed ();
1035+ ensureConnected ();
1036+ if (inFlightWindowSize > 1 ) {
1037+ sendQueue .pingAndDrain ();
1038+ } else {
1039+ syncPing ();
1040+ }
1041+ }
1042+
9631043 @ Override
9641044 public void reset () {
9651045 checkNotClosed ();
@@ -988,6 +1068,27 @@ public void setGorillaEnabled(boolean enabled) {
9881068 this .encoder .setGorillaEnabled (enabled );
9891069 }
9901070
1071+ /**
1072+ * Opts the connection in for STATUS_DURABLE_ACK frames. Must be called
1073+ * before any send operation — the flag is consulted once, during WebSocket
1074+ * upgrade. Setting this true on a server without primary replication
1075+ * enabled is a no-op: the server silently ignores the header.
1076+ * <p>
1077+ * Observe durable progress via {@link #getHighestDurableSequence()}.
1078+ *
1079+ * @throws LineSenderException if the connection is already established or closed
1080+ */
1081+ public void setRequestDurableAck (boolean enabled ) {
1082+ if (closed ) {
1083+ throw new LineSenderException ("Sender is closed" );
1084+ }
1085+ if (connected ) {
1086+ throw new LineSenderException (
1087+ "setRequestDurableAck must be called before the first send" );
1088+ }
1089+ this .requestDurableAck = enabled ;
1090+ }
1091+
9911092 /**
9921093 * Adds a SHORT column value to the current row.
9931094 *
@@ -1215,6 +1316,7 @@ private void ensureConnected() {
12151316 try {
12161317 client .setQwpMaxVersion (QwpConstants .MAX_SUPPORTED_VERSION );
12171318 client .setQwpClientId (QwpConstants .CLIENT_ID );
1319+ client .setQwpRequestDurableAck (requestDurableAck );
12181320 client .connect (host , port );
12191321 client .upgrade (WRITE_PATH , authorizationHeader );
12201322 } catch (Exception e ) {
@@ -1639,6 +1741,32 @@ private boolean shouldAutoFlush() {
16391741 return false ;
16401742 }
16411743
1744+ private void syncPing () {
1745+ client .sendPing (1000 );
1746+ long deadline = System .currentTimeMillis () + InFlightWindow .DEFAULT_TIMEOUT_MS ;
1747+ while (System .currentTimeMillis () < deadline ) {
1748+ sawPong = false ;
1749+ sawBinaryAck = false ;
1750+ boolean received = client .receiveFrame (ackHandler , 1000 );
1751+ if (received ) {
1752+ if (sawBinaryAck ) {
1753+ long sequence = ackResponse .getSequence ();
1754+ if (ackResponse .isDurableAck ()) {
1755+ if (sequence > highestDurableSequence ) {
1756+ highestDurableSequence = sequence ;
1757+ }
1758+ } else if (ackResponse .isSuccess ()) {
1759+ inFlightWindow .acknowledgeUpTo (sequence );
1760+ }
1761+ }
1762+ if (sawPong ) {
1763+ return ;
1764+ }
1765+ }
1766+ }
1767+ throw new LineSenderException ("Ping timed out" );
1768+ }
1769+
16421770 private long toMicros (long value , ChronoUnit unit ) {
16431771 switch (unit ) {
16441772 case NANOS :
@@ -1696,6 +1824,13 @@ private void waitForAck(long expectedSequence) {
16961824 return ; // Our batch was acknowledged (cumulative)
16971825 }
16981826 // Got ACK for lower sequence - continue waiting
1827+ } else if (ackResponse .isDurableAck ()) {
1828+ // Durable-upload watermark for opted-in connections. Record the highest
1829+ // value seen; this method does not block for durable acks — callers who
1830+ // need to wait on durability can poll getHighestDurableSequence().
1831+ if (sequence > highestDurableSequence ) {
1832+ highestDurableSequence = sequence ;
1833+ }
16991834 } else {
17001835 String errorMessage = ackResponse .getErrorMessage ();
17011836 LineSenderException error = new LineSenderException (
@@ -1746,5 +1881,10 @@ public void onBinaryMessage(long payloadPtr, int payloadLen) {
17461881 public void onClose (int code , String reason ) {
17471882 throw new LineSenderException ("WebSocket closed while waiting for ACK: " + reason );
17481883 }
1884+
1885+ @ Override
1886+ public void onPong (long payloadPtr , int payloadLen ) {
1887+ sender .sawPong = true ;
1888+ }
17491889 }
17501890}
0 commit comments