Agent Execution in Mongoose Server¶
This document explains what “agent execution” means in Mongoose Server, how it compares to running on a user thread, how agents are supported in the runtime, and when you would choose agent execution. It also clarifies which components always execute on agents and which can be hosted either within an agent or on their own threads.
See also:
- Threading model (deep dive): threading-model.md
- Architecture overview: overview.md
- Event flow: event-flow.md
1) What is agent execution?¶
An agent is a long-lived, cooperative worker that runs on its own looped thread with a configurable idle strategy. In
Mongoose Server, agents host components and repeatedly invoke their doWork() or process one event cycle at a time.
Key properties:
- Dedicated thread per agent group: Agents typically map to named threads (processor groups, service groups, scheduler).
- Cooperative, non-blocking style: Components hosted by an agent should do bounded, non-blocking work and return control to the loop.
- Deterministic ownership: State mutated by a processor on its agent thread is single-threaded by design.
Minimal mental model: An agent is the execution context that owns a component’s work loop. You configure which components are hosted by which agent thread.
2) Agent execution vs. running on a user thread¶
Sometimes you may write code that runs on a “user thread” (e.g., a thread you create yourself or a thread provided by a third-party client library). Here’s a comparison:
Advantages of agent execution
- Single-threaded event handling: Processors execute events on a dedicated agent thread, avoiding external synchronization inside handlers.
- Lifecycle and observability: Agents participate in server startup and shutdown and expose names, logs and metrics. Callback sequences differ between processors, feeds and services.
- Dispatch integration: Publishing uses
EventToQueuePublisherand group queues. Full-queue retries can end in counted drops; see overload behaviour. - Tunable latency and CPU use: Idle strategies (busy spin, yielding, backoff, sleeping) can be chosen per agent group; see threading-model.md.
Advantages of user-thread execution
- Simplicity for external libraries: Some client libraries insist on calling you back on their own threads; you may keep their callbacks there and publish into Mongoose as needed.
- Blocking operations: If you must perform blocking IO, doing it on a user-managed thread can avoid stalling an agent loop.
Trade-offs
- Shared-state safety: On a user thread, you must not mutate processor-owned state directly; marshal work into the processor via events.
- Observability: Work on user threads may not automatically appear under agent metrics/logging.
- Scheduling: Agent threads offer fine-grained control (idle strategies, core pinning) that you won’t get on arbitrary user threads.
3) How agents are supported in Mongoose Server¶
Mongoose provides several agent types and ways to host components:
- Processor Agent Groups: Host application processors and one generated Fluxtion control processor. The latter decides lifecycle changes; business events keep their own dispatch path. See built with Fluxtion.
- Service Agent Groups: Host worker services and optional agent-hosted event sources.
- Scheduler Agent: Runs the deadline wheel (
DeadWheelScheduler) for timed tasks.
Event publication between threads uses single-producer/single-consumer queues coordinated by the EventFlowManager and
EventToQueuePublisher.
Configuration
- Programmatic: Use
MongooseServerConfigandThreadConfigto declare agent threads, names, and idle strategies. - YAML style: Equivalent declarations exist for feeds/handlers/sinks and agent threads.
Example (Java builder):
import com.telamin.mongoose.config.MongooseServerConfig;
import com.telamin.mongoose.config.EventProcessorGroupConfig;
import com.telamin.mongoose.config.EventProcessorConfig;
import com.telamin.mongoose.config.EventFeedConfig;
import com.telamin.mongoose.config.ThreadConfig;
import org.agrona.concurrent.BusySpinIdleStrategy;
MongooseServerConfig mongooseServerConfig = MongooseServerConfig.builder()
.addThread(ThreadConfig.builder()
.agentName("processor-agent")
.idleStrategy(new BusySpinIdleStrategy())
.build())
.addThread(ThreadConfig.builder()
.agentName("service-agent")
.build())
// add processors/feeds/sinks as required
.build();
See:
- Processor agent:
ComposingEventProcessorAgent - Service agent:
ComposingServiceAgent - Scheduler agent:
DeadWheelScheduler - Queue publisher:
EventToQueuePublisher
4) When to choose agent execution¶
Use agent execution when any of the following apply:
- You need deterministic, single-threaded event processing for business logic (all event handlers).
- You want low-latency, predictable scheduling with an appropriate idle strategy.
- You need integrated lifecycle and observability of long-running workers.
- You need to safely cross threads between publishers and processors using the built-in queues.
Consider user threads when:
- A third-party library forces callbacks on its own threads and you just relay results into Mongoose.
- You perform blocking IO that shouldn’t run on an agent loop; isolate blocking work and publish results into a processor.
Hybrid approach:
- Host lightweight, non-blocking services or sources inside a service agent group.
- Run heavy, blocking connectors on their own threads and use
EventToQueuePublisherto feed processors.
5) What runs where: handlers, feeds, sinks, and services¶
- Event handlers: Always execute on processor agent threads. Every invocation of a handler (e.g.,
handleEvent) occurs within the processor’s agent context. - Feeds (event sources): Can run inside a service agent (agent-hosted source) or on their own threads (e.g., library-managed IO threads). Either way, they publish into processor queues.
- Services: Can be agent-hosted (cooperative
doWork()in a service agent group) or run on their own threads if they are external/blocked; publish back to processors for state changes. - Sinks: A synchronous sink method runs on its caller’s thread. A sink implementation may enqueue work for a service agent or its own worker; that handoff is not automatic.
Small diagram
flowchart TD
subgraph ProcessorAgent [Processor Agent Thread]
P1[StaticEventProcessor]
end
subgraph ServiceAgent [Service Agent Thread]
SVC[Worker Service]
SRC[Agent-hosted Source]
end
EXT[External Source <br>user thread]
SRC -- publish --> Q1[(Queue)] --> P1
SVC -- events/results --> Q2[(Queue)] --> P1
EXT -- publish --> Q3[(Queue)] --> P1
Implications:
- Treat processor state as single-threaded and only mutate it from events delivered into the processor.
- From services/feeds/scheduler/user threads, publish an event to affect a processor; don’t directly mutate handler state.
6) Practical tips¶
- Choose idle strategies per agent based on latency and CPU requirements; see threading-model.md.
- Avoid blocking operations inside agent
doWork()and inside handler event methods; offload to separate threads/services and publish results back. - Name agent groups meaningfully for easier ops (logs, metrics, core pinning).
- For periodic work in a processor, prefer scheduler-driven triggers over sleeping.
When one processor fails¶
A processor group keeps one processor's failure away from the other processors in the group and from the group's
agent error handler (under bootServer, GlobalErrorHandler, which logs it and exits the process only under the opt-in
fatalErrorPolicy: EXIT_PROCESS). Fluxtion-generated processors are quarantined instead of dropped or retried (see
the contract). Each failure is reported once
through ErrorReporting at ERROR:
| Failure | What happens |
|---|---|
start() throws, processor added after the server started |
Reported. The processor is dropped: its tearDown() runs, and its subscriptions, timers, replay bindings and audit-capture binding are released. The group runs on. addProcessor's reply is FAILED; the void addEventProcessor returns, or throws ProcessorStartException with addFailurePolicy: THROW (contract). |
start() throws while the server boots |
The boot fails (default). The processor is dropped as above, and the group's start fails with a ProcessorStartException naming the group and the processor, which goes to the agent error handler. Set isolateProcessorStartFailuresAtBoot: true (YAML, MongooseServerConfig or its builder) to isolate it instead: reported, dropped, and the server boots without it. |
stop() / tearDown() throws |
Reported. The processor is still removed and its subscriptions released. |
@ServiceRegistered / @ServiceDeregistered handler throws |
Reported. The processor keeps running; the broadcast still reaches the other processors. A service whose withdrawal a @ServiceDeregistered handler failed is not stopped by removeService; the server keeps it until shutdown (contract). |
| A timer callback throws | Reported, naming the processor. The other timers fire. |
Every Throwable is caught on these paths except VirtualMachineError (OutOfMemoryError, StackOverflowError, ...),
which still reaches the agent error handler. Under bootServer the default fatalErrorPolicy: CONTAIN keeps the process
up: the group's thread ends, the group is DEAD and group:<name>/control reports DOWN. EXIT_PROCESS exits instead.
7) FAQ¶
- Do all event handlers run on agents?
- Yes. All handler callbacks execute on processor agent threads.
- Can a sink run inside an agent?
- Yes. A sink can be a service hosted in a service agent, or it can run on its own thread.
- Can an event feed be agent-hosted?
- Yes. Agent-hosted sources are common; external user-thread sources can also publish safely into processors.
8) See also¶
- Threading model: threading-model.md
- Architecture overview: overview.md
- Event flow: event-flow.md
- How to core-pin agent threads: how to core pin threads