Keep paths alive on a timer of their own - #76
Merged
Conversation
#74 made the keep alive PING go out on every path, but hung that off QUIC_CONN_TIMER_KEEP_ALIVE. QuicConnResetIdleTimeout re-arms that timer on every ack-eliciting packet received and on the first packet put in flight, so it fires only once the whole connection has gone quiet. That is the right rule for a timer whose job is to stop the connection going idle, and the wrong rule for keeping a path alive: a connection busy on one path pushes the timer out indefinitely while another path sits untouched, loses its NAT mapping, and is eventually abandoned by the peer. Give the per-path job its own timer and setting, which nothing resets. PathKeepAliveIntervalMs (default 0, disabled) is how long a path may go without sending before it gets a PING, counted from what that path carried: - QUIC_PATH gains LastSendTimeUs, stamped in QuicPacketBuilderSendBatch. - QuicConnPathKeepAliveTimerUpdate sets SendKeepAlive on every path idle for the full interval, then arms QUIC_CONN_TIMER_PATH_KEEP_ALIVE for whichever path comes due next. A path that is sending on its own never gets a redundant PING. - Armed when the handshake completes -- the earliest point 1-RTT keys exist, which QuicSendPathKeepAlives needs -- and on SetParam. Cancelled when the interval is 0 or the connection closes. QuicConnProcessKeepAliveOperation goes back to a plain QUIC_CONN_SEND_FLAG_PING: KeepAliveIntervalMs means "keep the connection alive" again. QuicSendPathKeepAlives from #74 is unchanged and serves the new timer. The new timer type is appended at the end of QUIC_CONN_TIMER_TYPE so the existing codes keep their values for anything decoding traces. Basic/WithFamilyArgs.PathKeepAlive brings up a two-path connection with no connection keep alive on either side and the MTU pinned so discovery cannot contribute traffic, then asserts both paths carry client->server datagrams over a 600ms window. Removing the arming call flips it to failure. The "not reset by connection activity" half is measured rather than asserted -- with a server keep alive of 20ms holding the connection busy the path keep alive still fired 4 times, and emulating the old behaviour dropped that to 0 -- because QuicConnChoosePath picks a random active path per send, so under load neither path stays quiet enough for a datapath hook to tell keep alives from ordinary traffic. Plumbed through the usual places: msquic.h/.hpp, settings.h/.c, quicdef.h, docs/Settings.md, SettingsTest, the Linux clog artifacts and sidecar, and the Rust bindings. The C# bindings were not updated; they need their generator re-run. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
|
|
||
| if (!Settings->IsSet.PathKeepAliveIntervalMs) { | ||
| ValueLen = sizeof(Settings->PathKeepAliveIntervalMs); | ||
| CxPlatStorageReadValue( |
masa-koz
added a commit
that referenced
this pull request
Aug 4, 2026
Conflicts:
src/core/connection.c
QuicConnProcessKeepAliveOperation. qmux-01 branches on QuicConnIsQMux
to send a QX PING; seera-main dropped the multipath fan-out that #74
had put on this timer, now that #76 gives the paths one of their own.
Kept the QMux branch over the reverted body.
QuicConnPathKeepAliveTimerUpdate also declines to arm for a QMux
connection: it has one transport-provided path and no 1-RTT keys of
its own, so QuicSendPathKeepAlives has nothing to build on.
src/test/bin/quic_gtest.cpp
Both sides added a TEST_P after Multipath. Kept both, PathKeepAlive
first.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Codecov Report❌ Patch coverage is
📢 Thoughts on this report? Let us know! |
masa-koz
added a commit
that referenced
this pull request
Aug 14, 2026
Brings in #76 (per-path keep alive timer) and #77 (upstream main sync, 25 commits). Conflicts: scripts/build.rs qmux-01 added iOS/Apple support around the link search path; seera-main replaced the single "lib" path with a lib/lib64 probe (upstream microsoft#6168). Independent: took the probe, kept the `|| apple_ios` condition and the Apple framework branch. `path_extra` is gone with the single path it fed. src/core/connection.c QuicConnGenerateLocalTransportParameters. qmux-01 wrapped the UDP-only transport parameters in !QuicConnIsQMux; seera-main added the MinAckDelay clamp (upstream microsoft#6213). Put the clamp inside the non-QMux block, next to the MaxAckDelay/MinAckDelay it constrains -- QMux sets neither. src/core/send.c QuicSendCanSendFlagsNow. qmux-01 wrapped the check in !QuicConnIsQMux; seera-main added a clang-tidy ArrayBound suppression above it (upstream microsoft#6155). Kept both, with the suppression on the line it was written for. That alone was not enough. QuicConnIsQMux is now the first dereference of Connection, and being QUIC_INLINE the analyzer reports inside the helper in connection.h rather than at the call site, where no NOLINT in send.c can reach it. Suppressed inside QuicConnIsQMux instead, matching what upstream microsoft#6155 already does for QuicConnIsClosed one function below. Verified with the CI toolchain rather than the local one, which is too old to see these diagnostics: a full -CodeCheck build (clang-tidy 21, Debug, linux, x64, quictls) inside ghcr.io/microsoft/msquic/linux-build-xcomp: ubuntu-26.04-cross completes clean. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Description
#74 made the keep alive PING go out on every path, but hung that off
QUIC_CONN_TIMER_KEEP_ALIVE— the wrong timer.QuicConnResetIdleTimeoutre-arms it on every ack-eliciting packet received and on the first packet put in flight:So it fires only once the whole connection has gone quiet. That is the right rule for what it is — a timer that stops the connection going idle — and the wrong rule for keeping a path alive. A connection busy on one path pushes the timer out indefinitely while another path sits untouched, loses its NAT mapping, and is eventually abandoned by the peer.
Changes
A separate timer with its own setting, which nothing resets.
PathKeepAliveIntervalMs(default 0, disabled) is how long a path may go without sending before it gets a PING. It is counted per path, from what that path carried:QUIC_PATHgainsLastSendTimeUs, stamped inQuicPacketBuilderSendBatch.QuicConnPathKeepAliveTimerUpdatesetsSendKeepAliveon every path idle for the full interval, then armsQUIC_CONN_TIMER_PATH_KEEP_ALIVEfor whichever path comes due next. A path that is sending on its own never gets a redundant PING, and the timer never wakes earlier than it has to.QuicSendPathKeepAlivesneeds — and onSetParam. Cancelled when the interval is 0 or the connection closes.QuicConnProcessKeepAliveOperationgoes back to a plainQUIC_CONN_SEND_FLAG_PING.KeepAliveIntervalMsmeans "keep the connection alive" again; keeping the paths alive is now the other setting's job.QuicSendPathKeepAlivesfrom #74 is unchanged and serves the new timer.QUIC_CONN_TIMER_PATH_KEEP_ALIVEis appended at the end ofQUIC_CONN_TIMER_TYPEso the existing timer codes keep their values for anything decoding traces.Testing
New
Basic/WithFamilyArgs.PathKeepAliveinPathTest.cpp, registered inMsQuicTests.h,quic_gtest.cppandwinkernel/control.cpp. It brings up a two-path connection with no connection keep alive on either side and the MTU pinned so discovery cannot contribute traffic, then asserts both paths carry client→server datagrams over a 600ms window. Every datagram the server sees in that window is a keep alive. Removing the arming call flips it to failure:The "not reset by connection activity" half is not asserted, only measured. With a server keep alive of 20ms holding the connection busy, the path keep alive still fired 4 times in the window; emulating the old behaviour by re-arming the new timer from
QuicConnResetIdleTimeoutdropped that to 0 — the bug this change is about. It cannot become an assertion becauseQuicConnChoosePathpicks a random active path per send, so under load neither path stays quiet enough for a datapath hook to tell keep alives from ordinary traffic.*Multipath*:*Path*:*Migration*:*UnconnectedSocket*:*KeepAlive**Basic*:*Datagram*:*Receive*:*Recv*PathKeepAlive× 6 repeatsmsquiccoretest --gtest_filter=*Settings*cargo test --features preview-apiNo compiler warnings.
Documentation
docs/Settings.mdgainsPathKeepAliveIntervalMs, and theKeepAliveIntervalMsrow now says it is reset by connection activity — the distinction between the two.The Rust bindings are updated:
src/rs/settings.rsgains the setter andlinux_bindings.rswas regenerated withcargo build --features overwrite.win_bindings.rscannot be regenerated on Linux, so the identical hunks were applied by hand; the two diffs match line for line. The C# bindings were not updated — they need their generator re-run, and are already missing several fork settings.