Circuits
One of the most fundamental concepts introduced in the Substrates API is the Circuit. Thinking of Observability as merely data pipelines is an inadequate conceptualization that invariably leads to a deficient architecture consisting of one-way lanes of chained data collectors. Instead, a new approach based on circuitry is proposed. The essential idea of a Circuit involves a path or loop that allows something to circulate, flow, or operate in a specific manner.
In the context of the human brain, the term “circuit” describes interconnected neural pathways that facilitate the flow of information. Our brain houses billions of neurons (nerve cells) that communicate with each other through intricate networks of neural circuits. These circuits play a fundamental role in various cognitive functions, sensory processing, motor control, and generating thoughts and emotions. Why should it be different for Observability?
Percepts
Within the Substrates API, information flows into a Circuit through a Percept, which can be an Instrument or Sensor. An Instrument offers a direct means to emit information into a Circuit via a call on the Instrument interface from the application or service code space. In contrast, a Sensor emits automatically by other opaque means, typically via periodic sampling or an event callback. A Counter is an example of an Instrument interface.
More importantly, a Percept can be registered as a Subscriber with a Circuit, consume Events emitted by other Percepts, and, in turn, emit information triggered by the processing of such Events. The emittance can be directed to the same subscribed Circuit, an internal loopback, or another Circuit, an outbound-inbound flow.
Currents
The Current within a Circuit combines a serial thread of execution and an Event queue. The Event queue is populated with a Percept’s emittance passed into the Circuit from an executing thread managed by the application, service, or another Circuit. From the queue to Subscribers, the Event processing within a Circuit only evolves a single thread of execution at a time, eliminating the need for complicated concurrency control across Event callbacks. The thread that executes a call on the Instrument will not be the same thread that dispatches the Event to Subscribers unless the call to the Instrument is called from within the Event loop processing.
Counters
Let’s write some code using the Substrates API and see how this circuit-based approach performs in practice across different Circuit network topologies using a simple count-and-sum circuitry.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 |
final var circuit = cortex.circuit ( "circuit" ); final var counters = circuit.conduit( counters () ); final var counter = counters.instrument ( "counter" ); for ( int i = BILLION; i > 0; i-- ) { counter.inc (); } |
Executing the above takes 53 seconds, averaging 53 nanoseconds per increment. The above, when executed, will result in two threads consuming the CPU simultaneously. Because we have not registered any Subscribers with the Circuit via a Conduit, there will be no further processing when the Event is dequeued. Let’s remedy this omission.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 |
final var subscription = counters .source () .consume ( new Outlet<> () { private long count; @Override public void accept ( final Event< Long > event ) { count++; } } ); |
With the above, the average unit cost (delay) for an increment rises to 68 nanoseconds.
Up to this point, the code behaves much like a standard pipeline. Let’s try something different, highlighting the power of circularity, where more processing happens within the Current of the Circuit instead of at the inter-thread communication system. First, we change the Subscriber to invoke the inc method on the Counter, not just sum.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 |
final var subscription = counters .source () .consume ( new Outlet<> () { private long count; @Override public void accept ( final Event< Long > event ) { if ( count < BILLION ) { count++; counter.inc (); } } } ); |
We need only kick off the recursive-like event processing with a single inc call to the Counter.
1 2 3 4 5 6 7 8 9 10 11 12 |
final var counter = counters.instrument ( "counter" ); counter.inc (); circuit .current () .await (); |
This is faster in taking, on average, 23 nanoseconds per increment, but it also consumes only a single CPU core. Admittedly, this is somewhat extreme, but it demonstrates that both the latency and CPU cost are cheap once we get to the situation where more Complex Event Processing (CEP) happens within a Circuit. Here, a Circuit acts effectively as a System of Instruments. A developer can decide to attach multiple types of Instruments to the same Circuit instance or one type per Circuit, forming more complex network topologies where relays bridge Circuits.