After having defined the lanes and stores of the agent, we now want to add logic to be executed when particular events occur (for example, an event is fired each time an agent starts). Naively, we might expect that this could simply be a function that is called in response to the event.
Consider the following agent definition:
#[derive(AgentLaneModel)]
struct MyAgent {
add_name: CommandLane<String>,
accumulate: ValueLane<Vec<String>>,
length: ValueLane<u64>,
}
We want to attach the following behaviours:
- When
add_name
receives a command, it appends that string toaccumulate
. It then reads the value oflength
and prints it. - When the value of
accumulate
is set, it writes its own length to thelength
lane.
These two event handlers are independent of each other. However, the behaviour we expect is that when the handler
on add_name
reads the value of length
, it should see the total after the event handler on accumulate
has
executed.
It is guaranteed that a single agent runs within one task in the asynchronous runtime and so we need not worry about the event handlers executing on separate threads. However, satisfying the above expectation implies that the execution of event handlers can be suspended to run other handlers.
To accomplish this, rather than being modelled as simple function, event handlers are modelled as state machines. In
fact, the definition looks very similar to the Future
trait in the standard library.
Event handlers are modelled by the HandlerAction
trait:
trait HandlerAction<AgentType> {
type Completion;
/// ... implementation details.
}
The trait is parameterized on the type of the agent to allow handlers to interact with the lanes of the agent (by for
example setting a new value). The Completion
associated type is the type of the value that the action will produce
when it completes. (It is also possible that an action could fail and instead produce an error).
An event handler is an action that does not result in any final value. Particularly:
trait EventHandler<Context>: HandlerAction<Context, Completion = ()> {}
Every type that implements HandlerAction
with Completion = ()
also implements EventHandler
through a blanket
implementation.
It is not necessary to consider the methods of the HandlerAction
trait as it is not intended that you will need to
implement it directly. Instead, event handlers can be built up by composing simple predefined actions, using a suite of
handler combinators.
The canonical EventHandler
that will do nothing is the UnitHandler
which will immediately complete.
The process of running an event handler is similar to polling a Future
. The event handler is called, providing a
context that allows it to access the lanes of the agent, and performs some work. It may then either:
- Complete, ending the process.
- Fail with an error. In this case all execution will stop and the agent will fail.
- Yield back to the agent. In this case the handler will indicate if it has affected the state of any other lane in the agent.
In the third case, a check will be performed to determine if any event handlers are triggered on the lane that the handler has modified. If so, the process is then run recursively on that handler until it completes or fails. Following that, execution of the original handler resumes.
Note that there is currently no logic to detect an infinite chain of handler triggers. Therefore, any infinitely descending chain of events will exhaust the stack. It is intended to add heuristics to guard against this in future.
The aid in the construction of event handlers, a factor type is provided in the form of:
swimos::agent::agent_lifecycle::HandlerContext<AgentType>
This allows for the easier construction of event handlers that will run in the context of the AgentType
. Typically, in
locations where an event handler is required, an instance of this type will be provided. (It is always possible to
create event handlers directly, the utility simply aids with type inference and reduces the need for type ascriptions in
the code).
The simplest possible event handler runs a side effect. For example:
fn make_handler(context: HandlerContext<AgentType>) -> impl EventHandler<AgentType> {
context.effect(|| println!("Hello World!"))
}
An effect can be any function/closure satisfying FnOnce()
.
Additionally, the context allows for the creation of event handlers that will interact with the lanes of the agent.
Consider the following simple agent definition:
#[derive(AgentLaneModel)]
struct Example {
name: ValueLane<String>
}
To create an event handler that would set the value of the name
lane, you would do the following:
fn set_name_handler(context: HandlerContext<Example>) -> impl EventHandler<Example> {
context.set_value(|agent| &agent.name, "foo".to_string())
}
The first argument to the set_value
method is a projection which indicates which lane that the handler should be
applied to. For example, in this case it has the type:
fn(&Example) -> &ValueLane<String>
As projections of this kind will be required quite frequently, an attribute macro is provided to generate them for you. This could be applied to the previous example as follows:
use swimos::agent::{AgentLaneModel, projections};
#[projections]
#[derive(AgentLaneModel)]
struct Example {
name: ValueLane<String>
}
This is equivalent to adding an impl block like:
impl Example {
const NAME: fn(&Example) -> &ValueLane<String> = |agent: &Example| &agent.name;
}
and allows the event handler to be written as:
fn set_name_handler(context: HandlerContext<Example>) -> impl EventHandler<Example> {
context.set_value(Example::NAME, "foo".to_string())
}
Additional basic handlers are available to interact with other types of lane and to get the route URI of the running
agent instance. See the documentation for HandlerContext
for more details.
A number of combinators are provided to combine handler actions together. This are available as extensions methods using
the swimos::agent::event_handler::HandlerActionExt
trait. As an example, consider the and_then
combinator. When
applied to a handler action this applies a closure to the result of that handler which, in turn produces another
handler. When an and_then
actions is executed, the first handler action runs to completion, the function is applied to
the result, and execution continues with the resulting handler action. As an example:
fn print_uri_handler(context: HandlerContext<AgentType>) -> impl EventHandler<AgentType> {
context.get_agent_uri().and_then(move |uri| context.effect(move || {
println!("Agent is running at: {}", uri);
}))
}
This compound handler will fetch the route URI at which the agent is running and then print it to the console.
For more details on the available combinators, look at the documentation for HandlerActionExt
.
Event handlers run synchronously and so should never block the thread that is running them. Blocking code in an event handler will cause the host agent to lock up and stop handling input.
This has the consequence that it is impossible to use an async function/closure as an event handler directly. However, it is possible for an event handler to suspend an asynchronous function that will execute in the background. When this future completes it will result in another event handler which will then be executed by the agent.
Futures can be suspended using the HandlerContext
. Consider the following example:
async fn long_operation(context: HandlerContext<AgentType>) -> impl EventHandler<AgentType> {
// ... Asynchronous operations.
let result: i32 = ...;
context.effect(move || {
println!("Operation completed with: {}", result)
})
}
fn make_handler()(context: HandlerContext<AgentType>) => impl EventHandler<AgentType> {
context.suspend(long_operation(context))
}
This will suspend the function long_operation
into the background where it will be executed concurrently with the rest
of the agent. When it completes, it will create a header that prints the result of operation which will then be executed
by the agent, as with any other event handler.
As arbitrary futures may be suspended in this way, the resulting event handlers are necessarily executed by dynamic dispatch.
All of the functions that associate an EventHandler
with an event expect a return of:
impl EventHandler<AgentType>
This implies that the function is expected to return handlers of a single type only.
The HandlerAction
trait is implemented for Option<H>
where H: HandlerAction<..>
(resulting
in Option<H::Completion>
).
For a fixed enumeration of HandlerAction
types, it would be possible to manually implement the HandlerAction
trait,
however, in most cases this would be unnecessarily verbose.
The EventHandler
trait is object safe so, in situations where a little overhead is acceptable, it is possible to
return an event handler as:
pub type BoxEventHandler<'a, Context> = Box<dyn EventHandler<Context> + 'a>;
by simply boxing the various types of handler being returned.