Skip to content

Concurrency

The module is concurrent by construction: a host's cluster servers are called from many goroutines at once, and the session, subscription and MRP paths run side by side. This page lists the goroutines a running node owns, what a host callback may do, the ordering the module guarantees, and how shutdown ends it all. The race detector run (make race) is a release gate for these paths.

The goroutines of a running node

Goroutine Started by Cadence Stopped by
UDP read loop (one) bridge.Start per datagram bridge.Stop (context cancel, socket close)
Datagram handlers the read loop one per datagram, or per session (below) returns; not awaited by Stop
TCP accept loop and one reader per connection bridge.Start with Network.TCP per message bridge.Stop
ACK pump bridge.Start, when an ACK tracker is attached every 50 ms bridge.Stop
Subscription engine tick node.New every 250 ms Node.Stop
One report per due subscription the engine tick at most one at a time per subscription returns
Session reaper node.New sweep every minute; evicts sessions idle for 5 minutes Node.Stop
PASE and CASE handler reapers node.New sweep every minute; evict handlers idle for 30 s Node.Stop
Operational-time keeper Node.Start every minute Node.Stop (and waited for)
DNS-SD re-announce loop Node.Start, when the advertiser is an mdns.Reannouncer 30 minutes by default Node.Stop
Session-triggered re-announce a session event once, bounded by the advertise timeout returns
BDX transfer a peer's BDX init on a secure session one per exchange returns
Binding manager node.New on change Node.Stop
Client stack socket node.New with Config.Client.Enabled per datagram Node.Stop

node.New starts the engine and the reapers before Start. Call Stop on every node New returned, even one you never started.

Every cadence in the table runs on one clock: node.Config.Clock (bridge.Config.Clock for a host that builds the bridge itself), a clock.Clock — the module's one time port, clock.System when unset. No production code outside the clock package reads the time package's clock (clock/noraw_test.go fails the build on one), so a test that passes a clocktest.Fake moves the ACK pump, the engine tick, the reapers, the commissioning window and the cluster timers only when it calls Advance. AfterFunc callbacks run in their own goroutine on either clock. The node's assembler hands the same clock to the Identify server of every bridged endpoint (endpoint.Config.Clock) and to the Aggregator's; a device the host builds with the device layer takes it as device.Options.Clock.

fabrictest runs whole nodes and the module's controller on one clocktest.Fake and an in-memory network, and moves the clock only while no datagram is in flight and no goroutine of the process is working — the Testing in process guide.

Source: transport/udp/listener.go:147-212, bridge/bridge.go:784-840, bridge/ackpump.go:18-24, bridge/ackpump.go:291-314, im/subscription/manager.go:184-185, im/subscription/manager.go:229-235, im/subscription/engine.go:74-95, secure/operational/manager.go:223-227, secure/operational/manager.go:1145-1160, node/security.go:308-355, node/node.go:38-40, node/node.go:486-494, node/reannounce.go:15-31, node/config.go:175-179, bridge/securechannel.go:1268-1290, bridge/bdx_responder.go:93-110, binding/manager.go:238-244, node/client.go:96-106, node/node.go:95-108, clock/clock.go, node/config.go (Config.Clock), endpoint/assembler.go (Config.Clock), node/root.go (buildAggregatorClusters), device/options.go (Options.Clock), fabrictest/site.go (Run).

Dispatch and ordering

The read loop copies each datagram and hands it on without blocking.

Messages of one secure unicast session are handled one at a time, in arrival order. The listener keys them by their session id and queues them, so a Read sent right behind an Invoke sees the Invoke's effect. A handler that sends and then waits for the peer's answer on the same session releases its turn first, so the answer is not queued behind it. Examples are a chunked report waiting for its StatusResponse and the subscription priming loop. The TCP transport keeps the same contract per connection.

Everything else runs concurrently: unsecured messages (the PASE and CASE handshakes, session id 0) and group messages. Concurrent unordered handlers are bounded at 1024. A datagram that arrives when every slot is taken is dropped, and the peer's MRP retransmits it. Per session, at most 256 datagrams queue; beyond that a datagram is dropped too.

Between sessions there is no ordering. Two controllers act on the same server concurrently.

Source: bridge/session_order.go:14-69, transport/udp/ordered.go:13-110, transport/udp/listener.go:35-44, transport/udp/listener.go:196-210, transport/tcp/doc.go:24-35.

What a host callback may do

The module calls host code from its handler goroutines.

Cluster servers (MatterRead, MatterWrite, MatterInvoke). No dispatcher lock is held while a server is called. A call runs on its session's turn, so a slow MatterInvoke delays that session's next message, and other sessions go on. While it runs, the ACK pump sends a standalone acknowledgement for the request once the standalone-ACK delay (200 ms) has passed, so the controller does not retransmit. Reads also run on the subscription engine's report goroutines, at the same time as commands on other sessions. A server must therefore be safe for concurrent use. The ctx passed to the server ends when the bridge stops.

OnOff's Sink runs with no lock held that Server.Set or a read takes. It may confirm through Set, from its own goroutine or another. Commands, writes and countdown ticks of that server wait behind it.

node.Config.Observers run synchronously on the event's goroutine. For fabric events, that is inside the command that caused the event, before its response is sent, so a blocking observer delays the response. Hand network work to your own goroutine. An observer's error or panic is logged and never returned.

device.Device.OnChanged callbacks run on the server's own change notification. Keep them short. The quickstart only logs.

Controller SubscribeRequest.Updated runs on the goroutine that reads the report, inside the report's exchange. The priming report is delivered inside Subscribe, before it returns. The reference controller hands each report to a channel.

controller/discovery runs one goroutine per Names that holds all cache, service and scanner state. User callbacks never run on that loop.

Source: endpoint/dispatcher.go:364-389, bridge/session_order.go:55-69, bridge/receive.go:185, bridge/ackpump.go:95-105, bridge/ackpump.go:291-295, transport/mrp/ack.go:50-55, node/security.go:308, im/subscription/engine.go:74-95, cluster/onoff/server.go:54-72, cluster/onoff/server.go:749-768, node/observers.go:4-21, device/device.go:643-661, controller/interaction/handler.go:126-138, controller/interaction/subscribe.go:180-200, examples/reference-controller/cli/interact.go:289-330, controller/discovery/doc.go:21-30.

Changing the topology

Aggregator.Add and Remove reassemble a running node. Each reassembly takes a fresh snapshot of the set and swaps the new topology in atomically, so an in-flight request always sees one consistent topology.

Two reassemblies that run at the same time are not ordered: the last one to finish wins, even when it took its snapshot first. Call Add, Remove and MarkComplete from one goroutine, or serialise them yourself.

The Snapshotter of a host with its own topology is called at Start and on every Reassemble, and its result is never cached.

Source: node/aggregator.go:70-132, node/aggregator.go:172-178, bridge/bridge.go:868-893, bridge/bridge.go:217-231.

Contexts and shutdown

Call Context use
node.New takes none. Its goroutines run under the node's lifetime, which Stop cancels
Node.Start(ctx) used for the start-up work (assembly, loading the stored fabrics). A cancelled ctx fails Start
Node.Run(ctx) starts, waits for ctx to end, then stops with a fresh 5-second budget
Node.Stop(ctx) bounds the bridge's wait for its read loop. It closes the sessions with a CloseSession each, stops the engine and reapers, withdraws the mDNS records and closes the advertiser

Stop waits for the read loop, the TCP loop, the ACK pump and the operational-time keeper. It does not wait for datagram handlers that are already running. Their context is cancelled, and a server call can return after Stop has. Close the database only after your own servers have returned, or make them tolerate a closed handle.

Source: node/node.go:109-120, node/node.go:465-535, node/node.go:545-599, bridge/bridge.go:1541-1650, bridge/bridge.go:1805-1812, transport/udp/listener.go:147-212.