Pace the rate_limit SNI queue test on the plugin's own log lines - #13681
Conversation
The test choreographed its setup with wall-clock sleeps, so on a loaded runner the holder could fail to hold the single slot, nothing queued, and the run failed on its own precondition rather than on the behaviour under test. The same guesswork let it pass without exercising anything: on macOS the holder never released, so the resume and release paths never ran and the test passed against the unfixed plugin. Every step now waits for the plugin's debug line instead of sleeping, and every connection is held open on a FIFO and ended by signal, so neither platform's s_client EOF behaviour matters. The test also asserts the sweep resumed the queued connection and that no release wraps the counter, so the queue path cannot be silently skipped. Fixes: apache#13679
There was a problem hiding this comment.
🟡 Changes recommended
Two moderate issues remain in event-order validation and child-process cleanup.
Get a fresh assessment by requesting another Copilot review.
Pull request overview
Updates the rate_limit SNI queue test to synchronize with plugin logs and control TLS client lifetimes deterministically.
Changes:
- Replaces timing races with log-driven waits and bounded diagnostics.
- Uses FIFO-backed connections and PID-based teardown.
- Adds queue-resumption, rejection, and counter-wrap assertions.
File summaries
| File | Summary |
|---|---|
tests/gold_tests/pluginTest/rate_limit/rate_limit_sni_queue.test.py |
Passes the traffic log path and adds stronger queue-path assertions. |
tests/gold_tests/pluginTest/rate_limit/rate_limit_sni_queue_client.sh |
Implements deterministic queue choreography; moderate issues remain with early-resume enforcement and failure cleanup. |
Review details
Suppressed comments (1)
tests/gold_tests/pluginTest/rate_limit/rate_limit_sni_queue_client.sh:70
- On a timeout or other early failure, the EXIT trap only sends TERM and returns without waiting or escalating. This script explicitly treats a TERM-immune
s_clientas possible inend_connection, so a stuck child can survive the test while retaining the inherited FIFO/TLS connection and leave resources in ATS for subsequent tests. Reuse the bounded TERM/KILL-and-wait cleanup for every non-empty child PID.
cleanup() {
kill -TERM ${holder:-} ${queued:-} ${probe:-} 2>/dev/null || true
}
- Files reviewed: 2/2 changed files
- Comments generated: 1
- Review effort level: Lite
💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.
The post-release wait counted resumes with an absolute count, so a sweep that resumed while the limiter was still full satisfied it with the very event the test rejects. Assert the sweep's own reservation ahead of the resume instead -- same-thread program order rather than a race against the holder's release -- and give the EXIT trap the bounded TERM/KILL/wait that end_connection already uses, so a TERM-immune s_client cannot outlive the run.
There was a problem hiding this comment.
🔵 Needs a closer look
Preserve stderr before redirecting it so forced process termination diagnostics remain available.
Review details
Suppressed comments (1)
tests/gold_tests/pluginTest/rate_limit/rate_limit_sni_queue_client.sh:110
exec 4>&2runs inside the{ ... } 2>/dev/nullgroup, so fd 4 is duplicated from/dev/null, not the original stderr. Ifs_clientignores TERM and the KILL escalation is needed, the diagnostic promised by the comment is silently discarded, making this failure mode harder to diagnose. Save fd 4 before redirecting stderr (or order the group redirections as4>&2 2>/dev/null).
exec 4>&2
- Files reviewed: 2/2 changed files
- Comments generated: 0 new
- Review effort level: Lite
bneradt
left a comment
There was a problem hiding this comment.
Reviewed 9e106d6. No additional actionable findings beyond the existing inline requests to remove historical commentary.
The reservation-before-resume check agrees with the plugin's execution order: reserve() logs under the active-slot lock, and the sweep logs the resume after that call returns. The release check appropriately allows concurrent reservation to affect the logged counter value.
Validation: Bash syntax and Python parsing passed. A temporary mock client exercised the shell helper's expected sequence (exit 0), resume without a reservation (exit 1), and early resume while the holder remains active (exit 1). These checks exercise shell control flow only; I did not run the ATS/TLS AuTest because this checkout has no configured build.
The comments described how the test used to be wrong and cited the commit and issue behind each change, which is noise for someone reading the test today and is recoverable from blame anyway. Keep what the test does and why it does it that way, and state the accounting invariant in the present tense.
|
Two points from the automated reviews that came through as suppressed comments, EXIT trap not escalating -- fixed in 9e106d6.
probe() {
exec 4>&2
{ echo "MSG-VIA-FD4" >&4
echo "MSG-VIA-FD2" >&2
} 2>/dev/null
exec 4>&-
}
probeWith stderr sent to a file, the file contains |
bneradt
left a comment
There was a problem hiding this comment.
Reviewed 5a5b7eb. The historical commentary concerns are addressed. Verified that the shell executable lines are unchanged and the Python AST is unchanged apart from the module docstring. No new actionable findings; all 14 reported CI checks pass, including all four AuTest shards.
Fixes: #13679
Problem
rate_limit_sni_queuechoreographed its setup with wall-clock sleeps racing liveTLS handshakes. That made it fail intermittently on loaded CI runners, and, less
obviously, let it pass without exercising the queue at all. The only positive
assertion was
ContainsExpression('Queueing the VC'), which does not say whichconnection queued, so two separate failure modes hid behind it.
On Linux, under load, the holder never holds. Reproduced on a 32-core Fedora 44
box with 96 busy loops, 4 of 8 runs failed with exactly the CI signature:
No
Queueing the VCanywhere. The second connection found the slot free and wasserved rather than queued, so the precondition failed while every other assertion
passed and ATS stayed healthy.
On macOS, the holder never releases, and the guard was inert. LibreSSL's
s_clientdoes not exit on the FIFO EOF the script used to end the holder, so therun went:
No
Releasing, noEnabling queued VC. The resume and release paths never ran.Reverting the one-line fix from 508c1be and re-running confirmed it: the old
test passes against the unfixed plugin on macOS. It has not been guarding
anything on that platform.
Changes
rate_limit_sni_queue_client.shwaits for the plugin's debug line intraffic.outinstead of sleeping, with a 30s per-step ceiling that fails loudlyand dumps the log tail.
s_client: every connection reads stdinfrom a FIFO the parent holds open read-write on fd 3 and inherits via
<&3, sonothing depends on how
s_clientreacts to EOF, and each is ended by signallingopenssl's own PID with a bounded TERM/KILL/wait.
ahead of the first
Enabling queued VCand fails if the sweep's own reservationis not among them, naming the defect where it happens rather than waiting for the
counter wrap downstream.
ContainsExpression('Enabling queued VC')(the sweep really resumed it),ExcludesExpression('Rejecting connection')(the holder really released), andExcludesExpression(r'Releasing a slot, active entities == [0-9]{4,}')(no wrap).The existing
ExcludesExpression('_active <= _limit|received signal')isunchanged and still hard.
s_clientcan no longer outlive therun holding the inherited FIFO and a live TLS connection into ATS, and leak into
the next test in the shard.
commentary and per-change commit citations are gone; the accounting invariant is
stated in the present tense.
Approach
The one remaining
sleepis a deliberate lower bound that lets the 300ms sweep tickwhile the slot is held; a correct sweep emits nothing to wait on, and a slower
runner only gives it more ticks.
The release assertion matches only a wrapped value, not any non-zero one, because
Limiter::free()logs_activeafter dropping the lock, so a concurrentreserve()can legitimately make a release read back as 1. An unmatched decrementof a
uint32at limit 1 is unmistakable regardless — it reads 4294967295, never asmall number. For the same reason the resume is ordered against the sweep's own
reservation (same-thread program order) rather than against the holder's release,
which would be a race.
The header now says explicitly that a client cannot close a connection parked in the
ClientHello hook, so the "closes while parked" step the old script appeared to
perform never did anything. ATS does not read the socket while the hook is invoked,
so the FIN sits in the kernel until the sweep reenables the VC. Measured on both
platforms: no release for the full 2s a connection sat parked and killed, with the
release appearing only after
Enabling queued VC. The old 0.3s kill therefore alwaysresolved to the same resume-then-close path this script now drives deterministically.
The two platforms failed the old test for opposite reasons, which is why the fix
removes the dependency on
s_clientlifetime behaviour entirely rather than tuninga timeout.
Testing
Both platforms ran byte-identical files (md5 checked). Oracle = revert the
508c1be one-liner in
sni_selector.cc, rebuildrate_limit.so, reinstall, rerun.The oracle run on both platforms: