75 lines
4.2 KiB
Markdown
75 lines
4.2 KiB
Markdown
# ADR-WS-003: STOMP is an Advanced adapter with a declared destination catalog
|
||
|
||
- Status: Accepted
|
||
- Date: 2026-08-25
|
||
- Scope: `adapter:inbound:websocket` — `advanced.stomp`, `advanced.stomp.rabbit`
|
||
|
||
## Context
|
||
|
||
Advanced Tasks 9–13 add STOMP 1.2 alongside the platform's own protocol, plus a RabbitMQ broker
|
||
relay and cross-node user destinations.
|
||
|
||
This leaf already ships an older STOMP-over-SockJS channel (`stomp`, gated on
|
||
`ca-skeleton.websocket.enabled`). Two `@EnableWebSocketMessageBroker` configurations in one context
|
||
do not conflict loudly — both contribute a configurer, both call `configureMessageBroker`, and the
|
||
broker that results is whichever ran last. Nothing errors and nothing logs.
|
||
|
||
STOMP also brings a destination model that is a free string from the client. Without a catalog, the
|
||
set of reachable destinations is whatever the broker accepts, which for the simple broker is every
|
||
string.
|
||
|
||
## Decision
|
||
|
||
**The Advanced adapter is its own module (`advanced-stomp`), separate from `advanced`.** It is the
|
||
one Advanced capability that cannot be pure — STOMP here *is* the Spring Messaging types — and
|
||
folding it into `advanced` would relax that module's purity for every capability in it.
|
||
|
||
**The relay is a further module (`advanced-stomp-rabbit`).** The adapter parses a protocol; the
|
||
relay opens a TCP connection to somebody else's broker and makes every delivery depend on it.
|
||
Different blast radius, different decision, different module.
|
||
|
||
**Destinations are declared, per operation.** `StompDestinationCatalog` maps `(operation,
|
||
destination)` to a required permission. Undeclared is refused. `SUBSCRIBE` and `SEND` are separate
|
||
declarations, because reading a feed and publishing into it are different rights.
|
||
|
||
**The authorization decision is a value, not an interceptor method.** `StompAuthorizationPolicy`
|
||
returns a `StompAuthorizationDecision`; `StompSecurityInterceptor` only extracts and enforces. A rule
|
||
reachable only through a `MessageChannel` gets tested for the cases somebody built a channel for.
|
||
|
||
**Only one STOMP runtime may run.** `StompBrokerExclusivity` fails the context when both channels
|
||
are enabled, when both brokers are, or when the adapter is enabled with no broker behind it.
|
||
|
||
**A `RECEIPT` is never promoted to a commit.** `StompEvidence` has six stages and
|
||
`StompAckPolicy.evidenceForReceipt()` is fixed at `PROTOCOL_RECEIPT`. The receipt is written by the
|
||
protocol layer, which knows nothing about whether the work succeeded.
|
||
|
||
**The simple broker declares what it cannot do.** `SimpleBrokerProfile` cannot be constructed
|
||
claiming cluster support or durable acks, and refuses activation outside local/test — in a
|
||
multi-node deployment it does not error, it delivers to whichever fraction of users is on the
|
||
publishing node.
|
||
|
||
**Unresolved user destinations are broadcast once and then dead-lettered.**
|
||
`MultiNodeUserDestination` distinguishes a message that arrived *via* the broadcast from one that did
|
||
not. Without that, every node rebroadcasts every unresolvable message on receipt.
|
||
|
||
## Consequences
|
||
|
||
- Enabling Advanced STOMP requires disabling the legacy channel. There is no migration path that
|
||
runs both; the exclusivity check makes that explicit at startup rather than at 3am.
|
||
- A deployment must write its own catalog. There is deliberately no default: an empty one refuses
|
||
every frame and reads as a broken adapter, and a non-empty one publishes destinations nobody chose.
|
||
- The relay's cost is one broker connection per authenticated session plus one system connection.
|
||
`brokerConnectionsFor` exists so this is computed before the first outage.
|
||
- User-destination metrics are tagged with `UserDestinationAction`, never the destination — a user
|
||
destination contains a user identifier by construction.
|
||
|
||
## Alternatives considered
|
||
|
||
- **Extend the existing `stomp` package.** Rejected: it is Stable, and WS-ARCH-6 forbids a Stable
|
||
module naming an Advanced one. Making the legacy channel profile-driven would have required that
|
||
edge.
|
||
- **One `advanced-stomp` module including the relay.** Rejected: the relay is a separate operational
|
||
decision and deserves to be refusable on its own.
|
||
- **Allow undeclared destinations with a wildcard permission.** Rejected: the wildcard becomes the
|
||
default and the catalog becomes documentation.
|