Agent Lifecycles

Agent lifecycles provide the ability to build powerful, stateful, streaming data applications and they are where you will spend most of your time when writing SwimOS server applications. Inside an agent lifecycle implementation, a number of event handlers may be registered which are invoked at various stages within the lifecycle. There are two types of event handlers: agent handlers, and lane handlers.

Each lifecycle event handler accepts a HandlerContext (which aids in the creation of EventHandlers) and returns an EventHandler which is scoped to the agent that the lifecycle is bound to; this allows the handler to interact with the lanes within the agent. An EventHandler models an action that happens within an agent after an event is triggered; such as an agent starting or an event was received. Most handlers do not return a value but they may trigger an event to happen elsewhere within an agent, such as updating the state of a lane or running an async task; these handlers are created using the provided HandlerContext.

Event handlers are modelled as state machines and their execution closely resembles an implementation of the Future trait. A lane event handler may trigger the state of another lane to change and as a result, its event handler will also be executed. These events happen sequentially and the first handler will not complete until its handler’s dependency chain has been executed. This ensures that event obserability is maintained and the first handler will correctly view the state of the lanes that it has mutated.

Agent Handlers

Agent lifecycle event handlers are defined with the agent’s lifecycle implementation and two handlers may be declared:

There is no requirement for any agent lifecycle event handlers to be registered. Handlers are added to the implementation of the agent’s lifecycle. For example, to attach on_start and on_stop handlers:

use swimos::agent::{lifecycle, agent_lifecycle::utility::HandlerContext, event_handler::EventHandler};

#[derive(Clone)]
pub struct ExampleLifecycle;

#[lifecycle(ExampleAgent)]
impl ExampleLifecycle {
    #[on_start]
    pub fn on_start(
        &self,
        context: HandlerContext<ExampleAgent>,
    ) -> impl EventHandler<ExampleAgent> {
        context.effect(|| println!("Starting agent."))
    }

    #[on_stop]
    pub fn on_stop(
        &self,
        context: HandlerContext<ExampleAgent>,
    ) -> impl EventHandler<ExampleAgent> {
        context.effect(|| println!("Stopping agent."))
    }
}

Lane Handlers

Lane event handlers which are invoked when a lane receives an event. For example, for a ValueLane you may register a handler that is invoked after the state of the lane has changed. These handlers allow your agent to react to the events that it receives and to trigger updates elsewhere.

There is no requirement for any handlers to be registered and you could just have a stateful representation of your streaming data. However, handlers provide the functionality to be notified of certain events that happen during the lifecycle of an agent.

To register an event handler for ExampleAgent’s value lane, the function would be:

#[on_event(lane)]
pub fn on_event(
    &self,
    context: HandlerContext<ExampleAgent>,
    value: &i32,
) -> impl EventHandler<ExampleAgent> {
    // Build and return an EventHandler.
    //
    // It is in these implementations where your application logic will reside.
}

Going over the implementation:

Replace the lifecycle implementation with the following:

#[lifecycle(ExampleAgent)]
impl ExampleLifecycle {
    // Handler invoked when the agent starts.
    #[on_start]
    pub fn on_start(
        &self,
        context: HandlerContext<ExampleAgent>,
    ) -> impl EventHandler<ExampleAgent> {
        context.effect(|| println!("Starting agent."))
    }

    // Handler invoked when the agent is about to stop.
    #[on_stop]
    pub fn on_stop(
        &self,
        context: HandlerContext<ExampleAgent>,
    ) -> impl EventHandler<ExampleAgent> {
        context.effect(|| println!("Stopping agent."))
    }

    // Handler invoked after the state of 'lane' has changed.
    #[on_event(state)]
    pub fn on_event(
        &self,
        context: HandlerContext<ExampleAgent>,
        value: &i32,
    ) -> impl EventHandler<ExampleAgent> {
        let n = *value;
        // EventHandler::effect accepts a FnOnce()
        // which runs a side effect.
        context.effect(move || {
            println!("Setting value to: {}", n);
        })
    }
}

Running the applications

Now that both the client and server applications have been built they can be run and both will produce outputs.

The client application will produce:

Downlink event: 0
Downlink event: 1
Downlink event: 2
Downlink event: 3
Downlink event: 4
Downlink event: 5
Downlink event: 6
Downlink event: 7
Downlink event: 8
Downlink event: 9

And the server will produce:

Starting agent.
Setting value to: 0
Setting value to: 1
Setting value to: 2
Setting value to: 3
Setting value to: 4
Setting value to: 5
Setting value to: 6
Setting value to: 7
Setting value to: 8
Setting value to: 9

After the agent timeout period has elapsed you will observe a message that the agent is stopping too:

Stopping agent.

Expanding

The server will continue listening for events indefinately, starting and stopping the agents as required. Feel free to play around with what has been put together to get familar with what has been covered. You could:

The next section in this developer guide will cover using custom types.