Skip to content

Latest commit

 

History

History
488 lines (372 loc) · 16.4 KB

lifecycle.md

File metadata and controls

488 lines (372 loc) · 16.4 KB

Agent Lifecycles

For any given agent definition there are a number of different events to which event handlers can be attached. Some of these will receive one or more arguments and some receive none.

The AgentLifecycle trait

To attach event handlers to an agent implementation, it is necessary to provide an implementation of the swimos::agent::AgentLifecycle trait.

The details of implementing this trait are somewhat complex and will be covered in a later chapter. In general, lifecycles will be created using the swimos::agent::lifecycle attribute macro.

All further examples in this chapter will use the following agent definition:

use swimos::agent::{AgentLaneModel, projections};

#[projections]
#[derive(AgentLaneModel)]
struct ExampleAgent {
    example_command: CommandLane<i32>,
    example_demand: DemandLane<i32>,
    example_demand_map: DemandMapLane<String, i32>,
    example_value: ValueLane<i32>,
    example_map: MapLane<String, i32>,
    example_join_value: JoinValueLane<String, i32>,
    example_join_map: JoinMapLane<String, String, i32>,
    example_http: SimpleHttpLane<String>,
}

The derivation of an agent lifecycle for this agent will follow this template:

use swimos::agent::lifecycle;

#[derive(Clone, Copy)]
struct ExampleLifecycle;

#[lifecycle(ExampleAgent)]
impl ExampleLifecycle {

    //Definitions of event handlers.

}

With this annotation, the ExampleLifecycle type will now have a function with the signature:

pub fn into_lifecycle(self) -> impl AgentLifecycle + Clone + Send + 'static;

Agent lifecycle events

Regardless of what lanes the agent has, it will have the following events.

  1. The on_start event. This is triggered exactly once when the agent starts and receives no arguments.
  2. The on_stop event. This is triggered just before the agent stops and receives no arguments. It is the last event handler be run by an agent (aside from any that are triggered by the event handler attached to the on_stop event.)

To attach a handler to the on_start method, add a function annotated with #[on_start], with the following signature.

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

The on_stop event is identical but uses the #[on_stop] annotation.

Note that a function (with an appropriate signature) may have any number of lifecycle annotations. For example, the following is entirely acceptable:

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

Additionally, for each lane of the agent, there are events that can be attached to that lane, using its name. For example, to annotate a function to be used for the on_command event of the command lane example_command, the annotation #[on_command(example_command)] would be used.

Command lane events

The command lane has only one type of event that triggers any time it receives a command. The on_command event has the following signature:

#[on_command(example_command)]
fn my_command_handler(&self, context: HandlerContext<ExampleAgent>, value: &i32) -> impl EventHandler<ExampleAgent> {
    //...
}

Demand lane events

The demand lane has only one type of event that triggers when it is explicitly cued or an external sync request is received. In contrast to the other types of lane, this handler produces a value rather than consumes one. This value will then be sent on all connected uplinks (or just to the requester for a sync operation).

The default on_cue handler will produce an error and cause the agent to fail.

#[on_cue(example_demand)]
fn my_demand_handler(&self, context: HandlerContext<ExampleAgent>) -> impl HandlerAction<ExampleAgent, Completion = i32> {
    //...
}

Demand Map lane events

A demand map lane has two types of event that generate its contents, on demand.

  1. on_cue_key: This triggers each time a key of the map is cued to be generated. This occurs upon a manual cue operation and once for each defined key when a downlink attempts to sync with the lane.
  2. keys: This triggers each time a downlink attempts to sync with the lane. It generates the set of keys that are defined.

The on_cue_key event has the following signature:

#[on_cue_key(example_demand_map)]
fn my_demand_handler(&self, context: HandlerContext<ExampleAgent>, key: String) -> impl HandlerAction<ExampleAgent, Completion = Option<i32>> {
    //...
}

The keys event has the following signature:

#[keys(example_demand_map)]
fn my_demand_keys_handler(&self, context: HandlerContext<ExampleAgent>) -> impl HandlerAction<ExampleAgent, Completion = HashSet<i32>> {
    //...
}

Supply lane events

Supply lanes generate no events on a lifecycle.

Value lane events

A value lane generates events each time a new value is set. The two events that are generated are.

  1. on_event: This triggers first and only receives the new value that was set.
  2. on_set: This triggers second and also receives the old value that was replaced.

This signatures of these two events are as follows:

#[on_event(example_value)]
fn my_event_handler(&self, context: HandlerContext<ExampleAgent>, new_value: &i32) -> impl EventHandler<ExampleAgent> {
    //...
}

#[on_set(example_value)]
fn my_set_handler(&self, context: HandlerContext<ExampleAgent>, new_value: &i32, previous_value: Option<i32>) -> impl EventHandler<ExampleAgent> {
    //...
}

Map lane events

A map lanes generates events in the following three circumstances:

  1. on_update: Triggered when an entry in the map is updated. The handler will receive a reference to the contents of the map (after the update), the key that was changed, the previous value (if any) and a reference to the new value.
  2. on_remove: Triggered when an entry is removed. The handler will receive a reference to the contents of the map ( after the removal), the key that was removed and the previous value.
  3. on_clear: Triggered when the contents of the map are cleared. The handler will receive the previous contents of the map.

The signatures of these events are as follows:

#[on_update(example_map)]
fn my_update_handler(
    &self, 
    context: HandlerContext<ExampleAgent>,
    map: &HashMap<String, i32>,
    key: String,
    previous_value: Option<i32>,
    new_value: &i32) -> impl EventHandler<ExampleAgent> {
    //...
}

#[on_remove(example_map)]
fn my_remove_handler(
    &self, 
    context: HandlerContext<ExampleAgent>,
    map: &HashMap<String, i32>,
    key: String,
    previous_value: i32) -> impl EventHandler<ExampleAgent> {
    //...
}

#[on_clear(example_map)]
fn my_clear_handler(
    &self, 
    context: HandlerContext<ExampleAgent>,
    previous_map: HashMap<String, i32>) -> impl EventHandler<ExampleAgent> {
    //...
}

Join value lane events

A join value lane supports all of the events supported by a map lane (on_update, on_remove and on_clear).

Additionally, it is possible to attach events to the downlinks used for each key of the join value lane. To do this, add the following to the agent lifecycle implementation:

#[join_value_lifecycle(example_join_value)]
fn register_lifecycle(
    &self,
    context: JoinValueContext<ExampleAgent, String, i32>,
) -> impl JoinValueLaneLifecycle<String, i32, ExampleAgent> + 'static {
    context.builder()
        .on_linked(|context, key, address| context.effect(move || {
            println!("Linked downlink for key: {} from lane at: {}", key, address);
        }))
        .done()
}

An instance of the lifecycle will be created for each link that is attached to the lane so it is necessary that the lifecycle is Clone and has a static lifetime.

Supported handlers are on_linked, on_synced, on_unlinked and on_failed. These take closures with the following signatures:

  1. on_linked:
(context: HandlerContext<ExampleAgent>, key: K, address: Address<&str>) -> impl EventHandler<ExampleAgent>
  1. on_synced:
(context: HandlerContext<ExampleAgent>, key: K, address: Address<&str>, value: Option<&V>) -> impl EventHandler<ExampleAgent>
  1. on_unlinked and on_failed:
(context: HandlerContext<ExampleAgent>, key: K, address: Address<&str>) -> impl HandlerAction<ExampleAgent, Completion = LinkClosedResponse>

The on_unlinked and on_failed handlers return a LinkClosedResponse. This is an enumeration with the values Retry, Abandon and Delete. These have the following effects:

  1. Retry: An attempt will be made to reconnect the link (TODO: this is not implemented yet.)
  2. Abandon: The link will be abandoned by the entry will still remain in the map. (This is the default.)
  3. Delete: The link will be abandoned and the entry deleted from the map.

It is possible to add a shared state to a join value lifecycle in a similar way to other downlinks types ( see Downlinks). Note that this state is shared between the event handlers of a single instance of the lifecycle and not between all instances. If you have state that needs to be shared between all instances it must be stored inside an Arc.

Join map lane events

A join map lane supports all of the events supported by a map lane (on_update, on_remove and on_clear).

Additionally, it is possible to attach events to the downlinks used for each key of the join value lane. To do this, add the following to the agent lifecycle implementation:

#[join_map_lifecycle(example_join_map)]
fn register_lifecycle(
    &self,
    context: JoinMapContext<ExampleAgent, String, String, i32>,
) -> impl JoinMapLaneLifecycle<String, String, ExampleAgent> + 'static {
    context.builder()
        .on_linked(|context, link_key, address| context.effect(move || {
            println!("Linked downlink for link: {} from lane at: {}", key, address);
        }))
        .done()
}

An instance of the lifecycle will be created for each link that is attached to the lane so it is necessary that the lifecycle is Clone and has a static lifetime.

Supported handlers are on_linked, on_synced, on_unlinked and on_failed. These take closures with the following signatures:

  1. on_linked:
(context: HandlerContext<ExampleAgent>, link_key: L, address: Address<&str>) -> impl EventHandler<ExampleAgent>
  1. on_synced:
(context: HandlerContext<ExampleAgent>, link_key: L, address: Address<&str>, keys: &HashSet<K>) -> impl EventHandler<ExampleAgent>
  1. on_unlinked and on_failed:
(context: HandlerContext<ExampleAgent>, link_key: L, address: Address<&str>, keys: HashSet<K>) -> impl HandlerAction<ExampleAgent, Completion = LinkClosedResponse>

The on_unlinked and on_failed handlers return a LinkClosedResponse. This is an enumeration with the values Retry, Abandon and Delete. These have the following effects:

  1. Retry: An attempt will be made to reconnect the link (TODO: this is not implemented yet.)
  2. Abandon: The link will be abandoned by the entry will still remain in the map. (This is the default.)
  3. Delete: The link will be abandoned and the entry deleted from the map.

It is possible to add a shared state to a join value lifecycle in a similar way to other downlinks types ( see Downlinks). Note that this state is shared between the event handlers of a single instance of the lifecycle and not between all instances. If you have state that needs to be shared between all instances it must be stored inside an Arc.

HTTP lane events

An HTTP lane generates events when HTTP requests are received for that lane. The supported methods are GET, PUT, POST, DELETE and HEAD. The event handlers used to serve these methods are named.

  1. on_get for GET.
  2. on_put for PUT.
  3. on_post for POST.
  4. on_delete for DELETE.

When a HEAD request is received, the on_get handler will be called and then the payload of the response will be discarded. The signatures of these events are as follows:

#[on_get(example_http)]
fn my_get_handler(
    &self, 
    context: HandlerContext<ExampleAgent>,
    http_context: HttpRequestContext) -> impl HandlerAction<ExampleAgent, Completion = Response<String>> {
    //...
}

#[on_put(example_http)]
fn my_put_handler(
    &self, 
    context: HandlerContext<ExampleAgent>,
    http_context: HttpRequestContext,
    value: String) -> impl HandlerAction<ExampleAgent, Completion = UnitResponse> {
    //...
}

#[on_post(example_http)]
fn my_post_handler(
    &self, 
    context: HandlerContext<ExampleAgent>,
    http_context: HttpRequestContext,
    value: String) -> impl HandlerAction<ExampleAgent, Completion = UnitResponse> {
    //...
}

#[on_delete(example_http)]
fn my_delete_handler(
    &self, 
    context: HandlerContext<ExampleAgent>,
    http_context: HttpRequestContext) -> impl HandlerAction<ExampleAgent, Completion = UnitResponse> {
    //...
}

The HttpRequestContext passed to each of these handlers provides access to the request URI and the headers that were set in the request.

The Response type produced by the event handlers contains the payload, status code and any custom headers. It is the responsibility of the codec associated with the lane to interpret the content type and accept headers from the request and to append the correct content type header to the response. In most cases a response can be constructed as:

Response::from(value)

where value is the payload. The status code will be 200(OK) and no extra headers will be set. UnitResponse is equivalent to Response<()>.

Store events

The store types support exactly the same event handlers as their lane equivalents (for example ValueStores have the same events as ValueLanes).

Borrowing from lifecycles

Although none of the examples so far have used event handlers that borrow any data from the lifecycle, this is entirely possible.

Consider the following, alternative lifecycle type.

#[derive(Clone)]
struct NamedLifecycle {
    name: String
}

#[lifecycle(ExampleAgent)]
impl NamedLifecycle {

    // Definitions of event handlers.

}

We can attach an event to the value lane that uses the name as follows:

#[on_event(example_value)]
fn with_name_event<'a>(
    &'a self,
    context: HandlerContext<ExampleAgent>,
    value: &i32,
) -> impl EventHandler<ExampleAgent> + 'a {
    let n = *value;
    context.effect(move || {
        println!("name = {}, value = {}", self.name, n)
    })
}

Note that, whilst we can borrow &self.name safely, it is not possible to move the borrow of value into the event handler. This is as the current value of the lane can change during the execution of the event handler so it is not possible to hold a borrow of it.

Interior mutability in lifecycles

The reference to self for the agent lifecycle is always passed as &self and a function taking &mut self cannot be used for an event handler. This is as an event handler can trigger other handlers on the same lifecycle (even itself) which results in multiple borrows of the data in the lifecycle.

Often, it will not be possible for a stateful lifecycle to be Clone (as the state may not be cloneable). In such a case, the no_clone option to the lifecycle macro. When registering the agent in the server, it will be necessary to provide a factory rather than a template instance.

However, due to the way in which lifecycles are executed, only one event handler will be executing at any one time. This means that it is safe to use interior mutability in a lifecycle without requiring locking. Consider the following alternative lifecycle:

struct MutableLifecycle {
    list: RefCell<Vec<i32>>,
}

#[lifecycle(ExampleAgent, no_clone)]
impl MutableLifecycle {

    #[on_command(example_command)]
    fn add_to_list<'a>(
        &'a self,
        context: HandlerContext<ExampleAgent>,
        command: &i32) -> impl EventHandler<ExampleAgent> + 'a {
        let n = *command;
        context.effect(move || {
            self.list.borrow_mut().push(n);
        })
    }

}

This is guaranteed to never panic as only one mutable borrow of the list can exist at any time.