KubeMQ
ConnectorsSTOMPConcepts

Architecture

Inside the STOMP connector — the embedded STOMP server, the frame codec, version negotiation, handler dispatch, and the destination → pattern → channel router.

The KubeMQ STOMP connector is an embedded STOMP server hosted inside kubemq-server. It has its own dedicated TCP/TLS listeners (default plain TCP 61613, TLS 61614), a hand-rolled frame codec (no third-party STOMP server library), per-version negotiation (1.0/1.1/1.2), and a destination router that bridges the STOMP wire protocol onto KubeMQ's five patterns by destination prefix. It is built and started by default (CONNECTORS_STOMP_ENABLE=true).

A STOMP frame travels from a native client library, through the embedded STOMP server, onto one of KubeMQ's five native messaging patterns, and out to the message broker — and back. A STOMP client never touches proto, never touches a KubeMQ SDK, and never knows what the internal broker is; it speaks plain STOMP over TCP and the connector does the bridging.

The protocol stack

The connector is a thin, faithful STOMP endpoint in front of the KubeMQ array. Everything below the array — the Events/Queues/Commands/Queries machinery and the message broker — is the same engine every other KubeMQ transport (gRPC, REST, MQTT, AMQP) sits on top of.

The same array path the gRPC connector uses; the destination prefix selects the pattern and the remaining segments (slash→dot) become the KubeMQ channel.

The pieces, top to bottom:

  • TCP / TLS listener — binds all interfaces on 61613 / 61614 via an availability-first loader: a bind failure logs an error and the server continues without STOMP rather than crashing.
  • Frame codec — a hand-rolled codec for the STOMP frame shape (COMMAND\n + name:value\n lines + a blank line + the body + a NUL terminator). It applies per-version header escaping and is binary-safe via a content-length-aware reader; the writer always stamps content-length on egress.
  • CONNECT / auth / heartbeat — the handshake: negotiate the protocol version, check the connection limit, authenticate, derive the client id, and set up the heartbeat watchdog, then reply with CONNECTED.
  • Handlers — dispatch each client frame: SEND, SUBSCRIBE, UNSUBSCRIBE, ACK, NACK, and RPC dispatch.
  • Destination router — the heart of the bridge: the first destination segment selects the pattern and the remaining segments are .-joined into the channel.
  • Bridges & registry — the queue bridge, RPC bridge, and subscription registry sit between the handlers and the KubeMQ array.

Version negotiation

The connector supports STOMP 1.0, 1.1, and 1.2 and picks the highest common version from the client's CONNECT accept-version header (comma-separated, order-independent, whitespace-tolerant). A missing or empty header resolves to 1.0; no common version returns an ERROR frame and closes. The examples request accept-version:1.2, which is the recommended default — only 1.2 can always represent every header on egress (1.0 and 1.1 silently drop unrepresentable CR/LF in header values). The CONNECTED reply carries the negotiated version, the server's advertised heart-beat (sx,sy), a derived session id, and server = KubeMQ/<version>.

The heartbeat is governed by HeartbeatMs (advertised sx=sy, default 10000 ms). The dead-peer cutoff is 2× the negotiated client→server interval — the client must send a frame (or a bare newline) before that window elapses, or the watchdog force-closes the connection. Any inbound byte refreshes liveness.

Destination → pattern → channel mapping

This is the single most important mental model. The first destination segment selects a KubeMQ pattern (case-sensitive); the remaining segments are .-joined into the KubeMQ channel. ActiveMQ-style primary names are the canonical form; MQTT-style aliases map to the same patterns. On egress the connector always canonicalizes back to the primary name.

The destination prefix selects the KubeMQ pattern; the remaining segments (slash→dot) become the channel.

STOMP destination (ingress)AliasPatternKubeMQ channelCanonical egress (always primary)
/queue/orders/new/queues/Queuesorders.new/queue/orders/new
/topic/a/b/c/events/Eventsa.b.c/topic/a/b/c
/topic-store/audit/store/Events-Storeaudit/topic-store/audit
/command/exec/commands/Commands (RPC)exec/command/exec
/query/lookup/queries/Queries (RPC)lookup/query/lookup
/reply/r1(none)reply (connection-local)r1/reply/r1
sensor/temp (bare)events (via DefaultPattern)sensor.temp/topic/sensor/temp

The parse algorithm: the 512-byte length limit is checked on the raw destination string first; strip exactly one leading /; split on / (any empty segment is rejected); the first segment selects the pattern via a case-sensitive map; the remaining segments are .-joined into the channel; an unknown first segment falls back to DefaultPattern (default events). There is no /topic_store/ or /eventstore/ — the Events-Store prefix is /topic-store/ (primary) / /store/ (alias).

A literal . in a destination segment is lossy. /topic/a.b and /topic/a/b both map to channel a.b, and egress always emits the slash form /topic/a/b, so /topic/a.b is not round-trip safe. Prefer slashes; avoid literal dots in segments. Wildcard subscriptions are Events-only, subscribe-only, and use the message broker's native wildcard syntax (* = one segment, > = the final tail) — there is no MQTT-style +/#. A wildcard on SEND, or on queues / events-store / RPC, is rejected with invalid destination. See Destination grammar.

The three-step RPC flow (Commands & Queries)

/command/ and /query/ are request/reply, and STOMP is requester-only — the responder lives on the KubeMQ (gRPC) side. A STOMP SUBSCRIBE to /command or /query is rejected (cannot subscribe to RPC destinations + close). The flow is:

  1. SUBSCRIBE to /reply/<name> first — connection-local: no array, no authz, no ack tracking.
  2. SEND to /command/<svc> or /query/<svc> with a required reply-to: (an active /reply/ subscription on the same connection), an optional correlation-id: (echoed only when set), and an optional timeout: in milliseconds (effective min(timeout, RpcTimeoutSeconds * 1000), default 30000).
  3. The reply arrives as a MESSAGE on the /reply/ subscription.

A timeout, logical error, or dropped reply arrives as a MESSAGE carrying a stomp-error headernot an ERROR frame — and the connection stays open. Only a reply-to violation or pending-cap overflow closes the connection. The body shape differs by failure kind: a logical error carries the responder's body and tags alongside stomp-error; only a transport error/timeout or a nil response is empty-bodied. See Commands and Error frames.

Cross-protocol interop

Because the connector bridges onto the shared KubeMQ array, a message published from STOMP is readable by any other KubeMQ transport on the same channel, and vice-versa — via the shared KubeMQ array, the same path gRPC uses. A STOMP SEND to /topic/it/cross (channel it.cross) is received by a native gRPC SubscribeEvents consumer on channel it.cross, and a native gRPC publish to it.cross is delivered to a STOMP subscriber on /topic/it/cross.

The same KubeMQ channel backs both transports, so a STOMP client and a gRPC/REST client interoperate transparently.

Cross-protocol interop is array-proven in both directions for STOMP ↔ gRPC. Always phrase it as "via the shared KubeMQ array, the same path gRPC uses" — there are no separately-proven STOMP ↔ MQTT or STOMP ↔ AMQP flows, though all transports share the same channels.

Reliability at a glance

PatternDelivery guaranteeWhat happens on a full output buffer
Queuesat-least-onceNAck → requeue (never lost; duplicates tolerated)
Events / Events-Storeat-most-oncethat delivery is dropped for that subscriber; the connection stays alive

Never promise exactly-once. See Queues and Events.

Was this page helpful?

On this page