Skip to content

Commit 5d3a4a1

Browse files
committed
upload ack per table
1 parent 7012fad commit 5d3a4a1

8 files changed

Lines changed: 449 additions & 175 deletions

File tree

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

Lines changed: 48 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,7 @@
3535
import io.questdb.client.cutlass.line.array.LongArray;
3636
import io.questdb.client.cutlass.qwp.protocol.QwpConstants;
3737
import io.questdb.client.cutlass.qwp.protocol.QwpTableBuffer;
38+
import io.questdb.client.std.CharSequenceLongHashMap;
3839
import io.questdb.client.std.CharSequenceObjHashMap;
3940
import io.questdb.client.std.Chars;
4041
import 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

Comments
 (0)