When a node throws: failed event cycles¶
From runtime 1.1.3, with processors generated by compiler 1.0.80 or later, an exception thrown inside an event cycle no longer wedges the processor (telaminai/fluxtion-compiler#105, telaminai/fluxtion#36). This page states what the engine does with a failed cycle, and what it deliberately does not do.
The policy¶
When a node, a trigger, an event-end method, a lifecycle method, an exported service or a host action throws:
- The exception propagates to the caller. A generated processor rethrows the node's own exception. The interpreted processor may wrap it, with the node's exception as the cause. If cleaning up the failed cycle also fails, the original exception is still the one thrown, and the cleanup failure is attached to it as a suppressed exception.
- The failed cycle is closed. The in-cycle flag is cleared. Dirty flags and fork tasks are reset. Every auditor
with event receipt enabled gets
processingComplete, so the audit record of the failed cycle is closed and the next cycle's record is its own. - What the failed cycle queued is discarded, not dispatched later. This covers re-entrant events, callbacks, the
remaining elements of a flat-map, and the payloads a callback node stored for its queued triggers. A later,
successful cycle never receives a discarded payload. The discard runs after the close, so work queued while closing
(for example by an
@AfterEventmethod after a failedrunInEventCycleaction) is discarded too. - Node state is not rolled back. Whatever a node changed before the throw stays changed. Clearing the flag is not a transaction. The engine only guarantees that the processor is not wedged and that nothing of the failed cycle runs later.
- The host decides what happens next. It can keep sending events, stop the processor, reset nodes, or rebuild the processor. The engine does not choose on the host's behalf, and a processor that keeps working after a caught exception is not a claim that continuing is safe for your graph. Catch inside the node if a failure must not end the cycle.
Boundaries¶
| Entry point | On failure |
|---|---|
onEvent, typed onEvent entry points |
The cycle fails as above. A queued item that throws while the queue drains fails the same cycle, and the rest of the queue is discarded. The clock's shared reading for the drain is restored. |
| Exported service methods | The service call is a cycle of its own, and it fails as above. |
start, startComplete, stop, batchPause, batchEnd |
These fail as above. Their audit prologue is inside the boundary. |
runInEventCycle(auditEvent, action) |
A failed action has its queued work discarded. The cycle is still closed, so its record is complete. A throw while closing or draining after a successful action fails the cycle. The action's exception is the one the caller sees. |
bufferEvent |
A buffered handler that throws fails the whole buffered batch. Buffering ends, buffered dirty state is reset, the batch's queued work is discarded, and nothing of the batch is calculated later. Later bufferEvent / triggerCalculation calls start a new batch. The handlers of events buffered earlier in the batch have already run and changed node state, which is not rolled back: resubmitting the whole batch applies them twice. |
triggerCalculation |
The calculation is a cycle of its own, and it fails as above. It does not run again on the next event. |
A buffered calculation flushed by onEvent, a service call or runInEventCycle |
The flush fails as a triggerCalculation does. Its exception propagates from the call that flushed it, and that call's own event, service body or action is not run. The caller cannot tell this apart from a failure of its own work, so it cannot reliably decide to resubmit. When that matters, end each buffered batch with an explicit triggerCalculation(), so a calculation failure surfaces on its own. |
An exported service called from inside a cycle. For example, a node calls another node's exported service while handling an event. That call is not refused, and it runs inline. Its close clears the in-cycle flag of the cycle it is nested in; this behaviour predates 1.1.3. If the nested call throws, it fails as a cycle: it discards the enclosing cycle's queued work and closes the enclosing record. Its exception then propagates through the calling node and fails the enclosing cycle too, unless that node catches it. Prefer raising an event over calling an exported service from inside a cycle.
Proposed follow-up (not in 1.1.3): wrap a flush failure in a distinguishable exception, for example
BufferedCalculationFailedException with the calculation's exception as the cause, so a caller knows its own event,
service body or action did not run.
Runtime floor and upgrading¶
- Discarding the queue needs runtime 1.1.3. The generator emits the discard only for a client whose runtime reports 1.1.3 or later. It emits everything else on this page, the cleared flag and the closed cycle, for every runtime.
- On an older runtime, source from compiler 1.0.80+ is not wedged, but the failed cycle's queue is kept. The next cycle drains it. A queued item that throws stays at the head of the queue. Every later cycle dispatches it again and rethrows its exception after processing its own event. The compiler cannot mitigate this alone: before 1.1.3 the runtime offers no way to remove a queued item, because the dispatcher's queue is not accessible from generated code, and a reflective workaround would break native images. Upgrade the runtime.
- A snapshot runtime counts as older. Generated source treats a runtime that reports a qualified version (such as
1.1.3-SNAPSHOT) or no version (such as a shaded jar with nobuild.properties) as older than 1.1.3, so it gets the behaviour above. The interpreter checks the runtime on its classpath directly. - Upgrading the runtime does not change processors that are already generated. A processor generated before compiler 1.0.80 still wedges after an exception, whichever runtime it runs on. Regenerate processors after upgrading the runtime and the builder (or BOM) together.
For implementers of the callback SPI¶
Runtime 1.1.3 adds the abstract method EventProcessorCallbackInternal.discardQueuedCallbacks(). A third-party
EventProcessorCallbackInternal implementation does not compile against 1.1.3 until it implements the method. Your
implementation must:
- drop every queued trigger and event;
- reset any "dispatching" state a throw inside the drain may have left set;
- call
callbacksPending(false)on its processor; - call
discardQueuedPayloads()on the payload owners registered with it, if it implementsQueuedPayloadRegistry.
Generated processors call discardQueuedCallbacks() only when a cycle fails. A node that stores callback payloads
apart from the trigger it queues implements QueuedPayloadOwner and registers with the dispatcher through
QueuedPayloadRegistry.reRegister(previous, current, owner), as CallBackNode and AbstractCallbackNode do: it
unregisters from the previous dispatcher, so a node reused by another processor moves its registration.
QueuedPayloadRegistry.registerIfSupported suits an owner that only ever belongs to one processor.
Changes in runtime 1.1.3 / compiler 1.0.80¶
- Policy: an exception in an event cycle propagates to the caller, the failed cycle is closed, and what it queued, callback payloads included, is discarded rather than run later. Node state is not rolled back, and the host decides whether to continue. Before this change, one exception wedged the processor and later events were silently queued.
- New abstract SPI method:
EventProcessorCallbackInternal.discardQueuedCallbacks(). Third-party implementations must add it (see above). It is abstract on purpose. - Runtime floor: a processor generated by compiler 1.0.80 discards a failed cycle's queue only on runtime 1.1.3+. On an older runtime it is not wedged, but a throwing queued item poisons later cycles (see above).
- Regenerate after upgrading: upgrading the runtime does not change processors already generated. Regenerate them with the 1.0.80 builder (or BOM).
- Interpreted processor, exported services: an event raised by a node during an interpreted service call is now queued and dispatched after the call, as in generated processors. Before, it was dispatched synchronously, in the middle of the call.
- Interpreted processor, exported services: a checked exception thrown sneakily from an interpreted service call is
now wrapped in
java.lang.reflect.UndeclaredThrowableException, with the original as the cause. Generated processors still rethrow it unwrapped. - Buffering: a failing
bufferEventends the buffered batch, and a failingtriggerCalculationis not retried on the next event. - Callback payloads:
CallBackNodeandAbstractCallbackNodepayloads are discarded with a failed cycle (QueuedPayloadOwner/QueuedPayloadRegistry).
Not covered¶
The C++ target (fluxtion-generator-cpp) has its own queue guard and pending work. None of the above applies there
yet.