Skip to content

Latest commit

 

History

History
249 lines (179 loc) · 9.52 KB

event_handler.md

File metadata and controls

249 lines (179 loc) · 9.52 KB

Event Handlers

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:

  1. When add_name receives a command, it appends that string to accumulate. It then reads the value of length and prints it.
  2. When the value of accumulate is set, it writes its own length to the length 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.

The HandlerAction and EventHandler traits

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.

How event handlers are executed.

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:

  1. Complete, ending the process.
  2. Fail with an error. In this case all execution will stop and the agent will fail.
  3. 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 HandlerContext.

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.

Handler combinators

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.

Suspending futures

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.

Unifying EventHandler types

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.