3535import io .questdb .client .cutlass .line .array .LongArray ;
3636import io .questdb .client .cutlass .qwp .protocol .QwpConstants ;
3737import io .questdb .client .cutlass .qwp .protocol .QwpTableBuffer ;
38+ import io .questdb .client .std .CharSequenceLongHashMap ;
3839import io .questdb .client .std .CharSequenceObjHashMap ;
3940import io .questdb .client .std .Chars ;
4041import io .questdb .client .std .Decimal128 ;
@@ -147,11 +148,8 @@ public class QwpWebSocketSender implements Sender {
147148 // Async mode: pending row tracking
148149 private long pendingBytes ;
149150 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.
151+ private final CharSequenceLongHashMap syncCommittedSeqTxns = new CharSequenceLongHashMap ();
152+ private final CharSequenceLongHashMap syncDurableSeqTxns = new CharSequenceLongHashMap ();
155153 private boolean requestDurableAck ;
156154 private boolean sawBinaryAck ;
157155 private boolean sawPong ;
@@ -818,24 +816,28 @@ public int getAutoFlushRows() {
818816 }
819817
820818 /**
821- * Returns the highest client batch sequence acknowledged by the server
822- * (committed to WAL) , or -1 if no ACK has been received yet.
819+ * Returns the highest seqTxn committed (written to WAL) for the given
820+ * table , or -1 if no commit has been acknowledged for that table yet.
823821 */
824- public long getHighestAckedSequence () {
825- return inFlightWindow != null ? inFlightWindow .getHighestAckedSequence () : -1 ;
822+ public long getHighestAckedSeqTxn (CharSequence tableName ) {
823+ if (sendQueue != null ) {
824+ return sendQueue .getCommittedSeqTxn (tableName .toString ());
825+ }
826+ return syncCommittedSeqTxns .get (tableName );
826827 }
827828
828829 /**
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>
830+ * Returns the highest seqTxn durably uploaded to object store for the
831+ * given table, or -1 if no durable ACK has been observed for that table.
833832 * Only meaningful when the connection was opened with
834833 * {@link #setRequestDurableAck(boolean)} = true on a server where primary
835- * replication is enabled; otherwise remains -1 .
834+ * replication is enabled.
836835 */
837- public long getHighestDurableSequence () {
838- return sendQueue != null ? sendQueue .getHighestDurableSequence () : highestDurableSequence ;
836+ public long getHighestDurableSeqTxn (CharSequence tableName ) {
837+ if (sendQueue != null ) {
838+ return sendQueue .getDurableSeqTxn (tableName .toString ());
839+ }
840+ return syncDurableSeqTxns .get (tableName );
839841 }
840842
841843 /**
@@ -1022,7 +1024,7 @@ public QwpWebSocketSender longColumn(CharSequence columnName, long value) {
10221024 /**
10231025 * Sends a WebSocket PING and reads frames until the PONG comes back,
10241026 * processing any STATUS_DURABLE_ACK or STATUS_OK frames along the way.
1025- * After this method returns, {@link #getHighestDurableSequence( )} reflects
1027+ * After this method returns, {@link #getHighestDurableSeqTxn(CharSequence )} reflects
10261028 * the latest durable watermark reported by the server.
10271029 * <p>
10281030 * In async mode the PING is queued for the I/O thread; this method
@@ -1074,7 +1076,7 @@ public void setGorillaEnabled(boolean enabled) {
10741076 * upgrade. Setting this true on a server without primary replication
10751077 * enabled is a no-op: the server silently ignores the header.
10761078 * <p>
1077- * Observe durable progress via {@link #getHighestDurableSequence( )}.
1079+ * Observe durable progress via {@link #getHighestDurableSeqTxn(CharSequence )}.
10781080 *
10791081 * @throws LineSenderException if the connection is already established or closed
10801082 */
@@ -1750,13 +1752,11 @@ private void syncPing() {
17501752 boolean received = client .receiveFrame (ackHandler , 1000 );
17511753 if (received ) {
17521754 if (sawBinaryAck ) {
1753- long sequence = ackResponse .getSequence ();
17541755 if (ackResponse .isDurableAck ()) {
1755- if (sequence > highestDurableSequence ) {
1756- highestDurableSequence = sequence ;
1757- }
1756+ updateSyncDurableSeqTxns ();
17581757 } else if (ackResponse .isSuccess ()) {
1759- inFlightWindow .acknowledgeUpTo (sequence );
1758+ inFlightWindow .acknowledgeUpTo (ackResponse .getSequence ());
1759+ updateSyncCommittedSeqTxns ();
17601760 }
17611761 }
17621762 if (sawPong ) {
@@ -1767,6 +1767,26 @@ private void syncPing() {
17671767 throw new LineSenderException ("Ping timed out" );
17681768 }
17691769
1770+ private void updateSyncCommittedSeqTxns () {
1771+ for (int i = 0 , n = ackResponse .getTableEntryCount (); i < n ; i ++) {
1772+ String name = ackResponse .getTableName (i );
1773+ long seqTxn = ackResponse .getTableSeqTxn (i );
1774+ if (seqTxn > syncCommittedSeqTxns .get (name )) {
1775+ syncCommittedSeqTxns .put (name , seqTxn );
1776+ }
1777+ }
1778+ }
1779+
1780+ private void updateSyncDurableSeqTxns () {
1781+ for (int i = 0 , n = ackResponse .getTableEntryCount (); i < n ; i ++) {
1782+ String name = ackResponse .getTableName (i );
1783+ long seqTxn = ackResponse .getTableSeqTxn (i );
1784+ if (seqTxn > syncDurableSeqTxns .get (name )) {
1785+ syncDurableSeqTxns .put (name , seqTxn );
1786+ }
1787+ }
1788+ }
1789+
17701790 private long toMicros (long value , ChronoUnit unit ) {
17711791 switch (unit ) {
17721792 case NANOS :
@@ -1812,26 +1832,20 @@ private void waitForAck(long expectedSequence) {
18121832 boolean received = client .receiveFrame (ackHandler , 1000 ); // 1 second timeout per read attempt
18131833
18141834 if (received ) {
1815- // Non-binary frames (e.g. ping/pong/text) are not ACKs.
18161835 if (!sawBinaryAck ) {
18171836 continue ;
18181837 }
1819- long sequence = ackResponse .getSequence ();
18201838 if (ackResponse .isSuccess ()) {
1821- // Cumulative ACK - acknowledge all batches up to this sequence
1839+ long sequence = ackResponse . getSequence ();
18221840 inFlightWindow .acknowledgeUpTo (sequence );
1841+ updateSyncCommittedSeqTxns ();
18231842 if (sequence >= expectedSequence ) {
1824- return ; // Our batch was acknowledged (cumulative)
1843+ return ;
18251844 }
1826- // Got ACK for lower sequence - continue waiting
18271845 } 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- }
1846+ updateSyncDurableSeqTxns ();
18341847 } else {
1848+ long sequence = ackResponse .getSequence ();
18351849 String errorMessage = ackResponse .getErrorMessage ();
18361850 LineSenderException error = new LineSenderException (
18371851 "Server error for batch " + sequence + ": " +
0 commit comments