-
-
Notifications
You must be signed in to change notification settings - Fork 1
501. Hooks
Hooks are a powerful extension mechanism in F-Mesh that allow you to execute custom logic at specific points during mesh execution without modifying the core framework. Hooks provide a clean way to add cross-cutting concerns like logging, monitoring, debugging, and validation.
Hooks are available at multiple levels:
- F-Mesh level - Lifecycle hooks for the entire mesh
- Component level - Hooks for individual component activation
- Port level - Hooks for port operations
All hooks share a common characteristic: they can return errors, allowing you to implement fail-fast behavior when needed.
For complete API documentation, see:
F-Mesh level hooks allow you to intercept execution at key points in the mesh lifecycle and during each wave of component activations.
| Hook | When It Runs | Context Provided |
|---|---|---|
OnComponentAdded |
When a component is added via AddComponents
|
*ComponentAddedContext (mesh and component) |
BeforeRun |
Before the mesh starts execution | *FMesh |
AfterRun |
After the mesh completes execution | *FMesh |
BeforeCycle |
At the start of each wave of component activations |
*CycleContext (cycle number and mesh instance) |
AfterCycle |
At the end of each wave of component activations | *CycleContext |
Note: the mesh registers a default
BeforeRunhook that validates mesh structure on every run. You can add your own hooks freely, but don't clear theBeforeRungroup without re-adding equivalent validation.
fm, err := fmesh.New("monitored-mesh",
fmesh.WithUnlimitedCycles(),
fmesh.WithUnlimitedTime(),
)
if err != nil {
// handle error
}
_ = fm.AddComponents(c1, c2, c3)
fm.SetupHooks(func(h *fmesh.Hooks) {
h.BeforeRun(func(fm *fmesh.FMesh) error {
fmt.Printf("Starting mesh '%s'\n", fm.Name())
return nil
})
h.BeforeCycle(func(ctx *fmesh.CycleContext) error {
fmt.Printf("Wave starting (cycle #%d)\n", ctx.Cycle.Number())
return nil
})
h.AfterCycle(func(ctx *fmesh.CycleContext) error {
fmt.Printf("Wave complete (cycle #%d)\n", ctx.Cycle.Number())
return nil
})
h.AfterRun(func(fm *fmesh.FMesh) error {
fmt.Printf("Mesh '%s' completed\n", fm.Name())
return nil
})
})- Logging: Track mesh execution flow and timing
- Monitoring: Collect metrics about cycle duration and component activations
- Debugging: Inspect mesh state at specific execution points
- Validation: Verify invariants before/after execution
- Testing: Inject test-specific behavior without modifying components
Component hooks provide fine-grained control over individual component activation lifecycle.
| Hook | When It Runs | Context Provided |
|---|---|---|
OnCreation |
During component.New (after all options and plugins) |
*Component |
BeforeActivation |
Before the component's activation function runs | *Component |
OnActivation |
Appended after the main activation function — runs sequentially in the same activation and shares its error path (first error aborts the chain) |
*Component (it is an ActivationFunc) |
AfterActivation |
After activation completes (regardless of outcome) | *ActivationContext |
OnSuccess |
When activation function returns nil
|
*ActivationContext |
OnError |
When activation function returns an error | *ActivationContext |
OnPanic |
When activation function panics | *ActivationContext |
OnWaitingForInputs |
When component is waiting for inputs | *ActivationContext |
The ActivationContext provides access to:
-
Component- The component instance -
Result- The activation result including status code and error
c, _ := component.New("processor",
component.WithInputs("in"),
component.WithOutputs("out"),
component.WithActivationFunc(func(c *component.Component) error {
// Component logic here
return nil
}),
)
c.SetupHooks(func(h *component.Hooks) {
h.BeforeActivation(func(c *component.Component) error {
c.Logger().Printf("Starting activation\n")
return nil
})
h.OnSuccess(func(ctx *component.ActivationContext) error {
ctx.Component.Logger().Printf("Activation succeeded\n")
return nil
})
h.OnError(func(ctx *component.ActivationContext) error {
ctx.Component.Logger().Printf("Activation failed: %v\n", ctx.Result.ActivationError())
// Could return error here to escalate it
return nil
})
h.OnPanic(func(ctx *component.ActivationContext) error {
ctx.Component.Logger().Printf("Activation panicked: %v\n", ctx.Result.ActivationError())
return nil
})
h.AfterActivation(func(ctx *component.ActivationContext) error {
ctx.Component.Logger().Printf("Activation completed with code: %v\n", ctx.Result.Code())
return nil
})
})- Error Recovery: Log errors or implement fallback behavior
- Performance Monitoring: Measure component execution time
- Debugging: Inspect input/output signals at each activation
- Audit: Track which components activated and their results
- Testing: Verify component behavior without modifying activation function
Port hooks allow you to intercept signal operations on individual ports. Hooks must be set up on each port separately.
| Hook | When It Runs | Context Provided |
|---|---|---|
OnSignalsAdded |
After signals are added to port buffer | *SignalsAddedContext |
OnClear |
When port buffer is cleared | *ClearContext |
OnInboundPipe |
When a pipe is created to this port | *InboundPipeContext |
OnOutboundPipe |
When this port creates a pipe | *OutboundPipeContext |
Context Details:
-
SignalsAddedContext: Port, SignalsAdded (slice of signals) -
ClearContext: Port, SignalsCleared (count) -
InboundPipeContext: DestinationPort, SourcePort -
OutboundPipeContext: SourcePort, DestinationPort
c, _ := component.New("logger",
component.WithInputs("data", "config"),
component.WithActivationFunc(myFunc),
)
// Setup hook on individual input port
c.InputByName("data").SetupHooks(func(h *port.Hooks) {
h.OnSignalsAdded(func(ctx *port.SignalsAddedContext) error {
fmt.Printf("Port '%s' received %d signals\n",
ctx.Port.Name(),
len(ctx.SignalsAdded))
// Access individual signals
for _, sig := range ctx.SignalsAdded {
fmt.Printf(" Signal payload: %v\n", sig.PayloadOrNil())
}
return nil
})
h.OnClear(func(ctx *port.ClearContext) error {
fmt.Printf("Port '%s' cleared %d signals\n",
ctx.Port.Name(),
ctx.SignalsCleared)
return nil
})
})
// Setup hooks on output port
_ = c.AddOutputs("result")
c.OutputByName("result").SetupHooks(func(h *port.Hooks) {
h.OnOutboundPipe(func(ctx *port.OutboundPipeContext) error {
fmt.Printf("Pipe created: %s -> %s\n",
ctx.SourcePort.Name(),
ctx.DestinationPort.Name())
return nil
})
})- Signal Tracking: Monitor all signals flowing through specific ports
- Validation: Verify signal payloads match expected types
- Debugging: Log signal flow for troubleshooting
- Metrics: Count signals per port
- Topology Discovery: Track pipe connections
Understanding the order in which hooks execute is important for building predictable systems.
- BeforeRun (F-Mesh)
- For each wave of activations:
- BeforeCycle (F-Mesh)
- For each scheduled component (concurrent):
- BeforeActivation (Component)
- Activation function runs
- OnSuccess OR OnError OR OnPanic OR OnWaitingForInputs (Component)
- AfterActivation (Component)
- Signals forwarded through pipes
- AfterCycle (F-Mesh)
- AfterRun (F-Mesh)
Hooks can return errors, which allows you to implement fail-fast behavior:
fm.SetupHooks(func(h *fmesh.Hooks) {
h.BeforeRun(func(fm *fmesh.FMesh) error {
if !validateConfiguration(fm) {
return fmt.Errorf("invalid mesh configuration")
}
return nil
})
h.BeforeCycle(func(ctx *fmesh.CycleContext) error {
if ctx.Cycle.Number() > 1000 {
return fmt.Errorf("runaway mesh detected")
}
return nil
})
})When a hook returns an error:
- The error is propagated up and can stop mesh execution
- Subsequent hooks in the same group may not execute (hook groups are fail-fast)
- A failing component-level hook re-codes the activation result to
ActivationCodeHookFailed, which counts as an activation error — underStopOnFirstErrorOrPanicthe mesh stops and the hook error surfaces inRun()'s return - A failing
BeforeCycle/AfterCyclehook aborts the run; a failingOnComponentAddedhook failsAddComponents
Hooks run synchronously and can impact performance. Keep hook logic simple and fast:
// Good: lightweight logging
h.AfterCycle(func(ctx *fmesh.CycleContext) error {
log.Printf("Cycle %d done\n", ctx.Cycle.Number())
return nil
})
// Bad: expensive operations
h.AfterCycle(func(ctx *fmesh.CycleContext) error {
// Don't do this - sends HTTP request on every cycle
sendMetricsToServer(ctx)
return nil
})Choose the right level for your hook:
- Use F-Mesh hooks for mesh-wide concerns (startup, shutdown, metrics)
- Use Component hooks for component-specific logic (error handling, validation)
- Use Port hooks for signal-level concerns (tracing, validation)
Hooks should observe and extend behavior, not replace core logic:
// Good: observe and log
h.OnError(func(ctx *component.ActivationContext) error {
log.Printf("Component failed: %v\n", ctx.Result.ActivationError())
return nil // Let the error propagate normally
})
// Bad: changing component behavior
h.BeforeActivation(func(c *component.Component) error {
// Don't manipulate signals here - do it in activation function
c.InputByName("in").Clear()
return nil
})You can add multiple hooks of the same type - they execute in order:
fm.SetupHooks(func(h *fmesh.Hooks) {
h.BeforeRun(logStartup)
h.BeforeRun(validateConfig)
h.BeforeRun(initializeMetrics)
})var startTime time.Time
fm.SetupHooks(func(h *fmesh.Hooks) {
h.BeforeRun(func(fm *fmesh.FMesh) error {
startTime = time.Now()
return nil
})
h.AfterRun(func(fm *fmesh.FMesh) error {
duration := time.Since(startTime)
fmt.Printf("Mesh executed in %v\n", duration)
return nil
})
})componentTimings := make(map[string]time.Duration)
c.SetupHooks(func(h *component.Hooks) {
var start time.Time
h.BeforeActivation(func(c *component.Component) error {
start = time.Now()
return nil
})
h.AfterActivation(func(ctx *component.ActivationContext) error {
duration := time.Since(start)
componentTimings[ctx.Component.Name()] += duration
return nil
})
})// Setup tracing on specific input ports
c.InputByName("data").SetupHooks(func(h *port.Hooks) {
h.OnSignalsAdded(func(ctx *port.SignalsAddedContext) error {
for _, sig := range ctx.SignalsAdded {
fmt.Printf("[TRACE] Signal received on %s.%s: %v\n",
ctx.Port.ParentComponent().Name(),
ctx.Port.Name(),
sig.PayloadOrNil())
}
return nil
})
})Understanding when to use hooks versus activation functions:
| Feature | Activation Function | Hooks |
|---|---|---|
| Purpose | Core component logic | Cross-cutting concerns |
| Required | Yes (for processing) | No (optional) |
| Modifies signals | Yes | Should not |
| Performance impact | Direct | Overhead |
| Reusability | Component-specific | Can be shared |
Use activation functions for: Business logic, signal processing, computations
Use hooks for: Logging, monitoring, debugging, validation, testing
Hooks make F-Mesh more extensible and observable without compromising the simplicity of component design. They provide clean separation between core logic and auxiliary concerns.
| Type | Fields | Used by |
|---|---|---|
fmesh.ComponentAddedContext |
FMesh, Component
|
OnComponentAdded |
fmesh.CycleContext |
FMesh, Cycle
|
BeforeCycle, AfterCycle
|
component.ActivationContext |
Component, Result
|
AfterActivation, OnSuccess, OnError, OnPanic, OnWaitingForInputs
|
port.SignalsAddedContext |
Port, SignalsAdded
|
OnSignalsAdded |
port.ClearContext |
Port, SignalsCleared
|
OnClear |
port.InboundPipeContext |
DestinationPort, SourcePort
|
OnInboundPipe |
port.OutboundPipeContext |
SourcePort, DestinationPort
|
OnOutboundPipe |
For reading ActivationResults after a run, see Inspecting a run.