Firebreak.Cycles (Firebreak v0.1.0)

Copy Markdown View Source

Detects cycles of synchronous calls in the coupling graph — a deadlock hazard the supervision tree can't show.

If module A synchronously calls B (a GenServer.call) and, directly or transitively, B synchronously calls back to A, the two processes can deadlock: each blocks inside handle_call waiting for the other's reply, and neither can make progress. This is a property of the call graph, invisible in the supervision structure.

We build the directed graph of resolved synchronous :call edges and report each strongly-connected component of two or more modules — every such component contains a cycle. It is a hazard, not a certainty: the two calls must be able to be in flight at the same time for the deadlock to occur. So it is reported at :medium, best-effort, with the cycle's members spelled out.

Asynchronous edges (cast/send/pubsub) can't deadlock this way — the caller never blocks — so they're excluded.

Boot-order cycles

A cycle made entirely of in-init/1 synchronous calls is the sharper variant (boot_order_cycle, :high): on startup each init/1 synchronously calls the next module around the loop, but that module isn't running yet, so none can finish initialising — the supervision tree can't boot at all. Such a cycle is also a plain synchronous cycle, so it's reported once, under the more specific boot_order_cycle, and suppressed from cyclic_coupling.

Summary

Functions

analyze(edges)

@spec analyze([Firebreak.Edge.t()]) :: [Firebreak.Finding.t()]