Skip to content

benchmark: keep service ports out of the ephemeral pool, and stop the cache failing silently - #1191

Merged
MDA2AV merged 2 commits into
mainfrom
fix/production-stack-port-collisions
Aug 16, 2026
Merged

benchmark: keep service ports out of the ephemeral pool, and stop the cache failing silently#1191
MDA2AV merged 2 commits into
mainfrom
fix/production-stack-port-collisions

Conversation

@MDA2AV

@MDA2AV MDA2AV commented Aug 16, 2026

Copy link
Copy Markdown
Owner

production-stack fails at the end of a full run and passes when it is the only profile. Both failures were identical:

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.

9090 — the ports are in the ephemeral pool

system.sh widens 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:

run production-stack position result
12:01 first and only profile
11:27 last, after 17 profiles ❌ 9090 in use
12:16 last, after 17 profiles ❌ 9090 in use

tcp_tw_reuse doesn't 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 exactly what it's for. Reserved: 8080, 8081, 8082, 8443 from common.sh, 9090 for the auth sidecar, 6379 and 5432 for 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_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 — which is why aspnet-minimal_nginx and sark-production never saw it.

It collided every time, and nothing noticed: 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 with one io-thread, instead of the 16,80 and four io-threads the compose file asks for.

Two changes:

  • the three production-stack compose files give cache a 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 a cache service, so the port is actually free. gateway_down starts it again — ahead of its early returns, and in a subshell so redis_start's fail can't exit the cleanup trap

Docs

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.
  • 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.mdip_local_reserved_ports documented next to the range it compensates for, with the failure that motivated it, and a note 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

Against real containers:

--- stack that ships a cache (production-stack) ---
[info] stopping the harness redis sidecar: this stack ships its own cache on the same port
  GATEWAY_STOPPED_REDIS=true      (redis released 6379)
--- gateway_down restores it ---
  running: httparena-redis
--- stack with NO cache (gateway-64) leaves redis alone ---
  GATEWAY_STOPPED_REDIS=false     running: httparena-redis

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-run and /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_ports exists 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

… 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.
Repository owner deleted a comment from github-actions Bot Aug 16, 2026
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.
Repository owner deleted a comment from github-actions Bot Aug 16, 2026
@MDA2AV
MDA2AV merged commit daf5ecf into main Aug 16, 2026
3 of 4 checks passed
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.
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.

1 participant