-
Notifications
You must be signed in to change notification settings - Fork 66
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
mdbook contributions for new crates (#845)
docs(arbiter-engine / arbiter-macros): mdbook contributions for new crates and their usage.
- Loading branch information
1 parent
28f7ab3
commit ad701cb
Showing
14 changed files
with
429 additions
and
9 deletions.
There are no files selected for viewing
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
Oops, something went wrong.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,75 @@ | ||
use std::{str::FromStr, time::Duration}; | ||
|
||
use agents::{token_admin::TokenAdmin, token_requester::TokenRequester}; | ||
use arbiter_core::data_collection::EventLogger; | ||
use arbiter_macros::Behaviors; | ||
use ethers::types::Address; | ||
use tokio::time::timeout; | ||
|
||
use super::*; | ||
use crate::world::World; | ||
|
||
#[tokio::test(flavor = "multi_thread", worker_threads = 4)] | ||
async fn token_minter_simulation() { | ||
let mut world = World::new("test_world"); | ||
let client = RevmMiddleware::new(&world.environment, None).unwrap(); | ||
|
||
// Create the token admin agent | ||
let token_admin = Agent::builder(TOKEN_ADMIN_ID); | ||
let mut token_admin_behavior = TokenAdmin::new(Some(4)); | ||
token_admin_behavior.add_token(TokenData { | ||
name: TOKEN_NAME.to_owned(), | ||
symbol: TOKEN_SYMBOL.to_owned(), | ||
decimals: TOKEN_DECIMALS, | ||
address: None, | ||
}); | ||
// Create the token requester agent | ||
let token_requester = Agent::builder(REQUESTER_ID); | ||
let mut token_requester_behavior = TokenRequester::new(Some(4)); | ||
world.add_agent(token_requester.with_behavior(token_requester_behavior)); | ||
|
||
world.add_agent(token_admin.with_behavior(token_admin_behavior)); | ||
|
||
let arb = ArbiterToken::new( | ||
Address::from_str("0x240a76d4c8a7dafc6286db5fa6b589e8b21fc00f").unwrap(), | ||
client.clone(), | ||
); | ||
let transfer_event = arb.transfer_filter(); | ||
|
||
let transfer_stream = EventLogger::builder() | ||
.add_stream(arb.transfer_filter()) | ||
.stream() | ||
.unwrap(); | ||
let mut stream = Box::pin(transfer_stream); | ||
world.run().await; | ||
let mut idx = 0; | ||
|
||
loop { | ||
match timeout(Duration::from_secs(1), stream.next()).await { | ||
Ok(Some(event)) => { | ||
println!("Event received in outside world: {:?}", event); | ||
idx += 1; | ||
if idx == 4 { | ||
break; | ||
} | ||
} | ||
_ => { | ||
panic!("Timeout reached. Test failed."); | ||
} | ||
} | ||
} | ||
} | ||
|
||
#[derive(Serialize, Deserialize, Debug, Behaviors)] | ||
enum Behaviors { | ||
TokenAdmin(TokenAdmin), | ||
TokenRequester(TokenRequester), | ||
} | ||
|
||
#[tokio::test(flavor = "multi_thread", worker_threads = 4)] | ||
async fn config_test() { | ||
let mut world = World::new("world"); | ||
world.build_with_config::<Behaviors>("src/examples/minter/config.toml"); | ||
|
||
world.run().await; | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,5 +1,11 @@ | ||
[[ping]] | ||
TimedMessage = { delay = 1, send_data = "ping", receive_data = "pong", startup_message = "ping" } | ||
|
||
[[ping]] | ||
TimedMessage = { delay = 1, send_data = "zam", receive_data = "zim", startup_message = "zam" } | ||
|
||
[[pong]] | ||
TimedMessage = { delay = 1, send_data = "pong", receive_data = "ping" } | ||
|
||
[[pong]] | ||
TimedMessage = { delay = 1, send_data = "zim", receive_data = "zam" } |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,6 +1,5 @@ | ||
[package] | ||
name = "arbiter-macros" | ||
version = "0.1.0" | ||
|
||
[lib] | ||
proc-macro = true | ||
|
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file was deleted.
Oops, something went wrong.
58 changes: 58 additions & 0 deletions
58
documentation/src/usage/arbiter_engine/agents_and_engines.md
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,58 @@ | ||
# Agents and Engines | ||
`Behavior`s are the heartbeat of your `Agent`s and they are wrapped by `Engine`s. | ||
The main idea here is that you can have an `Agent` that has as many `Behavior`s as you like, and each of those behaviors may process different types of events. | ||
This gives flexibility in how you want to design your `Agent`s and what emergent properties you want to observe. | ||
|
||
## Design Principles | ||
It is up to you whether or not you prefer to have `Agent`s have multiple `Behavior`s or not or if you want them to have a single `Behavior` that processes all events. | ||
For the former case, you will build `Behavior<E>` for different types `E` and place these inside of an `Agent`. | ||
For the latter, you will create an `enum` that wraps all the different types of events that you want to process and then implement `Behavior` on that `enum`. | ||
The latter will also require a `stream::select` type of operation to merge all the different event streams into one, though this is not difficult to do. | ||
|
||
## `struct Agent` | ||
The `Agent` struct is the primary struct that you will be working with. | ||
It contains an ID, a client (`Arc<RevmMiddleware>`) that provides means to send calls and transactions to an Arbiter `Environment`, and a `Messager`. | ||
It looks like this: | ||
```rust | ||
pub struct Agent { | ||
pub id: String, | ||
pub messager: Messager, | ||
pub client: Arc<RevmMiddleware>, | ||
pub(crate) behavior_engines: Vec<Box<dyn StateMachine>>, | ||
} | ||
``` | ||
|
||
Your work will only be to define `Behavior`s and then add them to an `Agent` with the `Agent::with_behavior` method. | ||
|
||
The `Agent` is inactive until it is paired with a `World` and then it is ready to be run. | ||
This is handled by creating a world (see: [Worlds and Universes](./worlds_and_universes.md)) and then adding the `Agent` to the `World` with the `World::add_agent` method. | ||
Some of the intermediary representations are below: | ||
|
||
#### `struct AgentBuilder` | ||
The `AgentBuilder` struct is a builder pattern for creating `Agent`s. | ||
This is essentially invisible for the end-user, but it is used internally so that `Agent`s can be built in a more ergonomic way. | ||
|
||
#### `struct Engine<B,E>` | ||
Briefly, the `Engine<B,E>` struct provides the machinery to run a `Behavior<E>` and it is not necessary for you to handle this directly. | ||
The purpose of this design is to encapsulate the `Behavior<E>` and the event stream `Stream<Item = E>` that the `Behavior<E>` will use for processing. | ||
This encapsulation also allows the `Agent` to hold onto `Behavior<E>` for various different types of `E` all at once. | ||
|
||
## Example | ||
Let's create an `Agent` that has two `Behavior`s using the `Replier` behavior from before. | ||
```rust | ||
use arbiter_engine::agent::Agent; | ||
use crate::Replier; | ||
|
||
fn setup() { | ||
let ping_replier = Replier::new("ping", "pong", 5, None); | ||
let pong_replier = Replier::new("pong", "ping", 5, Some("ping")); | ||
let agent = Agent::new("my_agent") | ||
.with_behavior(ping_replier) | ||
.with_behavior(pong_replier); | ||
} | ||
``` | ||
In this example, we have created an `Agent` with two `Replier` behaviors. | ||
The `ping_replier` will reply to a message with "pong" and the `pong_replier` will reply to a message with "ping". | ||
Given that the `pong_replier` has a `startup_message` of "ping", it will send a message to everyone (including the "my_agent" itself who holds the `ping_replier` behavior) when it starts up. | ||
This will start a chain of messages that will continue in a "ping" "pong" fashion until the `max_count` is reached. | ||
``` |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,95 @@ | ||
# Behaviors | ||
The design of `arbiter-engine` is centered around the concept of `Agent`s and `Behavior`s. | ||
At the core, we place `Behavior`s as the event-driven machinery that defines the entire simulation. | ||
What we want is that your simulation is defined completely with how your `Agent`s behaviors are defined. | ||
All you should be looking for is how to define your `Agent`s behaviors and what emergent properties you want to observe. | ||
|
||
## `trait Behavior<E>` | ||
To define a `Behavior`, you need to implement the `Behavior` trait on a struct of your own design. | ||
The `Behavior` trait is defined as follows: | ||
```rust | ||
pub trait Behavior<E> { | ||
fn startup(&mut self, client: Arc<RevmMiddleware>, messager: Messager) -> EventStream<E>; | ||
fn process(&mut self, event: E) -> Option<MachineHalt>; | ||
} | ||
``` | ||
To outline the design principles here: | ||
- `startup` is a method that initializes the `Behavior` and returns an `EventStream` that the `Behavior` will use for processing. | ||
- This method yields a client and messager from the `Agent` that owns the `Behavior`. | ||
In this method you should take the client and messager and store them in your struct if you will need them in the processing of events. | ||
Note, you may not need both or even either! | ||
- `process` is a method that processes an event of type `E` and returns an `Option<MachineHalt>`. | ||
- If `process` returns `Some(MachineHalt)`, then the `Behavior` will stop processing events completely. | ||
|
||
**Summary:** A `Behavior<E>` is tantamount to engage the processing some events of type `E`. | ||
|
||
**Advice:** `Behavior`s should be limited in scope and should be a simplistic action driven from a single event. | ||
Otherwise you risk having a `Behavior` that is too complex and difficult to understand and maintain. | ||
|
||
### Example | ||
To see this in use, let's take a look at an example of a `Behavior` called `Replier` that replies to a message with a message of its own, and stops once it has replied a certain number of times. | ||
```rust | ||
use std::sync::Arc; | ||
use arbiter_core::middleware::RevmMiddleware; | ||
use arbiter_engine::{ | ||
machine::{Behavior, MachineHalt}, | ||
messager::{Messager, To}, | ||
EventStream}; | ||
|
||
pub struct Replier { | ||
receive_data: String, | ||
send_data: String, | ||
max_count: u64, | ||
startup_message: Option<String>, | ||
count: u64, | ||
messager: Option<Messager>, | ||
} | ||
|
||
impl Replier { | ||
pub fn new( | ||
receive_data: String, | ||
send_data: String, | ||
max_count: u64, | ||
startup_message: Option<String>, | ||
) -> Self { | ||
Self { | ||
receive_data, | ||
send_data, | ||
startup_message, | ||
max_count, | ||
count: 0, | ||
messager: None, | ||
} | ||
} | ||
} | ||
|
||
impl Behavior<Message> for Replier { | ||
async fn startup( | ||
&mut self, | ||
client: Arc<RevmMiddleware>, | ||
messager: Messager, | ||
) -> EventStream<Message> { | ||
if let Some(startup_message) = &self.startup_message { | ||
messager.send(To::All, startup_message).await; | ||
} | ||
self.messager = Some(messager.clone()); | ||
return messager.stream(); | ||
} | ||
|
||
async fn process(&mut self, event: Message) -> Option<MachineHalt> { | ||
if event.data == self.receive_data { | ||
self.messager.unwrap().messager.send(To::All, send_data).await; | ||
self.count += 1; | ||
} | ||
if self.count == self.max_count { | ||
return Some(MachineHalt); | ||
} | ||
return None | ||
} | ||
} | ||
``` | ||
In this example, we have a `Behavior` that upon `startup` will see if there is a `startup_message` assigned and if so, send it to all `Agent`s that are listening to their `Messager`. | ||
Then, it will store the `Messager` for sending messages later on and start a stream of incoming messages so that we have `E = Message` in this case. | ||
Once these are completed, the `Behavior` automatically transitions into the `process`ing stage where events are popped from the `EventStream<E>` and fed to the `process` method. | ||
|
||
As messages come in, if the `receive_data` matches the incoming message, then the `Behavior` will send the `send_data` to all `Agent`s listening to their `Messager` a message with data `send_data`. |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,66 @@ | ||
# Configuration | ||
To make it so you rarely have to recompile your project, you can use a configuration file to set the parameters of your simulation once your `Behavior`s have been defined. | ||
Let's take a look at how to do this. | ||
|
||
## Behavior Enum | ||
It is good practice to take your `Behavior`s and wrap them in an `enum` so that you can use them in a configuration file. | ||
For instance, let's say you have two struct `Maker` and `Taker` that implement `Behavior<E>` for their own `E`. | ||
Then you can make your `enum` like this: | ||
```rust | ||
use arbiter_macros::Behaviors; | ||
|
||
#[derive(Behaviors)] | ||
pub enum Behaviors { | ||
Maker(Maker), | ||
Taker(Taker), | ||
} | ||
``` | ||
Notice that we used the `Behaviors` derive macro from the `arbiter_macros` crate. | ||
This macro will generate an implementation of a `CreateStateMachine` trait for the `Behaviors` enum and ultimately save you from having to write a lot of boilerplate code. | ||
The macro solely requires that the `Behavior`s you have implement the `Behavior` trait and that the necessary imports are in scope. | ||
|
||
## Configuration File | ||
Now that you have your `enum` of `Behavior`s, you can configure your `World` and the `Agent`s inside of it from configuration file. | ||
Since the `World` and your simulation is completely defined by the `Agent` `Behavior`s you make, all you need to do is specify your `Agent`s in the configuration file. | ||
For example, let's say we have the `Replier` behavior from before, so we have: | ||
```rust | ||
#[derive(Behaviors)] | ||
pub enum Behaviors { | ||
Replier(Replier), | ||
} | ||
|
||
pub struct Replier { | ||
receive_data: String, | ||
send_data: String, | ||
max_count: u64, | ||
startup_message: Option<String>, | ||
count: u64, | ||
messager: Option<Messager>, | ||
} | ||
``` | ||
Then, we can specify the "ping" and "pong" `Behavior`s like this: | ||
```toml | ||
[[my_agent]] | ||
Replier = { send_data = "ping", receive_data = "pong", max_count = 5, startup_message = "ping" } | ||
|
||
[[my_agent]] | ||
Replier = { send_data = "pong", receive_data = "ping", max_count = 5 } | ||
``` | ||
If you instead wanted to specify two `Agent`s "Alice" and "Bob" each with one of the `Replier` `Behavior`s, you could do it like this: | ||
```toml | ||
[[alice]] | ||
Replier = { send_data = "ping", receive_data = "pong", max_count = 5, startup_message = "ping" } | ||
|
||
[[bob]] | ||
Replier = { send_data = "pong", receive_data = "ping", max_count = 5 } | ||
``` | ||
|
||
## Loading the Configuration | ||
Once you have your configuration file located at `./path/to/config.toml`, you can load it and run your simulation like this: | ||
```rust | ||
fn main() { | ||
let world = World::from_config("./path/to/config.toml")?; | ||
world.run().await; | ||
} | ||
``` | ||
At the moment, we do not configure `Universe`s from a configuration file, but this is a feature that is planned for the future. |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,27 @@ | ||
# Arbiter Engine | ||
`arbiter-engine` provides the machinery to build agent based / event driven simulations and should be the primary entrypoint for using Arbiter. | ||
The goal of this crate is to abstract away the work required to set up agents, their behaviors, and the worlds they live in. | ||
At the moment, all interaction of agents is done through the `arbiter-core` crate and is meant to be for local simulations and it is not yet generalized for the case of live network automation. | ||
|
||
## Heirarchy | ||
The primary components of `arbiter-engine` are, from the bottom up: | ||
- `Behavior<E>`: This is an event-driven behavior that takes in some item of type `E` and can act on that. | ||
The `Behavior<E>` has two methods: `startup` and `process`. | ||
- `startup` is meant to initialize the `Behavior<E>` and any context around it. | ||
An example could be an agent that deploys token contracts on startup. | ||
- `process` is meant to be a stage that runs on every event that comes in. | ||
An example could be an agent that deployed token contracts on startup, and now wants to process queries about the tokens deployed in the simulation (e.g., what their addresses are). | ||
- `Engine<B,E>` and `StateMachine`: The `Engine` is a struct that implements the `StateMachine` trait as an entrypoint to run `Behavior`s. | ||
- `Engine<B,E>` is a struct owns a `B: Behavior<E>` and the event stream `Stream<Item = E>` that the `Behavior<E>` will use for processing. | ||
- `StateMachine` is a trait that reduces the interface to `Engine<B,E>` to a single method: `execute`. | ||
This trait allows `Agent`s to have multiple behaviors that may not use the same event type. | ||
- `Agent` a struct that contains an ID, a client (`Arc<RevmMiddleware>`) that provides means to send calls and transactions to an Arbiter `Environment`, and a `Messager`. | ||
- `Messager` is a struct that owns a `Sender` and `Receiver` for sending and receiving messages. | ||
This is a way for `Agent`s to communicate with each other. | ||
It can also be streamed and used for processing messages in a `Behavior<Message>`. | ||
- `Agent` also owns a `Vec<Box<dyn StateMachine>>` which is a list of `StateMachine`s that the `Agent` will run. | ||
This is a way for `Agent`s to have multiple `Behavior`s that may not use the same event type. | ||
- `World` is a struct that has an ID, an Arbiter `Environment`, a mapping of `Agent`s, and a `Messager`. | ||
- The `World` is tasked with letting `Agent`s join in, and when they do so, to connect them to the `Environment` with a client and `Messager` with the `Agent`'s ID. | ||
- `Universe` is a struct that wraps a mapping of `World`s. | ||
- The `Universe` is tasked with letting `World`s join in and running those `World`s in parallel. |
Oops, something went wrong.