benchmark: keep service ports out of the ephemeral pool, and stop the cache failing silently - #1191
Merged
Merged
Conversation
… cache failing silently
production-stack fails at the end of a full run and passes when it is the only
profile. Both failures were the same:
cache-1 exited 1 Could not create server TCP listening socket 0.0.0.0:6379: bind: Address in use
authsvc-1 exited 101 bind 0.0.0.0:9090: Address in use (os error 98)
Nothing in the entry causes either, and nothing in the repo binds 9090 except
authsvc itself.
The ports are in the ephemeral pool. system.sh widens
net.ipv4.ip_local_port_range to '1024 65535' so connection-churning profiles do
not exhaust the default range, which also means every port a server listens on
is one the kernel can hand to an outbound socket. Across a full run - seventeen
profiles, millions of connections at up to 16384 concurrent - a load generator
ends up holding 9090, and the stack that starts afterwards cannot bind it. Run
production-stack first, with no load behind it, and the port is free.
tcp_tw_reuse does not help: it governs outbound reuse, not a listening bind.
ip_local_reserved_ports takes them out of ephemeral allocation while leaving
them bindable, which is the case it exists for. Reserved: 8080, 8081, 8082 and
8443 from common.sh, 9090 for the auth sidecar, 6379 and 5432 for the Redis and
Postgres sidecars. 8080/8443 have not been hit yet only because they are bound
early in a profile rather than at the end of a run.
The 6379 half is older and was never a failure, which is worse. redis_start
runs once per run whenever the entry subscribes to crud, and the sidecar holds
the host's 6379 for the whole run; production-stack ships its own cache on the
same port. fulmine is the only entry subscribed to both, so it collided every
time - and nothing noticed, because the server depended on the cache with the
short `depends_on: - cache` form, which waits only for the container to start.
The cache died, the server carried on against the harness Redis, and the
profile published numbers measured against a cache it never configured: cpuset
0,64 and one io-thread instead of the 16,80 and four the compose file asks for.
So, two changes there. The three production-stack compose files give the cache
a healthcheck and depend on it being healthy, which turns a silent downgrade
into a failed stack. And _gateway_yield_redis() stops the harness sidecar
before bringing up a stack whose compose defines a `cache` service, so the port
is actually free; gateway_down starts it again, ahead of its early returns and
inside a subshell so redis_start's `fail` cannot exit the cleanup trap.
Verified against real containers: the sidecar is stopped for production-stack
and restored afterwards, and left alone for gateway-64, which ships no cache.
The sysctl value is not exercised here - this box has no passwordless sudo.
The behaviour changed in this branch, so the pages that specify the profile change with it. test-profiles/gateway/production-stack/implementation.md gains two sections. "Required compose settings" now states the two dependencies that have to be conditions rather than the short list form, with the cache healthcheck written out, and says why: depends_on: - cache waits only for the container to start, so a cache that dies on boot leaves the server running against whatever else holds 6379 and the profile publishes numbers measured against a cache it never configured. "Ports the stack owns" lists the four host ports and the two things the driver does to keep them free - stopping its own Redis sidecar for the duration, and keeping all four out of the ephemeral range. The same requirement goes into the type-rules widget for standard and tuned, since it is now a rule an entry has to satisfy rather than advice. hardware/_index.md documents ip_local_reserved_ports next to the range it compensates for, with the failure that motivated it, and notes that tcp_tw_reuse governs outbound reuse rather than a listening bind. running-locally/scripts/ benchmark.md picks it up in the host-tuning step and links across. Verified the pages build and every link on them resolves: the two new cross-references land on /docs/hardware/#kernel-tuning-applied-per-run and /docs/test-profiles/h1/isolated/crud/implementation/, and the type-rules widget still renders its three tabs with the new sentence in Standard and Tuned.
MDA2AV
added a commit
that referenced
this pull request
Aug 16, 2026
…mark driver does (#1192) #1191 made the three production-stack compose files wait for a healthy cache, so a cache that cannot bind now fails the stack instead of being stepped over. It fixed the benchmark path at the same time, by having gateway_up stop the harness Redis sidecar before a stack that ships its own. It did not fix the validate path, because validate.sh does not source scripts/lib/gateway.sh - it carries its own compose handling. So validation started failing where it used to pass: dependency failed to start: container httparena-validate-gw-production-stack-fulmine-cache-1 exited (1) | Could not create server TCP listening socket 0.0.0.0:6379: bind: Address in use validate.sh starts its own Redis sidecar on 6379 whenever the entry subscribes to crud, and fulmine subscribes to both crud and production-stack. The cache has never been able to bind there either; the short depends_on form meant nobody found out, and validation was checking the stack against the sidecar's Redis rather than the one the profile configures. _prodstack_yield_redis stops the sidecar before the compose up and _prodstack_restore_redis brings it back, on the failure path as well as after teardown. The sidecar's docker run moves into redis_sidecar_start() so both callers use one definition rather than a copy. Entries that subscribe to production-stack without crud never set REDIS_CONTAINER, and the yield is a no-op for them. Verified by exercising the three functions against real containers: the sidecar is stopped and 6379 released, restarted afterwards, and left alone when REDIS_CONTAINER is unset.
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.
production-stack fails at the end of a full run and passes when it is the only profile. Both failures were identical:
Nothing in the entry causes either, and nothing in the repo binds 9090 except authsvc itself.
9090 — the ports are in the ephemeral pool
system.shwidens the range so connection-churning profiles don't exhaust it:sudo sysctl -w net.ipv4.ip_local_port_range='1024 65535'Which also puts every port a server listens on into the pool the kernel draws outbound ports from. Across a full run — 17 profiles, millions of connections at up to 16,384 concurrent — a load-generator socket ends up holding 9090, and the stack starting afterwards can't bind it:
tcp_tw_reusedoesn't help — it governs outbound reuse, not a listening bind.ip_local_reserved_portstakes them out of ephemeral allocation while leaving them bindable, which is exactly what it's for. Reserved:8080,8081,8082,8443fromcommon.sh,9090for the auth sidecar,6379and5432for the Redis and Postgres sidecars. 8080/8443 haven't been hit yet only because they're bound early in a profile rather than at the end of a run.6379 — older, never a failure, and worse for it
redis_startruns once per run whenever the entry subscribes tocrud, and the sidecar holds the host's 6379 for the whole run. production-stack ships its owncacheon the same port.fulmineis the only entry subscribed to both — which is whyaspnet-minimal_nginxandsark-productionnever saw it.It collided every time, and nothing noticed: the server depended on the cache with the short
depends_on: - cacheform, which waits only for the container to start. The cache died, the server carried on against the harness Redis, and the profile published numbers measured against a cache it never configured — cpuset0,64with one io-thread, instead of the16,80and four io-threads the compose file asks for.Two changes:
cachea healthcheck and depend on it being healthy, turning a silent downgrade into a failed stack_gateway_yield_redis()stops the harness sidecar before bringing up a stack whose compose defines acacheservice, so the port is actually free.gateway_downstarts it again — ahead of its early returns, and in a subshell soredis_start'sfailcan't exit the cleanup trapDocs
The behaviour changed, so the pages that specify the profile change with it:
test-profiles/gateway/production-stack/implementation.md— "Required compose settings" now states both dependencies that must be conditions rather than the short list form, with the cache healthcheck written out and the reason spelled out. A new "Ports the stack owns" section lists the four host ports and the two things the driver does to keep them free.hardware/_index.md—ip_local_reserved_portsdocumented next to the range it compensates for, with the failure that motivated it, and a note thattcp_tw_reusegoverns outbound reuse rather than a listening bindrunning-locally/scripts/benchmark.md— picks it up in the host-tuning step and links acrossVerified
Against real containers:
Compose files parse, both scripts pass
bash -n, and the doc pages build with every link resolving — the two new cross-references land on/docs/hardware/#kernel-tuning-applied-per-runand/docs/test-profiles/h1/isolated/crud/implementation/, and the type-rules widget still renders its three tabs.Not exercised locally: the sysctl value — this box has no passwordless sudo.
net.ipv4.ip_local_reserved_portsexists on the kernel here and reads empty; worth a glance at the first run's log to confirm it isn't warning.Follows #1189, which made these failures legible in the first place — without its log dump this was just
exited (101).🤖 Generated with Claude Code