Foreward

Fluxion exists to fulfil a very specific usecase: an actor library that can send messages between systems. How these systems are connected does not alter the behavior of Fluxion, nor should it matter to actors running on a Fluxion system. Fluxion is also different from traditional actor libraries in that it does not provide actors mutable access to themselves, nor does it provide "fire and forget" messages.

Fluxion makes both of these restrictions in the name of performance and extensibility. If Fluxion were to allow actors mutable access to themselves, Fluxion would need to implement a layer of synchronization on top of each actor. This needlessly harms the performance of actors that do not need mutable access. Additionally, actors that do need mutable access to themselves can simply implement their own synchronization where needed. Fluxion doesn't provide "fire and forget" messages, because it would require a dependency on a specific async executor. This is because Fluxion, via Slacktor, uses a "simulated messaging" system where raw function calls are used instead of channels. This significantly increases performance. If "fire and forget" messages are required, a user can implement them by spawning a new async task to send the message from.

Importantly, Fluxion is not an actor framework, but an actor library. Fluxion will not force your entire application to be designed following a specific pattern, and can be used as much, or as little as you want.

Due to Fluxion's limited scope and architecture, it tends to be very performant, especially when compared to other actor frameworks. Running the benchmark example (which is designed to test Fluxion's raw performance, not necessarily the performance of the actual actors implemented), Fluxion acheives a throughput of ~60 million messages per second on an intel i5-9400 compiled with release. The equivalent code running on Actix can only handle ~700,000 messages per second.

First Steps

Lets start by creating a Cargo project (in the current directory) and adding Fluxion:

cargo init project_name
cd project_name
cargo add fluxion

Fluxion needs to be called from an async context. As Fluxion is executor agnostic, it doesn't matter which library is used. Here we will be using Tokio, although any executor will work:

cargo add tokio --features full

Next, we need to make the main function async, and import a few helpers from Fluxion, and create the Fluxion system:

use fluxion::Fluxion;

#[tokio::main]
async fn main() {
    // Create the Fluxion system:
    let system = Fluxion::new("system_id", ());
}

The above code initializes a Fluxion system with the id "system_id" and the delegate ().

What is a delegate?

A delegate is an external type that provides methods to retrieve MessageSenders, which allow actors to communicate with actors on external systems. The unit type (()) is a simple delegate that always returns that no foreign actor was found. Later in this book, we will explore creating our own simple delegate.

Now that we have a system, we need to add an actor to it, and to do that, we must define an actor.

Defining Actors

Any type that is Send, Sync, and 'static can be an actor. Lets go ahead and define a unit struct, and use the actor macro to implement the Actor trait automatically.

use fluxion::actor;

#[actor]
struct MyActor;

Adding the Actor to the System

Adding actors to the system is rather simple:

let id = system.add(MyActor).await.unwrap();

This runs the actor's initialization method, adds the actor to the system, and returns the actor's ID.

The actor's ID can be used to retrieve a reference to the actor from the system. There are two ways to retrieve an actor from the system: get and get_local. We will take a look at get_local first:

let actor_ref = system.get_local::<MyActor>(id).await.unwrap();

The get_local method requires only the actor's type and ID, and returns a concrete type that depends on the actor's type. This allows a single actor reference to send any message that the actor implements, however, this message must reside on the local system.

get, on the other hand, enables foreign messages, but also requires that the message type is specified. The returned type, however, is abstracted over handlers of the message type.

To use get, we must first define a message type.

Defining Messages

Messages have similar requirements to actors, and we can use a macro to define them as well:

#[message(())]
struct MyMessage;

The above code defines MyMessage as a message with the response type (). Messages also each have an ID, which we can optionally set:

#[message((), "my_message")]
struct MyMessage;

We will just use the following code, however, as it sets the message's response to the default () and the message's ID to its full module path (in this case, project_name::MyMessage):

#[message]
struct Mymessage;

Handling Messages# Defining and Sending Messages

Implementing a message handler on an actor is relatively simple:

use fluxion::Handler;

impl Handler<MyMessage> for MyActor {
    async fn handle_message<D: Delegate>(&self, message: TestMessage, context: &ActorContext<D>) {
        println!("{:?} received by {}", message, context.get_id());
    }
}

Message handlers have access to the message, and to a context that provides information about the current actor, as well as access to the system to create more actors and send further messages.

Sending a message is pretty simple. Using the local handle we previously retrieved, we can send any message type that is handled by the actor:

use fluxion::MessageSender;

actor_ref.send(MyMessage).await;

We do not need to unwrap this call, because message sending will never error, and just returns the type dictated by the message's result. In this case, we used ().

Now we can also retrieve a MessageSender, which can only send a specific message type:

let actor_ref = system.get::<MyActor, MyMessage>(id).await.unwrap();
actor_ref.send(MyMessage).await;

We will look closer at MessageSenders when we get to foreign messages in the future. Our final code for this section, with thorough comments, can be found in the simple example on github.

Foreign Messages

This section has not yet been developed. A (still rather rough) example of foreign messages can be found in the foreign example. It requires both the serde and the foreign feature flags to compile. The example just uses slacktor as a basic method to communicate between two delegates, however any other mechanism will work (IPC pipes, sockets, even a serial port), as long as you can implement request/response semantics on top of it.