Skip to content

Commit 7776bbf

Browse files
committed
bugfixes and tests
1 parent fa25ed1 commit 7776bbf

11 files changed

Lines changed: 465 additions & 120 deletions

File tree

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

Lines changed: 46 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -70,6 +70,10 @@ public abstract class WebSocketClient implements QuietCloseable {
7070

7171
private static final int DEFAULT_RECV_BUFFER_SIZE = 65536;
7272
private static final int DEFAULT_SEND_BUFFER_SIZE = 65536;
73+
// tryParseFrame() return values
74+
private static final int PARSE_INCOMPLETE = 0;
75+
private static final int PARSE_NEED_MORE = -1;
76+
private static final int PARSE_OK = 1;
7377
private static final Logger LOG = LoggerFactory.getLogger(WebSocketClient.class);
7478
private static final String QWP_VERSION_HEADER_NAME = "X-QWP-Version:";
7579
private static final ThreadLocal<MessageDigest> SHA1_DIGEST = ThreadLocal.withInitial(() -> {
@@ -129,21 +133,26 @@ public WebSocketClient(HttpClientConfiguration configuration, SocketFactory sock
129133
this.recvBufSize = Math.max(configuration.getResponseBufferSize(), DEFAULT_RECV_BUFFER_SIZE);
130134
this.maxRecvBufSize = Math.max(configuration.getMaximumResponseBufferSize(), recvBufSize);
131135
this.recvBufPtr = Unsafe.malloc(recvBufSize, MemoryTag.NATIVE_DEFAULT);
136+
137+
this.sendBuffer = sendBuf;
138+
this.controlFrameBuffer = controlBuf;
139+
this.recvPos = 0;
140+
this.recvReadPos = 0;
141+
142+
this.frameParser = new WebSocketFrameParser();
143+
this.rnd = new SecureRnd();
144+
this.upgraded = false;
145+
this.closed = false;
132146
} catch (Throwable t) {
147+
if (recvBufPtr != 0) {
148+
Unsafe.free(recvBufPtr, recvBufSize, MemoryTag.NATIVE_DEFAULT);
149+
recvBufPtr = 0;
150+
}
133151
Misc.free(controlBuf);
134152
Misc.free(sendBuf);
135153
Misc.free(socket);
136154
throw t;
137155
}
138-
this.sendBuffer = sendBuf;
139-
this.controlFrameBuffer = controlBuf;
140-
this.recvPos = 0;
141-
this.recvReadPos = 0;
142-
143-
this.frameParser = new WebSocketFrameParser();
144-
this.rnd = new SecureRnd();
145-
this.upgraded = false;
146-
this.closed = false;
147156
}
148157

149158
@Override
@@ -211,6 +220,9 @@ public void connect(CharSequence host, int port) {
211220
/**
212221
* Disconnects the socket without closing the client.
213222
* The client can be reconnected by calling connect() again.
223+
* <p>
224+
* This method is NOT thread-safe. Only call it when no other thread
225+
* is using this client (e.g., after the I/O thread has stopped).
214226
*/
215227
public void disconnect() {
216228
Misc.free(socket);
@@ -222,6 +234,17 @@ public void disconnect() {
222234
resetFragmentState();
223235
}
224236

237+
/**
238+
* Closes the socket to force-unblock a thread blocked in send/recv.
239+
* <p>
240+
* Unlike {@link #disconnect()}, this method only closes the socket
241+
* and does not reset client state. It is safe to call from a different
242+
* thread than the one performing I/O.
243+
*/
244+
public void forceDisconnect() {
245+
Misc.free(socket);
246+
}
247+
225248
/**
226249
* Returns the connected host.
227250
*/
@@ -278,9 +301,9 @@ public boolean receiveFrame(WebSocketFrameHandler handler, int timeout) {
278301
checkConnected();
279302

280303
// First, try to parse any data already in buffer
281-
Boolean result = tryParseFrame(handler);
282-
if (result != null) {
283-
return result;
304+
int result = tryParseFrame(handler);
305+
if (result != PARSE_NEED_MORE) {
306+
return result == PARSE_OK;
284307
}
285308

286309
// Need more data
@@ -303,8 +326,8 @@ public boolean receiveFrame(WebSocketFrameHandler handler, int timeout) {
303326
recvPos += bytesRead;
304327

305328
result = tryParseFrame(handler);
306-
if (result != null) {
307-
return result;
329+
if (result != PARSE_NEED_MORE) {
330+
return result == PARSE_OK;
308331
}
309332
}
310333
}
@@ -382,9 +405,9 @@ public boolean tryReceiveFrame(WebSocketFrameHandler handler) {
382405
checkConnected();
383406

384407
// First, try to parse any data already in buffer
385-
Boolean result = tryParseFrame(handler);
386-
if (result != null) {
387-
return result;
408+
int result = tryParseFrame(handler);
409+
if (result != PARSE_NEED_MORE) {
410+
return result == PARSE_OK;
388411
}
389412

390413
// Try one non-blocking recv
@@ -403,7 +426,7 @@ public boolean tryReceiveFrame(WebSocketFrameHandler handler) {
403426

404427
// Try to parse again
405428
result = tryParseFrame(handler);
406-
return result != null && result;
429+
return result == PARSE_OK;
407430
}
408431

409432
/**
@@ -800,17 +823,17 @@ private void sendPongFrame(long payloadPtr, int payloadLen) {
800823
}
801824
}
802825

803-
private Boolean tryParseFrame(WebSocketFrameHandler handler) {
826+
private int tryParseFrame(WebSocketFrameHandler handler) {
804827
if (recvPos <= recvReadPos) {
805-
return null; // No data
828+
return PARSE_NEED_MORE;
806829
}
807830

808831
frameParser.reset();
809832
int consumed = frameParser.parse(recvBufPtr + recvReadPos, recvBufPtr + recvPos);
810833

811834
if (frameParser.getState() == WebSocketFrameParser.STATE_NEED_MORE ||
812835
frameParser.getState() == WebSocketFrameParser.STATE_NEED_PAYLOAD) {
813-
return null; // Need more data
836+
return PARSE_NEED_MORE;
814837
}
815838

816839
if (frameParser.getState() == WebSocketFrameParser.STATE_ERROR) {
@@ -909,10 +932,10 @@ private Boolean tryParseFrame(WebSocketFrameHandler handler) {
909932
// Compact buffer if needed
910933
compactRecvBuffer();
911934

912-
return true;
935+
return PARSE_OK;
913936
}
914937

915-
return false;
938+
return PARSE_INCOMPLETE;
916939
}
917940

918941
private void validateUpgradeResponse(int headerEnd) {

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

Lines changed: 10 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -165,13 +165,6 @@ public FrameInfo endFrame(int opcode) {
165165
return frameInfo.set(actualFrameStart, actualHeaderSize + payloadLen);
166166
}
167167

168-
/**
169-
* Finishes the current text frame, writing the header and applying masking.
170-
*/
171-
public FrameInfo endTextFrame() {
172-
return endFrame(WebSocketOpcode.TEXT);
173-
}
174-
175168
/**
176169
* Ensures the buffer has capacity for the specified number of additional bytes.
177170
* May reallocate the buffer if necessary.
@@ -200,13 +193,6 @@ public int getCapacity() {
200193
return bufCapacity;
201194
}
202195

203-
/**
204-
* Gets the payload length of the current frame being built.
205-
*/
206-
public int getCurrentPayloadLength() {
207-
return writePos - payloadStartOffset;
208-
}
209-
210196
/**
211197
* Gets the current write position (number of bytes written).
212198
*/
@@ -215,6 +201,16 @@ public int getPosition() {
215201
return writePos;
216202
}
217203

204+
@Override
205+
public long getWriteAddress() {
206+
return bufPtr + writePos;
207+
}
208+
209+
@Override
210+
public int getWritableBytes() {
211+
return bufCapacity - writePos;
212+
}
213+
218214
/**
219215
* Gets the current write position (total bytes written since last reset).
220216
*/

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

Lines changed: 86 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -107,10 +107,16 @@ public void close() {
107107
*/
108108
@Override
109109
public void ensureCapacity(int needed) {
110-
if (position + needed > capacity) {
111-
int newCapacity = Math.max(capacity * 2, position + needed);
112-
bufferPtr = Unsafe.realloc(bufferPtr, capacity, newCapacity, MemoryTag.NATIVE_DEFAULT);
113-
capacity = newCapacity;
110+
if ((long) position + needed > capacity) {
111+
long required = (long) position + needed;
112+
long doubled = (long) capacity * 2;
113+
long newCapacity = Math.max(doubled, required);
114+
if (newCapacity > Integer.MAX_VALUE) {
115+
throw new OutOfMemoryError("NativeBufferWriter capacity overflow: " + newCapacity);
116+
}
117+
int cap = (int) newCapacity;
118+
bufferPtr = Unsafe.realloc(bufferPtr, capacity, cap, MemoryTag.NATIVE_DEFAULT);
119+
capacity = cap;
114120
}
115121
}
116122

@@ -138,6 +144,16 @@ public int getPosition() {
138144
return position;
139145
}
140146

147+
@Override
148+
public long getWriteAddress() {
149+
return bufferPtr + position;
150+
}
151+
152+
@Override
153+
public int getWritableBytes() {
154+
return capacity - position;
155+
}
156+
141157
/**
142158
* Patches an int value at the specified offset.
143159
* Used for updating length fields after writing content.
@@ -231,10 +247,35 @@ public void putString(String value) {
231247
return;
232248
}
233249

234-
int utf8Len = utf8Length(value);
235-
putVarint(utf8Len);
236-
ensureCapacity(utf8Len);
237-
encodeUtf8(value);
250+
int charLen = value.length();
251+
// Optimistic: assume ASCII (utf8Len == charLen).
252+
// Reserve varint(charLen) + charLen bytes.
253+
int varintLen = varintSize(charLen);
254+
ensureCapacity(varintLen + charLen);
255+
256+
// Single-pass: write ASCII bytes directly after varint space
257+
long varintAddr = bufferPtr + position;
258+
long addr = varintAddr + varintLen;
259+
int i = 0;
260+
for (; i < charLen; i++) {
261+
char c = value.charAt(i);
262+
if (c >= 0x80) {
263+
break;
264+
}
265+
Unsafe.getUnsafe().putByte(addr++, (byte) c);
266+
}
267+
268+
if (i == charLen) {
269+
// All ASCII — write varint prefix, done in a single pass
270+
writeVarintDirect(varintAddr, charLen);
271+
position += varintLen + charLen;
272+
} else {
273+
// Non-ASCII — fall back to two-pass
274+
int utf8Len = utf8Length(value);
275+
putVarint(utf8Len);
276+
ensureCapacity(utf8Len);
277+
encodeUtf8(value);
278+
}
238279
}
239280

240281
/**
@@ -245,21 +286,45 @@ public void putUtf8(String value) {
245286
if (value == null || value.isEmpty()) {
246287
return;
247288
}
248-
int utf8Len = utf8Length(value);
249-
ensureCapacity(utf8Len);
250-
encodeUtf8(value);
289+
290+
int charLen = value.length();
291+
ensureCapacity(charLen);
292+
293+
// Single-pass: try ASCII encoding
294+
long addr = bufferPtr + position;
295+
int i = 0;
296+
for (; i < charLen; i++) {
297+
char c = value.charAt(i);
298+
if (c >= 0x80) {
299+
break;
300+
}
301+
Unsafe.getUnsafe().putByte(addr++, (byte) c);
302+
}
303+
304+
if (i == charLen) {
305+
// All ASCII — done in a single pass
306+
position += charLen;
307+
} else {
308+
// Non-ASCII — fall back to two-pass (re-encodes from start)
309+
int utf8Len = utf8Length(value);
310+
ensureCapacity(utf8Len);
311+
encodeUtf8(value);
312+
}
251313
}
252314

253315
/**
254316
* Writes a varint (unsigned LEB128).
255317
*/
256318
@Override
257319
public void putVarint(long value) {
320+
ensureCapacity(10); // max varint bytes
321+
long addr = bufferPtr + position;
258322
while (value > 0x7F) {
259-
putByte((byte) ((value & 0x7F) | 0x80));
323+
Unsafe.getUnsafe().putByte(addr++, (byte) ((value & 0x7F) | 0x80));
260324
value >>>= 7;
261325
}
262-
putByte((byte) value);
326+
Unsafe.getUnsafe().putByte(addr++, (byte) value);
327+
position = (int) (addr - bufferPtr);
263328
}
264329

265330
/**
@@ -282,6 +347,14 @@ public void skip(int bytes) {
282347
position += bytes;
283348
}
284349

350+
private static void writeVarintDirect(long addr, long value) {
351+
while (value > 0x7F) {
352+
Unsafe.getUnsafe().putByte(addr++, (byte) ((value & 0x7F) | 0x80));
353+
value >>>= 7;
354+
}
355+
Unsafe.getUnsafe().putByte(addr, (byte) value);
356+
}
357+
285358
private void encodeUtf8(String value) {
286359
long addr = bufferPtr + position;
287360
for (int i = 0, n = value.length(); i < n; i++) {

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

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -69,6 +69,23 @@ public interface QwpBufferWriter extends ArrayBufferAppender {
6969
*/
7070
int getPosition();
7171

72+
/**
73+
* Returns the native address where the next write will go.
74+
* <p>
75+
* Unlike {@code getBufferPtr() + getPosition()}, this method returns
76+
* the correct write address for all buffer implementations, including
77+
* segmented buffers where {@link #getPosition()} is a global offset.
78+
* <p>
79+
* The returned pointer is valid until the next buffer growth or flush.
80+
*/
81+
long getWriteAddress();
82+
83+
/**
84+
* Returns the number of bytes available for writing at
85+
* {@link #getWriteAddress()}.
86+
*/
87+
int getWritableBytes();
88+
7289
/**
7390
* Patches an int value at the specified offset in the buffer.
7491
* <p>

0 commit comments

Comments
 (0)