Fluxtion SEP Lifecycle — one page¶
A Static Event Processor (SEP) goes through six phases between build and event flow. Each phase has specific hooks; getting the order wrong is the single biggest source of "why isn't my node receiving X?" confusion.
The phases at a glance¶
┌─────────┐ ┌────────────┐ ┌──────────────────┐ ┌────────────┐
│ BUILD │ ─▶ │ CONSTRUCT │ ─▶ │ INIT │ ─▶ │ START │
│ (offline│ │ (runtime) │ │ (runtime) │ │ (runtime) │
│ codegen)│ │ │ │ │ │ │
└─────────┘ └────────────┘ └──────────────────┘ └────────────┘
│ │
▼ ▼
┌──────────────────┐ ┌────────────┐
│ EVENT PROCESSING │ ─▶ │ TEARDOWN │
│ (runtime, hot) │ │ (runtime) │
└──────────────────┘ └────────────┘
| Phase | What happens | Your hooks |
|---|---|---|
| BUILD | Builder constructs the graph; codegen emits .java + .graphml + .class. |
FluxtionGraphBuilder.buildGraph(config) |
| CONSTRUCT | SEP class instantiated. Nodes are field-initialised but unwired. | No-arg constructor on each node |
| INIT | Lifecycle.init() runs. Auditors call nodeRegistered on every node. DataFlowContextListener.currentContext fires. @ServiceRegistered listener tables populate. @Initialise methods run after all nodeRegistered calls complete. |
@Initialise |
| START | Lifecycle.start() runs. @Start methods fire on every node. |
@Start |
| EVENTS | Hot path. Events dispatched to @OnEventHandler, @OnTrigger, exported services. |
@OnEventHandler, @OnTrigger, @ExportService |
| TEARDOWN | Lifecycle.stop() → @Stop methods. Then Lifecycle.tearDown() → @TearDown methods. |
@Stop, @TearDown |
Detailed firing order during INIT¶
The sequence callers most often need to reason about:
sep.init()
│
├─ ServiceRegistryNode.init() // clear callback tables
│
├─ for each registered node N:
│ ├─ ServiceRegistryNode.nodeRegistered(N, name)
│ │ │
│ │ ├─ if N implements DataFlowContextListener:
│ │ │ N.currentContext(ctx) ◀── context delivered HERE
│ │ │
│ │ └─ scan N.getClass() for @ServiceRegistered methods
│ │ register callbacks under (type, name) keys
│ │
│ └─ other auditors' nodeRegistered (NodeNameLookup, EventLogManager, …)
│
└─ for each registered node N:
N.@Initialise method runs ◀── ctx is non-null by this point
query interfaces are populated
Key invariant: by the time @Initialise fires on any node, currentContext has already fired on every DataFlowContextListener, and the ServiceRegistryQuery graph is fully populated. Boot-time consistency checks belong here.
Sample code at each phase¶
public class MyNode implements DataFlowContextListener {
// ── CONSTRUCT phase ────────────────────────────────────────────
private DataFlowContext ctx;
private MyDependency dep;
public MyNode() {
// No-arg ctor. Don't touch fluxtion infrastructure here — it
// isn't wired yet. Just initialise plain fields.
}
// ── INIT phase ─────────────────────────────────────────────────
@Override
public void currentContext(DataFlowContext c) {
// Fires during nodeRegistered, BEFORE @Initialise.
// ctx becomes available; safe to stash for later use.
this.ctx = c;
}
@ServiceRegistered("my-sink")
public void onSink(MyDependency d) {
// Fires when registerService delivers a matching binding.
// For external services, may fire at any time; for internal
// (self-published) services, fires during init.
this.dep = d;
}
@Initialise
public void verifyWiring() {
// Runs AFTER all currentContext / @ServiceRegistered callbacks
// have fired across every node. The dependency graph is fully
// populated — boot-time consistency checks belong here.
ServiceRegistryQuery q = ctx.serviceRegistryQuery().orElseThrow();
if (q.findNamedDependency(MyDependency.class, "my-sink").isEmpty()) {
throw new IllegalStateException("my-sink not bound");
}
}
// ── START phase ────────────────────────────────────────────────
@Start
public void warmCaches() {
// Runs once after init completes. Use for work that depends on
// ALL nodes being initialised — e.g., pre-warming a cache from
// a sibling node's state.
}
// ── EVENT phase ────────────────────────────────────────────────
@OnEventHandler
public boolean onEvent(MyEvent e) {
// Hot path. ctx, dep, and any @Initialise-set fields are stable.
return true;
}
// ── TEARDOWN phase ─────────────────────────────────────────────
@Stop
public void drain() {
// Called before tearDown; chance to flush state.
}
@TearDown
public void releaseResources() {
// Final cleanup.
}
}
Lookup APIs and when they're safe¶
| API | Returns | Safe to call from |
|---|---|---|
dataFlow.getNodeById(id) |
The named node instance (throws if missing) | After init() |
dataFlow.getAuditorById(id) |
The named auditor (throws if missing) | After init() |
dataFlow.getServiceById(id, type) |
Optional<T> — node OR auditor, cast to type |
After init() |
dataFlow.serviceRegistryQuery() |
Optional<ServiceRegistryQuery> |
After init() |
ctx.serviceRegistryQuery() (from inside a node) |
Optional<ServiceRegistryQuery> |
@Initialise onwards (NOT in currentContext) |
ctx.getParentDataFlow() |
The enclosing DataFlow | @Initialise onwards |
Common gotchas¶
-
Build-time instance ≠ runtime instance. When using
c.addNode(new MyNode(), "x")in a builder, the instance you pass in is used at codegen time only. Compiled SEPs instantiate their own copy via the no-arg constructor. To inspect runtime state from a test, usedataFlow.getNodeById("x")rather than the instance you passed in. -
ctxis null inside the constructor. The no-arg ctor runs at CONSTRUCT phase;currentContextfires at INIT. Don't derefctxuntil@Initialise(or later). -
@Initialiseorder is not user-defined. Don't rely onMyNode.init()running beforeOtherNode.init(). If you need ordering, use@Startfor the later step — by then init is fully complete across all nodes. -
@ServiceRegisteredlisteners can fire after@Initialise. A@ServiceRegistered MySinkcallback fires whenregisterServicedelivers the binding, which may be during init OR later, when an external system pushes the service in. Don't assume the dependency is bound during@Initialiseunless you control the registration path. -
@ExportServicerequires void-returning methods. The codegen drops the export silently if any method on the interface returns a value. For typed query interfaces, expose viagetNodeById/getServiceByIdinstead, or wrap the read in a void-returning callback. -
Auditors live on a different lookup path in compiled SEPs. Pre-1.0.2 compiled SEPs route auditors through
getAuditorById(reflection on a public field), notnodeNameLookup. UsedataFlow.getServiceById(id, type)to abstract over both paths.
When auditors are involved¶
Auditors (Auditor subtypes added via EventProcessorConfig.addAuditor) get one extra hook: nodeRegistered(node, nodeName) fires for every regular node visited. This is how ServiceRegistryNode scans @ServiceRegistered annotations and how EventLogManager records every dispatch. If you're writing an auditor, your scanning logic goes in nodeRegistered, not @Initialise, because by @Initialise time you'd be one phase too late.
TL;DR¶
- Stash
ctxincurrentContext(CONSTRUCT-cheap, fires early). - Do boot-time validation in
@Initialise(graph fully populated). - Do "everyone is initialised" work in
@Start. - Don't dereference
ctxin the constructor. - Don't assume the test-time node instance is the runtime instance.