Tiny-actor is a minimal actor framework for Rust. Because it tries to stay as minimal as possible, it can be used both in libraries, as well as in applications.
The core priniciple of tiny-actor is merging inboxes with processes. It's impossible to create an inbox without a process, this allows for building simple pools and supervision-trees, with reliable shutdown behaviour.
A channel is that which underlies the coupling of inboxes, addresses and children. A channel contains:
* One Child
or ChildPool
* Zero or more Address
es
* Zero or more Inbox
es
An Inbox
refers is the receiver-part of a Channel
, this is always coupled with a tokio::task
. The Inbox
is
primarily used to take messages out of the Channel
.
Inbox
es can only be created by spawning new processes and should stay coupled to the tokio::task
they were
spawned with. Therefore, an Inbox
should only be dropped when the tokio::task
is exiting.
An Address
is the cloneable sender-part of a Channel
. The Address
is primarily used to send messages to
Inbox
es.
When all Address
es are dropped, the Channel
is closed automatically.
Address
es can be awaited, which will return when all Inbox
es linked to the Channel
have exited.
A Child
is a handle to a Channel
with a one Inbox
. The Child
can be awaited to return the exit-value
of the tokio::task
that has been spawned. A Child
is non-cloneable, and therefore unique to the Channel
.
When the Child
is dropped, the attached process will be aborted. This can be prevented by detaching
the Child
. More processes can be spawned later, which transforms the Child
into a ChildPool
.
A ChildPool
is similar to a Child
, except that the Channel
can have more than one Inbox
.
A ChildPool
can be streamed to get the exit-values of all spawned tokio::task
s.
When a Channel
is closed, it is not longer possible to send new messages into it. It is still possible to
take out any messages that are remaining.
A channel that is closed does not have to exit afterwards.
Any senders are notified with a SendError::Closed
. Receivers will receive RecvError::ClosedAndEmpty
once
the channel has been emptied.
An Inbox
can be halted exactly once. When receiving a RecvError::Halted
the process should exit.
A Channel
can be partially halted, meaning that only some of the Inbox
es have been halted.
A process can be aborted through tokio's abort method.This causes the process to exit abruptly, and can leave bad state behind. Wherever possible, use halt instead
of abort.
By default, a spawned process is automatically aborted when the Child
is dropped. This can be prevented by
detaching a Child
.
Exit can refer to two seperate events which, with good practise, always occur at the same time:
* An Inbox
can exit by being dropped. Once all Inbox
es of a Channel
have been dropped, the Channel
itself
has exited. This type of exit can be retrieved/awaited from the Channel
at any time.
* A tokio::task
can exit, which means the process is no longer alive. This can only be queried only once, by
awaiting the Child
or ChildPool
.
Therefore, it is recommended to drop an Inbox
only when the process itself is also exiting. This way, an exit
always refers to the same event.
A Child
or ChildPool
has an abort-timer. If the Child
or ChildPool
is attached, then it will instantly
send a Halt
-signal to all inboxes. Then, after the abort-timer, if the child still has not exited, it will be
aborted.
```rust use tiny_actor::*;
async fn main() {
let (child, address) = spawn(Config::unbounded(), |mut inbox: Inbox
address.send(10).await.unwrap();
address.send(5).await.unwrap();
child.halt();
match child.await {
Ok(exit) => {
assert_eq!(exit, "Halt");
println!("Actor exited with message: {exit}")
},
Err(error) => match error {
JoinError::Panic(_) => todo!(),
JoinError::Abort => todo!(),
},
}
} ```
```rust use tiny_actor::*; use std::time::Duration; use futures::stream::StreamExt;
async fn main() {
let (pool, address) = spawnpooled(
0..3,
Config {
aborttimer: Duration::from_secs(1),
attached: true,
capacity: Some(5),
},
|i, mut inbox: Inbox
tokio::time::sleep(Duration::from_millis(10)).await;
for num in 0..20 {
address.send(num).await.unwrap()
}
pool.halt_all();
let exits: Vec<Result<&str, JoinError>> = pool.collect().await;
for exit in exits {
match exit {
Ok(exit) => {
assert_eq!(exit, "Halt");
println!("Actor exited with message: {exit}")
}
Err(error) => match error {
JoinError::Panic(_) => todo!(),
JoinError::Abort => todo!(),
},
}
}
} ```