Skip to content

Message Bus Topology ​

The platform's loose coupling rides on a single message bus: most of the asynchronous traffic between services flows through a set of topic exchanges named by business domain. The messaging layer itself is broker-neutral (the dc3-mq abstraction; DC3_MQ_TYPE selects rabbitmq/kafka/pulsar/mqtt, defaulting to rabbitmq). This page is the full inventory of the default RabbitMQ implementation — every exchange, queue, routing-key prefix, TTL, and dead-letter destination, plus who publishes and who consumes. When troubleshooting, open the RabbitMQ management page (15672), find the backlog, and this table tells you which link is stuck.

You are here: the data and command plane pages cover the two main pipelines in depth; this page is the ledger — including state, alarm, metadata, and event domains not expanded elsewhere. Names come straight from RabbitNames / RabbitTopology in dc3-mq-rabbitmq; the code is authoritative.

Naming conventions ​

  • Exchanges: dc3.e.<domain>; queues: dc3.q.<domain>[.<sub>]; routing keys: dc3.r.<domain>.<prefix>.
  • Queue names with a dot suffix (e.g. dc3.q.point_command.) are per-consumer prefixed queues — each driver instance owns its own queue under its client id (dc3.driver.client, e.g. dc3.q.point_command.dc3-driver-virtual), and commands route to it by the key dc3.r.point_command.<service>.<node>.
  • Exception: dc3.q.event.report carries a dot but is a fixed-suffix shared queue (every driver's events land in this one queue) — it is not a per-consumer queue.
  • Every name can carry a prefix tag (empty by default — names are exactly as in the tables below); it's what isolates stacks sharing one RabbitMQ.

When the prefix appears: dev_<user>. in dev environments ​

The rule is a single line: when spring.env is dev, the prefix is dev_<spring.group>. — the group defaults to the launching user's OS username, overridable via SPRING_GROUP; test / pre / pro carry no prefix. Physical names then look like dev_pnoker.dc3.e.value. Two input sources: the dev profile YAML (spring.env: dev, spring.group: ${user.name}) or the environment variables SPRING_ENV / SPRING_GROUP (which outrank the profile YAML). The intent: multiple developers on one machine each get their own topology and never trade messages.

The prefix must match across the whole stack

A publisher and a consumer with different prefixes are two mutually invisible topologies — values published by a driver pile up in a queue nobody consumes. Two classic traps:

  1. Native images mixed with JVM services: native images bake SPRING_ENV=test in at build time (so AOT doesn't freeze the dev profile into the binary), which naturally disagrees with JVM services running dev. When mixing JVM + native stacks, set SPRING_ENV explicitly and uniformly (see the native stack section of the deployment guide).
  2. The single stack: docker-compose-single.yml explicitly sets SPRING_ENV: dev + SPRING_GROUP: root, aligning centers and drivers on one prefix — that's the pattern to copy.

Full inventory ​

Values and commands (details in the data/command plane pages) ​

ExchangeQueueRoutingWho → whomNotes
dc3.e.valuedc3.q.value.pointdc3.r.value.point.*driver → data centerThe point-value pipeline; TTL 7 days
dc3.e.point_value_deaddc3.q.point_value_dead—value-queue dead letterLanded on expiry or retry exhaustion (a quarantine with no consumer by design)
dc3.e.point_commanddc3.q.point_command.<client>dc3.r.point_command.<service>.<node>data center → target driverPoint read/write commands; queue TTL 30s (expiry dead-letters), plus a lease-coupled x-expires (default 5 minutes — the queue self-deletes after the driver departs)
dc3.e.point_command_deaddc3.q.point_command_dead—command dead letterExpired / retry-exhausted commands
dc3.e.point_command_resultdc3.q.point_command_resultdc3.r.point_command_result.*driver → data centerCommand receipts; TTL 60s

Custom commands and events ​

ExchangeQueueWho → whomNotes
dc3.e.commanddc3.q.command.<client>data center → target driverDevice-level custom commands (CommandCallDTO; a separate namespace from point commands); same 30s TTL + dead letter + lease x-expires
dc3.e.command_resultdc3.q.command_resultdriver → data centerCustom-command receipts; TTL 60s
dc3.e.command_deaddc3.q.command_deadcommand dead letter
dc3.e.eventdc3.q.event.reportdriver → data centerEvent reports (dc3_event_history); can trigger alarms. Fixed queue, TTL 60s, routing key dc3.r.event.<driver service name>

Registration and metadata ​

Driver registration does not use the message bus: once a driver is up it registers with the Manager Center directly over gRPC (DriverClient in dc3-api-driver), carrying its protocol attribute definitions — don't look for a "register queue" in the management console; it doesn't exist (the old register constants were dropped from the code). What does ride the bus in this domain is metadata invalidation:

ExchangeQueueWho → whomNotes
dc3.e.metadatadc3.q.metadata.driver.<client>manager → target driverAfter a device/point/driver metadata change, the Manager Center delivers a targeted notice by driver service name (routing key dc3.r.metadata.driver.<service>); the driver consumes it and invalidates its local metadata cache. Queue is auto-delete + TTL 30s

State and timeouts ​

ExchangeQueueNotes
dc3.e.statedc3.q.state.driver / dc3.q.state.deviceDriver/device liveness heartbeats to the data center (source of the dc3_entity_state lease); queue TTL 30s
dc3.e.state_timeout_delaydc3.q.state_timeout.driver.45s (TTL 45s)Delay queue: on every driver heartbeat received, the data center publishes a 45s delayed check; on expiry it dead-letters into dc3.e.state_timeout_check
dc3.e.state_timeout_checkdc3.q.state_timeout.driver_checkConsumed by the data center on expiry; judges offline against the database lease (fenced by leaseVersion)
samedc3.q.state_timeout.device_scan_tick.10s (10s)Periodic device-scan tick (a 10s self-perpetuating cycle the data center drives itself)
samedc3.q.state_timeout.device_scanDevice-scan execution queue

Alarms and notifications ​

ExchangeQueueWho → whomNotes
dc3.e.alarmdc3.q.alarm.driver / dc3.q.alarm.devicedriver → data centerDriver-originated driver/device alarms (routing keys dc3.r.alarm.driver.<service> / dc3.r.alarm.device.<service>); the data center persists them to dc3_entity_alarm and runs rule evaluation. The data center's own status-flip alarms enter the service layer directly without these queues
dc3.q.notify.task (TTL 24h)data center (alarm engine) → notification workerNotification delivery tasks, routed per channel (dc3.r.notify.task.<channel>); channels (Feishu bot/webhook/email) in Alarms & Notifications

MQTT bridge ​

dc3.e.mqtt is a broker-side exchange: it is declared by the RabbitMQ image's definitions.json / rabbitmq.conf (mqtt.exchange, environment variable RABBITMQ_MQTT_EXCHANGE, default dc3.e.mqtt) together with the MQTT plugin — inbound MQTT messages on 1883/8883 enter the AMQP side through it. It is a plugin entry point, not part of the application-declared topology; there is no dc3.q.mqtt queue in application code (the old constant was dropped), and the MQTT driver connects straight to the broker via dc3-common-mqtt.

Troubleshooting with this table ​

  1. Which queue is backed up: RabbitMQ management → Queues → sort by messages ready;
  2. Match "who consumes" above — a dc3.q.value.point backlog means the data center consumes slowly (batch / concurrency settings); a dc3.q.point_command.<client> backlog means that driver is offline (nobody consumes; dead-letters after 30s);
  3. Dead-letter queues have messages: something was rejected or expired — check the consumer's logs for the rejection / retry-exhaustion reason in that domain;
  4. Queue missing: the consumer never registered (a driver's prefixed queues are created only when it subscribes);
  5. Changed a device's config but the driver didn't notice: the consumer on that link is the driver (it receives the metadata event and invalidates its local cache), not the data center — check the driver's logs for an event routed as dc3.r.metadata.driver.<service>.

Further reading ​

Released under the AGPL-3.0 License