Skip to content

feat(q): q_connect arms TCP keepalive, as the core's client does - #24

Merged
protocolstardust merged 2 commits into
RayforceDB:masterfrom
ihrfv:feat/q-connect-keepalive
Oct 24, 2026
Merged

protocolstardust merged 2 commits into
RayforceDB:masterfrom
ihrfv:feat/q-connect-keepalive

Conversation

@ihrfv

@ihrfv ihrfv commented Oct 23, 2026 •

Copy link
Copy Markdown
Contributor

How it looks from the user's side

A process subscribed to a q publisher through .q.connect (or a binding's q_connect) waits for pushes. When the link between them dies without a FIN (a VPN tunnel is reaped, a NAT drops the flow, the publisher's host is suspended), nothing reaches the subscriber again, and nothing tells it so.

Before: q_connect sets no keepalive. With no request in flight, timeout_ms bounds nothing, and the connection waits forever.

After: every q_connect connection has kernel TCP keepalive at the core's 60 s default, as ray_ipc_connect already has since #589. A silent peer is declared dead and the socket fails. A caller that wants a tighter bound passes a budget to the new q_connect_opts.

What changed

Modelled on the core's ray_ipc_connect_opts (src/core/ipc.h, src/core/ipc.c), so AUTO, OFF and a budget mean the same thing across the engine:

  • q_connect_opts(host, port, user, password, timeout_ms, keepalive_ms) in q.h. keepalive_ms follows ray_ipc_connect_opts:

    • Q_KEEPALIVE_AUTO (0): keepalive at the core's RAY_IPC_KEEPALIVE_DEFAULT_MS (60 s), with no bound on unacknowledged data.
    • Q_KEEPALIVE_OFF (-1): nothing is armed.
    • n > 0: keepalive at a budget of n ms, plus TCP_USER_TIMEOUT.

    A _Static_assert keeps the two constants equal to RAY_IPC_KEEPALIVE_AUTO and RAY_IPC_KEEPALIVE_OFF.

  • q_connect becomes q_connect_opts(…, Q_KEEPALIVE_AUTO), as ray_ipc_connect defaults to AUTO. Every caller gets it, .q.connect included.

  • The options are set by the core's own ray_sock_set_keepalive, after the handshake succeeds, on the blocking socket. They survive q_conn_attach. A failure to arm them is ignored, as ray_ipc_connect_opts ignores it.

q.c now includes core/ipc.h and core/sock.h, so it needs rayforce core v2.11.0 or later, the first release with ray_sock_set_keepalive. On an older core it fails to compile. CI clones core master, which has it.

Docs: a Keepalive section and the core requirement in docs/INTEGRATING.md, README.md, and an [Unreleased] entry in docs/CHANGELOG.md.

Tests

driver --keepalive-selftest HOST PORT, run in run.sh's server leg, so CI covers it without a q binary. It mirrors the core's test_ipc_keepalive_sockopts: it connects to driver --serve three times and reads the socket options back before and after q_conn_attach, checking them against the core's mapping:

connection SO_KEEPALIVE idle interval probes user timeout
q_connect 1 30 s 10 s 3 none
q_connect_opts, 4000 ms 1 2 s 1 s 3 4000 ms
Q_KEEPALIVE_OFF 0 – – – none
  • Idle: read as TCP_KEEPIDLE, or TCP_KEEPALIVE on macOS.

  • User timeout: read as TCP_USER_TIMEOUT on Linux, or TCP_RXT_CONNDROPTIME (whole seconds) on macOS, where the core sets that instead.

  • The budget connection then answers (+ 1 2).

    make test # macOS, core master d4899d9
    make test CC=clang # ubuntu:24.04, core cloned from GitHub master, as CI does

Both pass: keepalive selftest: ok, with the server round-trip and selftests unchanged. Legs that need a q binary are skipped, as in CI. With the keepalive argument ignored, the selftest fails on every option, e.g. q_connect, connected: SO_KEEPALIVE is 0, want 1.

Against a peer that really goes silent

That needs the peer's packets dropped (iptables or tc, as root), so it is not in CI. Measured by hand on Linux 6.12, with iptables dropping everything the peer sends:

keepalive idle armed dead after the peer went silent
budget 25 000 ms 12 s 28.3 s
budget 10 000 ms, no user timeout 5 s 8.1 s
Q_KEEPALIVE_AUTO 30 s 60.8 s

So there are two rules, and docs/INTEGRATING.md now states them:

  • With a budget, TCP_USER_TIMEOUT decides. The kernel only drops the connection on a probe tick, so it can be up to one interval late: 25 s became 28.3 s.
  • Otherwise (AUTO, or a budget on a platform without the user timeout) it is idle plus three intervals in whole seconds, which can be early: 10 s became 8.1 s.

The same again end to end, through a binding: rayforce-rs connecting with this branch's q_connect_opts, the connection attached to a poll as a subscription, and is_alive() polled every 200 ms (Linux 6.12, amd64):

keepalive kernel timer after connect is_alive() false after
budget 25 s keepalive,11sec 28.1 s
AUTO keepalive,29sec 61.0 s
OFF none still alive at 90 s

A peer that closes, killed 40 times, turned is_alive() false within the first poll slice every time, as before: keepalive plays no part there.

The trade-off, also in the changelog: an idle connection whose network is down for longer than its budget (60 s by default) is now closed, where it used to resume once the network came back. Q_KEEPALIVE_OFF keeps the old behaviour.

ihrfv added 2 commits October 23, 2026 11:43
A q connection whose peer vanished without closing, such as a VPN tunnel
reaped without a FIN, waited forever: q_connect set no keepalive, and a
subscription that only receives has no request in flight for a timeout
to bound. The core solved the same problem for its own client (#589),
and this follows it.

q_connect_opts is q_connect with a keepalive argument, which means what
keepalive_ms means to the core's ray_ipc_connect_opts.
Q_KEEPALIVE_AUTO (0) arms keepalive at the core's 60 s default with no
bound on unacknowledged data. Q_KEEPALIVE_OFF (-1) arms nothing. A
budget in ms arms keepalive at that budget plus TCP_USER_TIMEOUT. A
static assert holds the two constants equal to the core's
RAY_IPC_KEEPALIVE_*. q_connect now calls it with AUTO, so every caller,
.q.connect included, gets keepalive. The core made the same default
change for ray_ipc_connect.

The options are set through the core's ray_sock_set_keepalive once the
handshake succeeds, on the blocking socket, so q_conn_attach keeps them.
A failure to arm them is ignored, as in the core: the link is no worse
than before. q.c now needs core v2.11.0 or later.

driver --keepalive-selftest connects to driver --serve through
q_connect, through q_connect_opts with a 4000 ms budget, and with
Q_KEEPALIVE_OFF, and reads the options back before and after
q_conn_attach against the core's mapping, as the core's
test_ipc_keepalive_sockopts does. The budget connection then answers a
query. run.sh runs it in the server leg, which needs no q. Tested on
macOS and on Linux (Ubuntu 24.04, clang).
…ured

INTEGRATING.md said a silent peer is dead about n ms after it went quiet.
Measured on Linux 6.12 with its packets dropped, there are two rules.
With a budget, TCP_USER_TIMEOUT decides, but the kernel only drops the
connection on a probe tick, so it can be up to one interval late: a 25 s
budget died at 28.3 s. Otherwise (AUTO, or a budget elsewhere) it is
idle + 3 x interval in whole seconds, which can be early: a 10 s budget
without the user timeout died at 8.1 s. AUTO died at 60.8 s.

A failure to arm was said to leave the connection without keepalive.
That holds only when SO_KEEPALIVE itself fails; a tuning option the
kernel rejects keeps its system default, as in the core. q.h said the
same and is corrected too.

The changelog now names the trade-off: an idle connection whose network
is down for 30-60 s or more is closed, where it used to resume, and
Q_KEEPALIVE_OFF keeps the old behaviour.
ihrfv added a commit to ihrfv/rayforce-rs that referenced this pull request Oct 23, 2026
The pinned rayforce-q commit, 2cc931c, is the head of the open
RayforceDB/rayforce-q#24 and exists only on ihrfv/rayforce-q, so a
build that clones the submodule from upstream cannot find it. The URL
goes back to upstream, and the pin to the merge commit, once #24
merges.
@ihrfv

ihrfv commented Oct 23, 2026

Copy link
Copy Markdown
Contributor Author

@morsel-ray could you please review the changes?

@morsel-ray morsel-ray left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Reviewed exact2cc931c6b9a7b767ad1abcb758eed956781b7b77: complete seven-file delta, connector/attach path, and the core keepalive helper. AUTO/positive/OFF dispatch matches the core contract; the new default is an intentional observable behaviour change, with the interruption-vs-detection tradeoff and best-effort option arming documented. No new hot-path validation requested.

Independently ran the q-free suite against pinned core5d5f47c2 (v2.11.5): codec/exchange/CLI,33 server assertions, AUTO/OFF/4000ms socket-option checks before/after attach, malformed/fragmented-frame controls all pass. Removing only the new keepalive calls in an external counterfactual builds but fails the required socket-option assertions. Exact clean source, core archive and original driver hashes rechecked; whole relevant logs contain no sanitizer diagnostics.

Scope: adapter/driver ASan+UBSan, preexisting uninstrumented release core archive; test-only linker wrapper confined the listener to127.0.0.1. No real-q, packet-loss/deadline/timing, macOS, minimum-v2.11.0 linking or combined#24+#25 execution claim. I used the explicit core SHA, not a floating branch. This does not address the separate trusted-evaluation/handler-dispatch boundary in#26.

Approved for this keepalive change; not merged by me.

@protocolstardust
protocolstardust merged commit 226f27c into RayforceDB:master Oct 24, 2026
1 check passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants