Skip to content

fix(edge): release the WebSocket admission permit, add a budget key - #126

Merged
cuioss-oliver merged 11 commits into
mainfrom
feature/websocket-admission-permit-leak
Jul 29, 2026
Merged

fix(edge): release the WebSocket admission permit, add a budget key#126
cuioss-oliver merged 11 commits into
mainfrom
feature/websocket-admission-permit-leak

Conversation

@cuioss-oliver

@cuioss-oliver cuioss-oliver commented Jul 29, 2026

Copy link
Copy Markdown
Collaborator

Summary

Fixes a remotely-reachable denial-of-service defect in the API Sheriff edge: every WebSocket upgrade
permanently leaked one admission permit from the gateway's single admission semaphore, so a sustained
WebSocket workload could exhaust the pool and the gateway would then reject all traffic — HTTP
included — with 503 until restarted. This PR releases the permit correctly, promotes the admission
budget to operator-facing configuration with a dedicated WebSocket-relay budget key, adds a
containerised regression test that reproduces and proves the fix, records why the existing IT suite
was blind to the leak, and re-enables the previously-quarantined websocketEcho benchmark goal.

Changes

  • Leak fix (D1a) — the admission permit is now released exactly once via the existing
    releaseAdmission(AtomicBoolean) CAS guard, held for the whole relay lifecycle instead of being
    dropped at upgrade time. Both the established-relay teardown funnel (RelaySession.closeBoth) and
    the client-upgrade-failure path now release correctly:

    • api-sheriff/src/main/java/de/cuioss/sheriff/gateway/edge/WebSocketRelayStage.java
    • api-sheriff/src/main/java/de/cuioss/sheriff/gateway/edge/GatewayEdgeRoute.java
  • Operator-facing config surface (D1b) — the admission cap is no longer a hard-coded constant; it
    is now a configurable, WebSocket-relay-specific budget key threaded through the full config chain:

    • api-sheriff/src/main/java/de/cuioss/sheriff/gateway/config/model/EdgeHardeningConfig.java
    • api-sheriff/src/main/java/de/cuioss/sheriff/gateway/config/model/GatewayConfig.java
    • api-sheriff/src/main/java/de/cuioss/sheriff/gateway/config/validation/ConfigValidator.java
    • api-sheriff/src/main/java/de/cuioss/sheriff/gateway/edge/EdgeHardeningOptions.java
    • api-sheriff/src/main/java/de/cuioss/sheriff/gateway/quarkus/ConfigModelReflection.java
    • api-sheriff/src/main/java/de/cuioss/sheriff/gateway/quarkus/ConfigProducer.java
    • api-sheriff/src/main/resources/schema/gateway.schema.json
  • Containerised regression test (D2) — a dedicated compose service reusing the same native image
    with an overlaid, low-admission-cap gateway.yaml (mirroring the existing api-sheriff-mtls /
    api-sheriff-cookie idiom) so the shared primary IT instance is untouched:

    • integration-tests/src/main/docker/sheriff-config-ws-admission/gateway.yaml
    • integration-tests/src/test/java/de/cuioss/sheriff/gateway/integration/WebSocketProxyIT.java
    • integration-tests/src/test/java/de/cuioss/sheriff/gateway/integration/WsAdmissionActivationWiringTest.java
    • integration-tests/docker-compose.yml
    • integration-tests/pom.xml
    • integration-tests/scripts/dump-keycloak-logs.sh
    • integration-tests/scripts/start-integration-container.sh
  • Blind-spot record (D3)WebSocketProxyIT now documents why the prior suite (7 tests, one
    handshake each, per-request status assertions only) could never exhaust a large admission pool and
    therefore never observed the leak.

  • Benchmark re-activation (D4)websocketEcho is re-enabled end to end: the skip flag defaults
    to false, the goal moves from the skipped table into the coverage step's expected list, and stale
    goal-count prose is updated:

    • benchmarks/pom.xml
    • .github/workflows/benchmark.yml
    • benchmarks/README.adoc
  • Three-layer documentation (D5) — the new operator-visible configuration key is documented across
    the authoritative reference, the user layer, and the development layer:

    • doc/configuration.adoc
    • doc/user/protocol-routes.adoc
    • doc/development/protocol-processors.adoc
  • Regression coverage for the touched production classes:

    • api-sheriff/src/test/java/de/cuioss/sheriff/gateway/config/model/ConfigModelContractTest.java
    • api-sheriff/src/test/java/de/cuioss/sheriff/gateway/config/validation/ConfigValidatorTest.java
    • api-sheriff/src/test/java/de/cuioss/sheriff/gateway/edge/EdgeHardeningTest.java
    • api-sheriff/src/test/java/de/cuioss/sheriff/gateway/edge/GatewayEdgeRouteTest.java
    • api-sheriff/src/test/java/de/cuioss/sheriff/gateway/edge/WebSocketRelayStageTest.java
    • api-sheriff/src/test/java/de/cuioss/sheriff/gateway/quarkus/ConfigProducerTest.java
    • api-sheriff/src/test/java/de/cuioss/sheriff/gateway/quarkus/SheriffMetricsTest.java

Test Plan

  • Quality gate (verify -Ppre-commit) passed
  • Full verify (verify) passed
  • Containerised IT suite (verify -Pintegration-tests -pl integration-tests -am), including the
    new WebSocketProxyIT regression case, passed
  • Sonar gate GREEN
  • Post-merge Performance Benchmark run confirms the re-enabled websocketEcho goal produces a
    summary document (tracked separately, not a pre-merge gate)

Related Issues

None.


Generated by plan-finalize skill

Summary by CodeRabbit

  • New Features
    • Added operator-configurable edge_hardening admission limits: admission_cap and websocket_relay_cap, including defaults and derived relay-cap behavior.
  • Bug Fixes
    • Improved WebSocket relay admission/permit accounting to release limits correctly across success, refusal, and teardown; upgrades beyond websocket_relay_cap are rejected with HTTP 503.
  • Documentation
    • Expanded configuration schema, validation rules, and gateway.yaml example, plus WebSocket relay sizing and lifecycle guidance.
  • Benchmarking
    • Updated CI/benchmark coverage so WebSocket echo runs by default; only the large-upload benchmark remains skipped.

@sourcery-ai sourcery-ai Bot 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.

Sorry @cuioss-oliver, you have reached your weekly rate limit of 500000 diff characters.

Please try again later or upgrade to continue using Sourcery

@coderabbitai

coderabbitai Bot commented Jul 29, 2026

Copy link
Copy Markdown

Review Change Stack

📝 Walkthrough

Walkthrough

Adds configurable admission and WebSocket relay caps, validates and wires them into gateway runtime behavior, fixes permit release across WebSocket lifecycle paths, adds regression integration coverage, and re-enables the websocketEcho benchmark.

Changes

Edge hardening configuration and runtime

Layer / File(s) Summary
Configuration contract and resolution
api-sheriff/src/main/java/de/cuioss/sheriff/gateway/config/..., api-sheriff/src/main/java/de/cuioss/sheriff/gateway/edge/EdgeHardeningOptions.java, api-sheriff/src/main/java/de/cuioss/sheriff/gateway/quarkus/..., api-sheriff/src/main/resources/schema/gateway.schema.json, api-sheriff/src/test/java/de/cuioss/sheriff/gateway/config/..., api-sheriff/src/test/java/de/cuioss/sheriff/gateway/edge/EdgeHardeningTest.java, api-sheriff/src/test/java/de/cuioss/sheriff/gateway/quarkus/ConfigProducerTest.java
Introduces optional admission and WebSocket relay caps with defaults, schema and validation rules, native reflection registration, CDI resolution, and test coverage.
Admission and relay teardown lifecycle
api-sheriff/src/main/java/de/cuioss/sheriff/gateway/edge/GatewayEdgeRoute.java, api-sheriff/src/main/java/de/cuioss/sheriff/gateway/edge/WebSocketRelayStage.java, api-sheriff/src/test/java/de/cuioss/sheriff/gateway/edge/*Test.java
Adds a WebSocket relay sub-budget and releases general and relay permits exactly once across successful teardown, rejected upgrades, and client-upgrade failures.

Integration regression coverage

Layer / File(s) Summary
Low-budget gateway integration
integration-tests/docker-compose.yml, integration-tests/pom.xml, integration-tests/scripts/*, integration-tests/src/main/docker/sheriff-config-ws-admission/gateway.yaml, integration-tests/src/test/java/de/cuioss/sheriff/gateway/integration/*
Adds a dedicated low-cap gateway instance and verifies sequential WebSocket relays release permits and preserve HTTP admission capacity.

Benchmark and documentation updates

Layer / File(s) Summary
Benchmark execution coverage
.github/workflows/benchmark.yml, benchmarks/README.adoc, benchmarks/pom.xml
Changes websocketEcho to an executing goal, updates expected coverage to eleven goals, and retains only uploadLarge as deliberately skipped.
Admission-budget documentation
doc/adr/*, doc/configuration.adoc, doc/development/protocol-processors.adoc, doc/user/protocol-routes.adoc, .plan/project-architecture/benchmarks/enriched.json
Documents the new configuration fields, defaults, validation constraints, WebSocket permit lifecycle, relay sub-budget behavior, and benchmark coverage metadata.

Estimated code review effort: 4 (Complex) | ~45 minutes

Possibly related PRs

  • cuioss/API-Sheriff#60: Earlier configuration-schema and gateway configuration work related to the added edge_hardening schema and validation.
  • cuioss/API-Sheriff#123: Updates the same benchmark coverage and websocketEcho skip expectations.
🚥 Pre-merge checks | ✅ 2
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title accurately captures the main fix (WebSocket permit release) and the new edge-hardening budget/config addition.

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@cuioss-review-bot

Copy link
Copy Markdown

PR Reviewer Guide 🔍

🧪 PR contains tests
🔒 No security concerns identified
⚡ No major issues detected

@coderabbitai coderabbitai Bot 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.

Actionable comments posted: 2

🧹 Nitpick comments (2)
integration-tests/scripts/start-integration-container.sh (1)

218-220: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

The gateway-instance set is re-enumerated by hand in two scripts. The authoritative definition is the api-sheriff* services (and their :9000 publications) in integration-tests/docker-compose.yml; both scripts copy that set into a literal list guarded only by a "keep in lockstep" comment, so a newly added instance is silently unwaited and undiagnosable.

  • integration-tests/scripts/start-integration-container.sh#L218-L220: derive the service names and management ports from compose (e.g. docker compose ps --services filtered on ^api-sheriff plus docker compose port <svc> 9000) instead of the literal service:port pairs.
  • integration-tests/scripts/dump-keycloak-logs.sh#L54-L58: derive the container names from the running compose project (e.g. docker ps --format '{{.Names}}' | grep api-sheriff) instead of the literal integration-tests-api-sheriff-*-1 list.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@integration-tests/scripts/start-integration-container.sh` around lines 218 -
220, The gateway instance lists are duplicated instead of being derived from the
running Compose configuration. In
integration-tests/scripts/start-integration-container.sh:218-220, replace the
literal service/port pairs in the gateway_instance loop with api-sheriff
services discovered via docker compose and management ports resolved from each
service’s published 9000 port. In
integration-tests/scripts/dump-keycloak-logs.sh:54-58, replace the literal
container-name list with api-sheriff container names discovered from the running
Compose project.

Source: Path instructions

integration-tests/src/test/java/de/cuioss/sheriff/gateway/integration/WsAdmissionActivationWiringTest.java (1)

67-78: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Both constants mirror authoritative values elsewhere without deriving them.

WS_ADMISSION_PORT_PREFIX must track the test.ws.admission.port failsafe property in integration-tests/pom.xml, and SEQUENTIAL_UPGRADES must track WebSocketProxyIT.LOW_CAP_SEQUENTIAL_UPGRADES. Both are copied by hand, so a change on either authoritative side leaves this guard asserting the wrong thing — and the upgrade count is precisely what makes cap drift detectable, so its divergence silently weakens the check. Reading the port from System.getProperty("test.ws.admission.port") and referencing the IT's public constant (or hosting one shared constant) closes both.

As per path instructions: "Treat a hardcoded list that must mirror a set defined elsewhere … as a defect unless it is derived from that source at build or run time. Name the authoritative definition and the drift it permits."

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In
`@integration-tests/src/test/java/de/cuioss/sheriff/gateway/integration/WsAdmissionActivationWiringTest.java`
around lines 67 - 78, Replace the duplicated WS_ADMISSION_PORT_PREFIX value in
WsAdmissionActivationWiringTest with the authoritative
System.getProperty("test.ws.admission.port") value from
integration-tests/pom.xml, adapting it to the existing prefix usage. Replace
SEQUENTIAL_UPGRADES with WebSocketProxyIT.LOW_CAP_SEQUENTIAL_UPGRADES, or move
both values to a shared authoritative constant, so changes to either source
cannot silently weaken the regression check.

Source: Path instructions

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In @.github/workflows/benchmark.yml:
- Line 154: Replace the hardcoded expected benchmark names in the workflow’s
validation step with values derived at build or run time from the authoritative
run-k6-* executions or manifest in benchmarks/pom.xml. Ensure the generated set
is used for summary comparison so additions, removals, and renames remain
synchronized automatically.

In
`@api-sheriff/src/main/java/de/cuioss/sheriff/gateway/config/model/EdgeHardeningConfig.java`:
- Around line 82-87: Update EdgeHardeningConfig.effectiveWebsocketRelayCap() so
an omitted relay cap defaults to the effective admission cap when that is lower
than DEFAULT_WEBSOCKET_RELAY_CAP, while preserving explicitly declared relay
caps. Add a regression test covering edge_hardening with only admission_cap set
and verify ConfigValidator accepts it.

---

Nitpick comments:
In `@integration-tests/scripts/start-integration-container.sh`:
- Around line 218-220: The gateway instance lists are duplicated instead of
being derived from the running Compose configuration. In
integration-tests/scripts/start-integration-container.sh:218-220, replace the
literal service/port pairs in the gateway_instance loop with api-sheriff
services discovered via docker compose and management ports resolved from each
service’s published 9000 port. In
integration-tests/scripts/dump-keycloak-logs.sh:54-58, replace the literal
container-name list with api-sheriff container names discovered from the running
Compose project.

In
`@integration-tests/src/test/java/de/cuioss/sheriff/gateway/integration/WsAdmissionActivationWiringTest.java`:
- Around line 67-78: Replace the duplicated WS_ADMISSION_PORT_PREFIX value in
WsAdmissionActivationWiringTest with the authoritative
System.getProperty("test.ws.admission.port") value from
integration-tests/pom.xml, adapting it to the existing prefix usage. Replace
SEQUENTIAL_UPGRADES with WebSocketProxyIT.LOW_CAP_SEQUENTIAL_UPGRADES, or move
both values to a shared authoritative constant, so changes to either source
cannot silently weaken the regression check.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository: cuioss/coderabbit/.coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: 52c21658-caaa-4590-8f1f-999b0cce0047

📥 Commits

Reviewing files that changed from the base of the PR and between d8a18d9 and e97e97d.

📒 Files selected for processing (29)
  • .github/workflows/benchmark.yml
  • api-sheriff/src/main/java/de/cuioss/sheriff/gateway/config/model/EdgeHardeningConfig.java
  • api-sheriff/src/main/java/de/cuioss/sheriff/gateway/config/model/GatewayConfig.java
  • api-sheriff/src/main/java/de/cuioss/sheriff/gateway/config/validation/ConfigValidator.java
  • api-sheriff/src/main/java/de/cuioss/sheriff/gateway/edge/EdgeHardeningOptions.java
  • api-sheriff/src/main/java/de/cuioss/sheriff/gateway/edge/GatewayEdgeRoute.java
  • api-sheriff/src/main/java/de/cuioss/sheriff/gateway/edge/WebSocketRelayStage.java
  • api-sheriff/src/main/java/de/cuioss/sheriff/gateway/quarkus/ConfigModelReflection.java
  • api-sheriff/src/main/java/de/cuioss/sheriff/gateway/quarkus/ConfigProducer.java
  • api-sheriff/src/main/resources/schema/gateway.schema.json
  • api-sheriff/src/test/java/de/cuioss/sheriff/gateway/config/model/ConfigModelContractTest.java
  • api-sheriff/src/test/java/de/cuioss/sheriff/gateway/config/validation/ConfigValidatorTest.java
  • api-sheriff/src/test/java/de/cuioss/sheriff/gateway/edge/EdgeHardeningTest.java
  • api-sheriff/src/test/java/de/cuioss/sheriff/gateway/edge/GatewayEdgeRouteTest.java
  • api-sheriff/src/test/java/de/cuioss/sheriff/gateway/edge/WebSocketRelayStageTest.java
  • api-sheriff/src/test/java/de/cuioss/sheriff/gateway/quarkus/ConfigProducerTest.java
  • api-sheriff/src/test/java/de/cuioss/sheriff/gateway/quarkus/SheriffMetricsTest.java
  • benchmarks/README.adoc
  • benchmarks/pom.xml
  • doc/configuration.adoc
  • doc/development/protocol-processors.adoc
  • doc/user/protocol-routes.adoc
  • integration-tests/docker-compose.yml
  • integration-tests/pom.xml
  • integration-tests/scripts/dump-keycloak-logs.sh
  • integration-tests/scripts/start-integration-container.sh
  • integration-tests/src/main/docker/sheriff-config-ws-admission/gateway.yaml
  • integration-tests/src/test/java/de/cuioss/sheriff/gateway/integration/WebSocketProxyIT.java
  • integration-tests/src/test/java/de/cuioss/sheriff/gateway/integration/WsAdmissionActivationWiringTest.java

Comment thread .github/workflows/benchmark.yml
@cuioss-oliver

Copy link
Copy Markdown
Collaborator Author

Triage dispositions

In reply to comment_id: PRR_kwDOPatrT88AAAABHnDDXg

Both nitpicks reviewed against the worktree; declining both, with reasoning. (1) start-integration-container.sh:218-220 / dump-keycloak-logs.sh:54-58 - deriving the gateway set from compose is feasible but a net loss here: api-sheriff and api-sheriff-mtls are already waited for separately above this loop with their own richer diagnostics, so a grep '^api-sheriff' derivation would either re-wait them or need a hand-maintained exclusion list, relocating the duplication rather than removing it; and the loop needs the MANAGEMENT port, so it would additionally depend on every instance publishing 9000 plus parsing docker compose port output. The drift failure mode is also loud and immediate, not silent - an unwaited instance surfaces as a connection refusal in the IT phase, as the comment above the loop already states. Trivial-severity cleanup that adds more failure modes than it removes. (2) WsAdmissionActivationWiringTest:67-78 - the port half of this suggestion does not work: test.ws.admission.port is declared only in the maven-failsafe-plugin systemPropertyVariables block (integration-tests/pom.xml:264), and WsAdmissionActivationWiringTest is a *Test run by surefire, not a *IT, so System.getProperty("test.ws.admission.port") is null there - the change would either NPE or silently fall back to a literal default, destroying the very drift check it is meant to strengthen. The SEQUENTIAL_UPGRADES half is a real (if narrow) drift risk, but WebSocketProxyIT.LOW_CAP_SEQUENTIAL_UPGRADES is private, so applying it means widening an IT's private constant purely to serve a sibling test, or introducing a new shared constant holder - a structural change out of proportion to a Trivial nitpick on test-support code whose coupling is already documented in the constant's own Javadoc. Left as-is deliberately; happy to revisit if the two counts ever actually diverge.

@cuioss-oliver
cuioss-oliver added this pull request to the merge queue Jul 29, 2026
@github-merge-queue
github-merge-queue Bot removed this pull request from the merge queue due to failed status checks Jul 29, 2026
cuioss-oliver and others added 11 commits July 29, 2026 13:44
GatewayEdgeRoute.handle registers its sole admission release inside ctx.addEndHandler. A completed WebSocket upgrade takes the connection over, so the HTTP response never ends, the end handler never fires, and one of the gateway's admission permits is stranded for the process lifetime — a sustained WebSocket workload degrades the gateway to blanket 503s.

Stash the existing releaseAdmission CAS guard on the RoutingContext under a new key (the same idiom as ROUTE_KEY, and the seam that already crosses the virtual-thread hop), read it back in dispatchWebSocket, and hand WebSocketRelayStage a release callback. The relay invokes it on both teardown paths: the established relay's single idempotent RelaySession.closeBoth funnel, and the client-upgrade-failure branch that constructs no RelaySession at all. It is never invoked at upgrade completion, which would under-count concurrent relays. The upstream-dial-failure path is unchanged — it ends the response, so the end handler still releases through the same guard, and the shared CAS guard keeps any overlap from double-releasing.
…t relay sub-cap

A WebSocket relay now holds its admission permit for the connection's whole lifetime, so a modest number of long-lived relays could consume the general pool and starve HTTP — trading the permit leak for a slow squeeze. Bound them separately.

Adds the operator-facing edge_hardening block: admission_cap (previously the hard-coded 2048 in EdgeHardeningOptions) and websocket_relay_cap, a sub-budget acquired in addition to the general permit rather than instead of it. Wired through the whole config chain — the EdgeHardeningConfig record, the GatewayConfig component, the JSON schema (whose root additionalProperties:false would otherwise refuse the block at boot), native reflection registration, and a boot validator requiring both caps >= 1 with the relay cap within the pool it draws from. EdgeHardeningOptions is now supplied by a ConfigProducer producer method behind the same buildOnce() guard, so the caps come from the single boot-time assembly; an omitted block resolves to the documented defaults, preserving today's behaviour. GatewayEdgeRoute acquires the sub-permit before the relay hand-off, refuses 503 beyond it, and releases both permits together through the deliverable-1 CAS guard so neither a refusal nor a dial failure can strand one.
Seven passing WebSocket tests coexisted with a permit leak on every established relay, because each performs at most one handshake and asserts only that one response — three leaked permits against a 2048-permit pool is invisible by construction. A per-request correctness assertion cannot observe cumulative resource exhaustion.

Adds the assertion of the shape that can: N sequential relays, then a claim about the edge's remaining capacity. It needs a budget small enough to exhaust, and the admission budget is a property of the whole gateway process, so the caps live on a dedicated low-cap instance (api-sheriff-ws-admission, port 10447, admission_cap 8 / websocket_relay_cap 2) rather than on the primary instance that serves the rest of the suite. Ten sequential upgrades exceed both caps, so a leak of either permit fails the run; a following plain HTTP request proves the general permit came back too. The relay releases both permits before closing either leg, so awaiting the close frame makes each iteration race-free rather than timed.

WsAdmissionActivationWiringTest is the guard against that regression quietly stopping: it is a no-Docker descriptor assertion that the overlay is mounted, the instance is defined, and both caps stay below the ten upgrades actually driven — because against the shipped defaults the exhaustion test passes whether or not the permits are ever returned, and no black-box assertion inside the IT can tell the difference.

Also records the blind-spot analysis in the suite's own javadoc, and corrects the benchmark lane's stack-startup budget comment, which still claimed four gateway instances.
…k is fixed

The goal was suppressed because a sustained WebSocket load run exhausted the edge admission pool and left the gateway 503-ing all traffic until restart — which would also have poisoned every goal sequenced after it. The relay now releases its permit from its own teardown funnel, so the aspect measures relay throughput instead of the exhaustion path.

Flips skip.benchmark.websocket.echo to false. The property is kept rather than deleted so the goal stays individually suppressible without editing the execution, and the goal keeps its defense-in-depth position at the end of the profile.

Re-derives every count and narrative the flip invalidates rather than only the property: the CI lane now expects ELEVEN of the twelve declared goals to produce a result, so websocketEcho joins the coverage step's expected list — without which the goal could vanish from a run and the job would still pass — and leaves the "Deliberately skipped" table, where only uploadLarge remains. The goal-execution budget term and the README's coverage and trend-series passages move with it. Every uploadLarge / skip.benchmark.upload.large row is left byte-unchanged; that goal is still blocked on its own gateway defect.
…ayers

A configuration key that exists only in the schema is a key operators cannot find, so the new block is documented at each of the three layers that answer a different question about it.

doc/configuration.adoc is the reference: the gateway.yaml sample gains the block with both keys commented, and the "Edge hardening defaults" section is rewritten around the distinction the block introduces — the framing and drain bounds remain fixed in code, while the two admission budgets are now operator-owned, because their sizing depends on the deployment's traffic shape rather than on protocol safety. That section previously claimed the whole set was applied in code "not by gateway.yaml"; the claim is now false and is gone. A Field Reference entry and two Validation Rules entries (both caps >= 1, and the relay cap within the pool it draws from) complete the reference surface.

doc/user/protocol-routes.adoc answers the operator's actual question — what to set it to. A relay holds its permit for the connection's lifetime rather than a round-trip, so relays accumulate against the pool HTTP draws from; size the sub-cap to peak concurrent relays plus reconnect-storm headroom, and read admission_cap minus websocket_relay_cap as the request concurrency guaranteed to survive a saturated relay pool.

doc/development/protocol-processors.adoc records the permit lifecycle as a contract, because this is the one dispatch path where the edge's ordinary release site does not work: a completed upgrade takes the connection over, so ctx.addEndHandler never fires. Names all three release sites and the reason both permits must be returned together.
…ources

Formatter and import-sort output the quality gate produced against the files the two preceding commits touched: import-group separation in EdgeHardeningOptions and EdgeHardeningTest, continuation-indent normalisation in the two WebSocket test helpers. No behavioural change; committed so the tree the gate produces and the tree in the branch agree.
Sonar java:S7467 - the ExecutionException binding in connectWhenAdmitted is never read, so replace it with the Java 25 unnamed pattern.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

Claude-Session: https://claude.ai/code/session_01UV4pXNUtk5CKceRLWxvapz
…admission cap

A partially declared edge_hardening block self-rejected at boot: with
admission_cap lowered below 512 and websocket_relay_cap omitted, the relay
cap resolved to the fixed DEFAULT_WEBSOCKET_RELAY_CAP (512) while
ConfigValidator compares the resolved pair, so the block was refused with
'websocket_relay_cap 512 must not exceed admission_cap 64' - a configuration
the class Javadoc documents as valid.

effectiveWebsocketRelayCap() now resolves an absent member to
Math.max(1, effectiveAdmissionCap() / 4) instead of the fixed constant. That
preserves the documented three-quarters-for-HTTP reservation at every
admission cap (a min-clamp against 512 would resolve relay == admission below
512, letting relays consume the whole general pool) and is byte-identical at
the shipped default, since 2048 / 4 == 512. An explicitly declared relay cap
is untouched, so an explicit cap above admission_cap is still refused at boot.

Javadoc, doc/configuration.adoc and doc/user/protocol-routes.adoc updated to
describe the derived default. EdgeHardeningTest.fallsBackPerOmittedMember
asserted the old fixed-constant contract at admission_cap 4096 and now asserts
the derived 1024; the four default-cap assertions are unchanged.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UV4pXNUtk5CKceRLWxvapz
Promoted from a recurring finalize disposition: review-bot suggestions to
derive the benchmark coverage expected= list from benchmarks/pom.xml were
declined twice in this plan, because independence from the pom is the
property the check relies on.

Co-Authored-By: Claude <noreply@anthropic.com>
Records why a WebSocket relay holds its general admission permit for the
connection lifetime and additionally acquires a derived websocket_relay_cap
sub-budget, and why the omitted relay cap derives proportionally from the
admission cap rather than being clamped to a constant.

Co-Authored-By: Claude <noreply@anthropic.com>
PR #125 landed ADR-0018 and ADR-0019 while this branch was queued, so the
number this ADR was authored under is taken. The rebase was conflict-free
because the filenames differ, which is exactly why the collision had to be
caught by inspection rather than by git.

Co-Authored-By: Claude <noreply@anthropic.com>
@cuioss-oliver
cuioss-oliver force-pushed the feature/websocket-admission-permit-leak branch from 2b69abf to 27866fa Compare July 29, 2026 11:54

@coderabbitai coderabbitai Bot 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.

Actionable comments posted: 5

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In @.plan/project-architecture/benchmarks/enriched.json:
- Around line 3-5: Replace the hand-maintained benchmark goal list described in
the insights entry with an automated consistency mechanism tied to the
authoritative run-k6-* goals in benchmarks/pom.xml. Ensure the workflow coverage
oracle detects both newly added or un-skipped goals and removed goals, rather
than silently accepting divergence between the Maven definitions and
benchmark.yml.

In
`@api-sheriff/src/test/java/de/cuioss/sheriff/gateway/config/validation/ConfigValidatorTest.java`:
- Around line 433-446: Extend shouldAcceptLoweredAdmissionCapWithoutRelayCap or
add a focused test covering an omitted websocket_relay_cap with admission_cap
set to 1, and assert effectiveWebsocketRelayCap() returns 1 rather than a
quarter-rounded value of 0. Preserve the existing validation-success assertion
and use the existing EdgeHardeningConfig and gateway setup patterns.

In
`@doc/adr/0020-WebSocket_relays_hold_their_admission_permit_for_the_connection_lifetime_and_are_bounded_by_a_derived_sub-budget.adoc`:
- Around line 74-90: Ensure the configuration validation and defaulting
described in the ADR enforce a strict websocket_relay_cap < admission_cap
relationship, including the derived default when admission_cap is 1. Update the
related guarantees and examples in the affected sections to state the required
minimum admission cap or qualify HTTP-headroom claims when separation is not
guaranteed; keep all references consistent.

In `@integration-tests/scripts/start-integration-container.sh`:
- Around line 218-220: Replace the hardcoded gateway_instance inventory in the
startup loop with entries derived from the authoritative gateway service and
management-port mappings in integration-tests/docker-compose.yml, or consume a
shared/generated manifest. Preserve the existing GATEWAY_SERVICE and
GATEWAY_MGMT_PORT parsing while ensuring future Compose additions or port
changes are automatically included.
- Around line 221-223: Update the readiness polling loop around the gateway curl
check to request the Quarkus readiness endpoint /q/health/ready instead of
/q/health/live. Keep the existing retry and success behavior unchanged so
startup waits for GatewayReadinessCheck validation to complete.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository: cuioss/coderabbit/.coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: 632c9457-0b56-4847-b345-c60aeed4414c

📥 Commits

Reviewing files that changed from the base of the PR and between 245a180 and 27866fa.

📒 Files selected for processing (31)
  • .github/workflows/benchmark.yml
  • .plan/project-architecture/benchmarks/enriched.json
  • api-sheriff/src/main/java/de/cuioss/sheriff/gateway/config/model/EdgeHardeningConfig.java
  • api-sheriff/src/main/java/de/cuioss/sheriff/gateway/config/model/GatewayConfig.java
  • api-sheriff/src/main/java/de/cuioss/sheriff/gateway/config/validation/ConfigValidator.java
  • api-sheriff/src/main/java/de/cuioss/sheriff/gateway/edge/EdgeHardeningOptions.java
  • api-sheriff/src/main/java/de/cuioss/sheriff/gateway/edge/GatewayEdgeRoute.java
  • api-sheriff/src/main/java/de/cuioss/sheriff/gateway/edge/WebSocketRelayStage.java
  • api-sheriff/src/main/java/de/cuioss/sheriff/gateway/quarkus/ConfigModelReflection.java
  • api-sheriff/src/main/java/de/cuioss/sheriff/gateway/quarkus/ConfigProducer.java
  • api-sheriff/src/main/resources/schema/gateway.schema.json
  • api-sheriff/src/test/java/de/cuioss/sheriff/gateway/config/model/ConfigModelContractTest.java
  • api-sheriff/src/test/java/de/cuioss/sheriff/gateway/config/validation/ConfigValidatorTest.java
  • api-sheriff/src/test/java/de/cuioss/sheriff/gateway/edge/EdgeHardeningTest.java
  • api-sheriff/src/test/java/de/cuioss/sheriff/gateway/edge/GatewayEdgeRouteTest.java
  • api-sheriff/src/test/java/de/cuioss/sheriff/gateway/edge/WebSocketRelayStageTest.java
  • api-sheriff/src/test/java/de/cuioss/sheriff/gateway/quarkus/ConfigProducerTest.java
  • api-sheriff/src/test/java/de/cuioss/sheriff/gateway/quarkus/SheriffMetricsTest.java
  • benchmarks/README.adoc
  • benchmarks/pom.xml
  • doc/adr/0020-WebSocket_relays_hold_their_admission_permit_for_the_connection_lifetime_and_are_bounded_by_a_derived_sub-budget.adoc
  • doc/configuration.adoc
  • doc/development/protocol-processors.adoc
  • doc/user/protocol-routes.adoc
  • integration-tests/docker-compose.yml
  • integration-tests/pom.xml
  • integration-tests/scripts/dump-keycloak-logs.sh
  • integration-tests/scripts/start-integration-container.sh
  • integration-tests/src/main/docker/sheriff-config-ws-admission/gateway.yaml
  • integration-tests/src/test/java/de/cuioss/sheriff/gateway/integration/WebSocketProxyIT.java
  • integration-tests/src/test/java/de/cuioss/sheriff/gateway/integration/WsAdmissionActivationWiringTest.java
🚧 Files skipped from review as they are similar to previous changes (27)
  • api-sheriff/src/main/java/de/cuioss/sheriff/gateway/quarkus/ConfigModelReflection.java
  • doc/development/protocol-processors.adoc
  • doc/user/protocol-routes.adoc
  • api-sheriff/src/main/java/de/cuioss/sheriff/gateway/config/model/GatewayConfig.java
  • api-sheriff/src/main/resources/schema/gateway.schema.json
  • api-sheriff/src/test/java/de/cuioss/sheriff/gateway/quarkus/SheriffMetricsTest.java
  • integration-tests/scripts/dump-keycloak-logs.sh
  • api-sheriff/src/main/java/de/cuioss/sheriff/gateway/config/model/EdgeHardeningConfig.java
  • doc/configuration.adoc
  • api-sheriff/src/main/java/de/cuioss/sheriff/gateway/config/validation/ConfigValidator.java
  • api-sheriff/src/main/java/de/cuioss/sheriff/gateway/edge/WebSocketRelayStage.java
  • api-sheriff/src/test/java/de/cuioss/sheriff/gateway/config/model/ConfigModelContractTest.java
  • integration-tests/docker-compose.yml
  • benchmarks/pom.xml
  • integration-tests/src/main/docker/sheriff-config-ws-admission/gateway.yaml
  • api-sheriff/src/main/java/de/cuioss/sheriff/gateway/quarkus/ConfigProducer.java
  • .github/workflows/benchmark.yml
  • integration-tests/pom.xml
  • api-sheriff/src/main/java/de/cuioss/sheriff/gateway/edge/EdgeHardeningOptions.java
  • benchmarks/README.adoc
  • api-sheriff/src/test/java/de/cuioss/sheriff/gateway/quarkus/ConfigProducerTest.java
  • api-sheriff/src/test/java/de/cuioss/sheriff/gateway/edge/WebSocketRelayStageTest.java
  • integration-tests/src/test/java/de/cuioss/sheriff/gateway/integration/WebSocketProxyIT.java
  • api-sheriff/src/test/java/de/cuioss/sheriff/gateway/edge/EdgeHardeningTest.java
  • integration-tests/src/test/java/de/cuioss/sheriff/gateway/integration/WsAdmissionActivationWiringTest.java
  • api-sheriff/src/test/java/de/cuioss/sheriff/gateway/edge/GatewayEdgeRouteTest.java
  • api-sheriff/src/main/java/de/cuioss/sheriff/gateway/edge/GatewayEdgeRoute.java

Comment thread .plan/project-architecture/benchmarks/enriched.json
Comment thread integration-tests/scripts/start-integration-container.sh
Comment thread integration-tests/scripts/start-integration-container.sh
@cuioss-oliver

Copy link
Copy Markdown
Collaborator Author

Triage dispositions

In reply to comment_id: PRR_kwDOPatrT88AAAABHnDDXg

Both nitpicks reviewed against the worktree; declining both, with reasoning. (1) start-integration-container.sh:218-220 / dump-keycloak-logs.sh:54-58 - deriving the gateway set from compose is feasible but a net loss here: api-sheriff and api-sheriff-mtls are already waited for separately above this loop with their own richer diagnostics, so a grep '^api-sheriff' derivation would either re-wait them or need a hand-maintained exclusion list, relocating the duplication rather than removing it; and the loop needs the MANAGEMENT port, so it would additionally depend on every instance publishing 9000 plus parsing docker compose port output. The drift failure mode is also loud and immediate, not silent - an unwaited instance surfaces as a connection refusal in the IT phase, as the comment above the loop already states. Trivial-severity cleanup that adds more failure modes than it removes. (2) WsAdmissionActivationWiringTest:67-78 - the port half of this suggestion does not work: test.ws.admission.port is declared only in the maven-failsafe-plugin systemPropertyVariables block (integration-tests/pom.xml:264), and WsAdmissionActivationWiringTest is a *Test run by surefire, not a *IT, so System.getProperty("test.ws.admission.port") is null there - the change would either NPE or silently fall back to a literal default, destroying the very drift check it is meant to strengthen. The SEQUENTIAL_UPGRADES half is a real (if narrow) drift risk, but WebSocketProxyIT.LOW_CAP_SEQUENTIAL_UPGRADES is private, so applying it means widening an IT's private constant purely to serve a sibling test, or introducing a new shared constant holder - a structural change out of proportion to a Trivial nitpick on test-support code whose coupling is already documented in the constant's own Javadoc. Left as-is deliberately; happy to revisit if the two counts ever actually diverge.

In reply to comment_id: IC_kwDOPatrT88AAAABMOTZXQ

Not an actionable review comment: this is the plan's OWN round-2 triage-response comment (authored by cuioss-oliver via github_pr post_responses) being re-ingested by the round-3 FIND pass. Responding to it would post a reply to our own reply. No code change and no provider response warranted. The three enabled review bots (coderabbit, sourcery, pr-agent) are all accounted for as responded_bots and the completeness guard returned complete:true, so no genuine bot feedback is outstanding.

In reply to comment_id: PRR_kwDOPatrT88AAAABHpGnWA

Acknowledged - this is the review summary; the five inline comments it aggregates are each dispositioned individually on their own threads. Net outcome this round: no code change. Two comments re-raise points already settled earlier on this PR (the benchmark.yml expected= oracle and the gateway-instance wait loop). One is factually incorrect - the implicit relay-cap floor is already covered by ConfigModelContractTest.edgeHardeningConfigFloorsDerivedRelayCapAtOne, which asserts effectiveWebsocketRelayCap() == 1 at admission_cap 1. One is declined on the merits - the ADR separation claim is not contradicted at a total budget of one permit, where separation is undefined rather than violated. One - the liveness-versus-readiness startup gate - is a valid observation deferred to a follow-up, because the correct fix spans all four wait sites in that script including two that predate this PR and changes a startup gate to a network-dependent one under a budget sized for liveness.

@cuioss-oliver
cuioss-oliver added this pull request to the merge queue Jul 29, 2026
Merged via the queue into main with commit 23949a9 Jul 29, 2026
56 checks passed
@cuioss-oliver
cuioss-oliver deleted the feature/websocket-admission-permit-leak branch July 29, 2026 12:33
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