Skip to content

Commit e34fc1c

Browse files
committed
Use a fixed deadline in the native connect timeout
awaitConnectComplete reset its time baseline (start = now) on every EINTR and subtracted only whole milliseconds of elapsed time. Under a high-frequency signal storm on the connecting thread -- e.g. a wall-clock profiler or interval timer interrupting the blocked poll() more than once per millisecond -- each interval truncated to 0 ms, so the budget never decremented and poll() was re-armed with the full timeout every iteration. The connect timeout could then extend well past its bound, or never fire at all, contradicting the comment that EINTR storms cannot extend it. Even at lower rates each interrupt discarded up to ~1 ms of accounting, drifting the timeout one-directionally. Compute one absolute monotonic deadline up front and derive the remaining poll() budget from it each iteration. The remaining time can only decrease, so the timeout is a strict upper bound regardless of interrupt frequency; truncation now only under-shoots the final poll by < 1 ms, which never extends the wait. The success, refused, and timeout paths are otherwise unchanged. Validated: NetConnectTimeoutTest (loopback success, refused-vs-timeout, black-hole timeout within budget) passes against the rebuilt library. A standalone harness driving the old vs new logic under a simulated 2.5 kHz EINTR storm confirms the old logic runs unbounded (aborted at >10x the budget) while the new logic returns at the budget. A deterministic regression test at the JNI layer is not practical: Java cannot target a POSIX signal at the specific connecting thread, and the only hanging- connect fixture available (TEST-NET-1 black-hole) is routing-dependent and already Assume-skipped on most runners. Rebuilds the committed linux-x86-64 libquestdb.so from the net.c change (mirrors the copy step in ci.yml); other platforms are refreshed by the rebuild-native-libs workflow.
1 parent da4bd1d commit e34fc1c

2 files changed

Lines changed: 31 additions & 16 deletions

File tree

core/src/main/c/share/net.c

Lines changed: 31 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -306,17 +306,42 @@ JNIEXPORT jint JNICALL Java_io_questdb_client_network_Net_connectAddrInfo
306306
// caller can read it via Os.errno()), or com_questdb_network_Net_ECONNTIMEOUT
307307
// on timeout.
308308
static jint awaitConnectComplete(int fd, jint timeout_millis) {
309-
struct timespec start;
310-
clock_gettime(CLOCK_MONOTONIC, &start);
309+
// Fix a single absolute deadline up front. Recomputing the remaining budget
310+
// against a moving baseline on each EINTR (reset start = now, then subtract
311+
// whole milliseconds) lets a high-frequency signal storm extend the timeout:
312+
// under sub-millisecond interrupts every interval truncates to 0 ms, the
313+
// budget never decrements, and poll is re-armed with the full budget each
314+
// time. A fixed deadline is immune to interrupt frequency -- the remaining
315+
// time can only ever decrease.
316+
struct timespec deadline;
317+
clock_gettime(CLOCK_MONOTONIC, &deadline);
318+
long budget_millis = timeout_millis > 0 ? timeout_millis : 0;
319+
deadline.tv_sec += budget_millis / 1000L;
320+
deadline.tv_nsec += (budget_millis % 1000L) * 1000000L;
321+
if (deadline.tv_nsec >= 1000000000L) {
322+
deadline.tv_sec += 1;
323+
deadline.tv_nsec -= 1000000000L;
324+
}
311325

312326
for (;;) {
327+
struct timespec now;
328+
clock_gettime(CLOCK_MONOTONIC, &now);
329+
// Remaining time until the deadline, truncated to whole milliseconds for
330+
// poll(). Truncation only ever under-shoots by < 1 ms (it never extends
331+
// the wait), which keeps the timeout a strict upper bound.
332+
long remaining_millis = (deadline.tv_sec - now.tv_sec) * 1000L
333+
+ (deadline.tv_nsec - now.tv_nsec) / 1000000L;
334+
if (remaining_millis <= 0) {
335+
errno = ETIMEDOUT;
336+
return com_questdb_network_Net_ECONNTIMEOUT;
337+
}
338+
313339
struct pollfd pfd;
314340
pfd.fd = fd;
315341
pfd.events = POLLOUT;
316342
pfd.revents = 0;
317343

318-
int wait_millis = timeout_millis > 0 ? timeout_millis : 0;
319-
int rc = poll(&pfd, 1, wait_millis);
344+
int rc = poll(&pfd, 1, (int) remaining_millis);
320345
if (rc > 0) {
321346
// The connect attempt has finished one way or another; the only
322347
// authoritative result is SO_ERROR (POLLOUT alone does not mean
@@ -339,18 +364,8 @@ static jint awaitConnectComplete(int fd, jint timeout_millis) {
339364
if (errno != EINTR) {
340365
return -1;
341366
}
342-
// Interrupted by a signal: recompute the remaining budget against a
343-
// monotonic clock so EINTR storms cannot extend the timeout, and retry.
344-
struct timespec now;
345-
clock_gettime(CLOCK_MONOTONIC, &now);
346-
long elapsed = (now.tv_sec - start.tv_sec) * 1000L
347-
+ (now.tv_nsec - start.tv_nsec) / 1000000L;
348-
start = now;
349-
timeout_millis = (jint) (timeout_millis - elapsed);
350-
if (timeout_millis <= 0) {
351-
errno = ETIMEDOUT;
352-
return com_questdb_network_Net_ECONNTIMEOUT;
353-
}
367+
// Interrupted by a signal: loop and recompute the remaining time against
368+
// the fixed deadline. EINTR storms cannot extend the timeout.
354369
}
355370
}
356371

Binary file not shown.

0 commit comments

Comments
 (0)