feat: rewrite #1
6 changed files with 446 additions and 213 deletions
109
Cargo.lock
generated
109
Cargo.lock
generated
|
|
@ -3,102 +3,53 @@
|
||||||
version = 4
|
version = 4
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "bytes"
|
name = "cfg-if"
|
||||||
version = "1.11.0"
|
version = "1.0.4"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "b35204fbdc0b3f4446b89fc1ac2cf84a8a68971995d0bf2e925ec7cd960f9cb3"
|
checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "futures-core"
|
name = "crossbeam-queue"
|
||||||
version = "0.3.31"
|
version = "0.3.12"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "05f29059c0c2090612e8d742178b0580d2dc940c837851ad723096f87af6663e"
|
checksum = "0f58bbc28f91df819d0aa2a2c00cd19754769c2fad90579b3592b1c9ba7a3115"
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "futures-sink"
|
|
||||||
version = "0.3.31"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "e575fab7d1e0dcb8d0c7bcf9a63ee213816ab51902e6d244a95819acacf1d4f7"
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "pin-project-lite"
|
|
||||||
version = "0.2.16"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "3b3cff922bd51709b605d9ead9aa71031d81447142d828eb4a6eba76fe619f9b"
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "proc-macro2"
|
|
||||||
version = "1.0.103"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "5ee95bc4ef87b8d5ba32e8b7714ccc834865276eab0aed5c9958d00ec45f49e8"
|
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"unicode-ident",
|
"crossbeam-utils",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "quote"
|
name = "crossbeam-utils"
|
||||||
version = "1.0.42"
|
version = "0.8.21"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "a338cc41d27e6cc6dce6cefc13a0729dfbb81c262b1f519331575dd80ef3067f"
|
checksum = "d0a5c400df2834b80a4c3327b3aad3a4c4cd4de0629063962b03235697506a28"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "getrandom"
|
||||||
|
version = "0.2.17"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "ff2abc00be7fca6ebc474524697ae276ad847ad0a6b3faa4bcb027e9a4614ad0"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"proc-macro2",
|
"cfg-if",
|
||||||
|
"libc",
|
||||||
|
"wasi",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "libc"
|
||||||
|
version = "0.2.180"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "bcc35a38544a891a5f7c865aca548a982ccb3b8650a5b06d0fd33a10283c56fc"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "swactor"
|
name = "swactor"
|
||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"tokio",
|
"crossbeam-queue",
|
||||||
"tokio-util",
|
"getrandom",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "syn"
|
name = "wasi"
|
||||||
version = "2.0.111"
|
version = "0.11.1+wasi-snapshot-preview1"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "390cc9a294ab71bdb1aa2e99d13be9c753cd2d7bd6560c77118597410c4d2e87"
|
checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b"
|
||||||
dependencies = [
|
|
||||||
"proc-macro2",
|
|
||||||
"quote",
|
|
||||||
"unicode-ident",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "tokio"
|
|
||||||
version = "1.48.0"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "ff360e02eab121e0bc37a2d3b4d4dc622e6eda3a8e5253d5435ecf5bd4c68408"
|
|
||||||
dependencies = [
|
|
||||||
"pin-project-lite",
|
|
||||||
"tokio-macros",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "tokio-macros"
|
|
||||||
version = "2.6.0"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "af407857209536a95c8e56f8231ef2c2e2aff839b22e07a1ffcbc617e9db9fa5"
|
|
||||||
dependencies = [
|
|
||||||
"proc-macro2",
|
|
||||||
"quote",
|
|
||||||
"syn",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "tokio-util"
|
|
||||||
version = "0.7.17"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "2efa149fe76073d6e8fd97ef4f4eca7b67f599660115591483572e406e165594"
|
|
||||||
dependencies = [
|
|
||||||
"bytes",
|
|
||||||
"futures-core",
|
|
||||||
"futures-sink",
|
|
||||||
"pin-project-lite",
|
|
||||||
"tokio",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "unicode-ident"
|
|
||||||
version = "1.0.22"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "9312f7c4f6ff9069b165498234ce8be658059c6728633667c526e27dc2cf1df5"
|
|
||||||
|
|
|
||||||
|
|
@ -6,6 +6,10 @@ edition = "2024"
|
||||||
[lib]
|
[lib]
|
||||||
crate-type = ["cdylib", "rlib"]
|
crate-type = ["cdylib", "rlib"]
|
||||||
|
|
||||||
|
[features]
|
||||||
|
default = ["getrandom"]
|
||||||
|
getrandom = ["dep:getrandom"]
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
tokio = { version = "1.48.0", features = ["rt", "macros"] }
|
getrandom = { version = "0.2", optional = true }
|
||||||
tokio-util = "0.7.17"
|
crossbeam-queue = "0.3.12"
|
||||||
|
|
|
||||||
77
DESIGN.md
Normal file
77
DESIGN.md
Normal file
|
|
@ -0,0 +1,77 @@
|
||||||
|
# Design goals
|
||||||
|
Get as much usability and speed as possible while keeping line count low. Aim for no footguns, ability to plug in
|
||||||
|
logic easily, and run near anywhere. We may make this a `![no_std]` library, but the MVP will use the
|
||||||
|
memory allocator and threading provided by the rust standard library.
|
||||||
|
|
||||||
|
We are not building a new erlang/BEAM. Minimal feature set means spawning actor processes, not having supervisiors, lots of process
|
||||||
|
monitoring tools, prempting, etc.
|
||||||
|
|
||||||
|
## Actor model
|
||||||
|
|
||||||
|
An actor has:
|
||||||
|
|
||||||
|
- An inbox:
|
||||||
|
this is a mpsc channel that the runtime/router dumps messages into and the actor consumes when the runtime loads it
|
||||||
|
Implemented as a barebones atomic ring buffer. The router is responsible for inserting messages.
|
||||||
|
|
||||||
|
- an outbox channel connection:
|
||||||
|
this is a mpmc channel that is implemented by the runtime and router. Actors on this specific channel put responses and outgoing messages into this channel, to be routed to the given address.
|
||||||
|
|
||||||
|
- a growable and mutable state:
|
||||||
|
An actor owns some, from the runtime perspective, type erased bytes. The actor when processing messages can access its own state, but no other task can. This includes viewing.
|
||||||
|
|
||||||
|
- a set of functions for processing messages:
|
||||||
|
When the runtime loads the actor, it locks the inbox and attempts to process the messages therein.
|
||||||
|
|
||||||
|
## Runtime
|
||||||
|
|
||||||
|
In order for an actor to consume and send messages, it is processed by a runtime. The runtime, in order to negotiate messages between
|
||||||
|
actors, possesses a router.
|
||||||
|
|
||||||
|
A runtime has:
|
||||||
|
- An actor processing thread(s):
|
||||||
|
the processor will mark an actor as busy, load its state and inbox, and begin consuming messages from the inbox. The number of messages consumed is determined by the runtime. A good start is a backpressure strategy: after loading, process messages until mailbox is empty or size drops below a threshold (e.g., "drain to 50%").
|
||||||
|
|
||||||
|
- A message router:
|
||||||
|
the router is responsible for ensuring messages posted by actors get delivered to the appropriate inbox.
|
||||||
|
|
||||||
|
- An atomic ring buffer containing thread-safe references to actors that are not currently loaded. Actors are popped off the buffer, messages are
|
||||||
|
processed, and the reference is returned to the buffer/queue before the next actor is loaded.
|
||||||
|
|
||||||
|
## Router
|
||||||
|
|
||||||
|
The router is the engine for message delivery. It posesses:
|
||||||
|
|
||||||
|
- An actor address book:
|
||||||
|
The address book maps actor ids to `Sender` references that can be used to deliver messages to the actor inbox.
|
||||||
|
|
||||||
|
- Its own inbox:
|
||||||
|
The router possesses its own mpsc queue where references to messages are stored. The router will process this queue by dereferencing and writing directly into the recipient's inbox buffer.
|
||||||
|
|
||||||
|
### Misc
|
||||||
|
|
||||||
|
A means of providing an emergency overflow without adding much more code complexity. The mutex means
|
||||||
|
this will not be `no_std` however.
|
||||||
|
|
||||||
|
```rust
|
||||||
|
struct HybridChannel<T> {
|
||||||
|
// Start with lock-free ring buffer
|
||||||
|
ring: AtomicRingBuffer<T>,
|
||||||
|
|
||||||
|
// When full, spill into a Mutex<VecDeque<T>>
|
||||||
|
overflow: parking_lot::Mutex<VecDeque<T>>,
|
||||||
|
|
||||||
|
// Track overflow frequency to resize ring proactively
|
||||||
|
overflow_count: AtomicUsize,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<T> HybridChannel<T> {
|
||||||
|
fn push(&self, value: T) {
|
||||||
|
if self.ring.push(value).is_err() {
|
||||||
|
self.overflow.lock().push_back(value);
|
||||||
|
self.overflow_count.fetch_add(1, Relaxed);
|
||||||
|
// Optionally: if overflow_count > threshold, grow ring
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
@ -1,46 +1,58 @@
|
||||||
use swactor::Actor;
|
use swactor::{ActorAddress, ActorInterface, Message, Runtime, RuntimeFlavor};
|
||||||
use tokio::sync::oneshot;
|
|
||||||
|
|
||||||
pub enum GreeterMessage {
|
#[derive(Debug, Default)]
|
||||||
Name(String),
|
struct Greeter {
|
||||||
|
pub num_greeted: usize,
|
||||||
}
|
}
|
||||||
|
|
||||||
pub enum GreeterResponse {
|
#[derive(Debug, Default, Clone)]
|
||||||
Hello(String),
|
struct GreetMessage {
|
||||||
|
/// who do we greet?
|
||||||
|
who: String,
|
||||||
|
|
||||||
|
/// who do we send out greeting back to?
|
||||||
|
return_addr: ActorAddress,
|
||||||
}
|
}
|
||||||
|
impl Message for GreetMessage {}
|
||||||
|
|
||||||
pub struct Greeter;
|
impl ActorInterface for Greeter {
|
||||||
|
type Incoming = GreetMessage;
|
||||||
|
type Response = GreetResponse;
|
||||||
|
|
||||||
impl Actor for Greeter {
|
fn handle(&mut self, ctx: &Runtime, msg: GreetMessage) {
|
||||||
type Message = GreeterMessage;
|
let res = GreetResponse(format!("Hello, {}!", msg.who));
|
||||||
type Response = GreeterResponse;
|
self.num_greeted += 1;
|
||||||
|
if let Err(_) = ctx.send_to(msg.return_addr, res) {
|
||||||
fn handle_message(&self, msg: Self::Message, tx: oneshot::Sender<Self::Response>) {
|
// no error handling
|
||||||
let rep = match msg {
|
self.num_greeted -= 1;
|
||||||
GreeterMessage::Name(name) => GreeterResponse::Hello(format!("Hello, {name}!")),
|
|
||||||
};
|
|
||||||
|
|
||||||
if let Err(_) = tx.send(rep) {
|
|
||||||
// Greeter is not responsible for a dropped Receiver
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Default, Clone)]
|
||||||
|
struct GreetResponse(String);
|
||||||
|
impl Message for GreetResponse {}
|
||||||
|
|
||||||
fn main() {
|
fn main() {
|
||||||
let rt = tokio::runtime::Builder::new_current_thread()
|
let mut rt = Runtime::new(100, Some(RuntimeFlavor::SingleThreaded));
|
||||||
.build()
|
let addr = rt
|
||||||
.expect("failed to build runtime");
|
.spawn(Greeter::default())
|
||||||
let greeter = Greeter.spawn(&rt);
|
.expect("failed to spawn greeter");
|
||||||
|
let inbox = rt.new_inbox::<GreetResponse>();
|
||||||
|
|
||||||
let response = rt
|
rt.send_to(
|
||||||
.block_on(async move {
|
addr,
|
||||||
greeter
|
GreetMessage {
|
||||||
.send(GreeterMessage::Name("world".to_string()))
|
who: "world".into(),
|
||||||
.await
|
return_addr: *inbox.addr(),
|
||||||
})
|
},
|
||||||
.expect("failed to get respose");
|
)
|
||||||
|
.unwrap();
|
||||||
match response {
|
for _ in 0..3 {
|
||||||
GreeterResponse::Hello(hello) => println!("{hello}"),
|
rt.tick();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let resp = inbox.try_recv().expect("greeter should have said hello");
|
||||||
|
|
||||||
|
println!("{}", resp.0);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
333
src/lib.rs
333
src/lib.rs
|
|
@ -1,118 +1,251 @@
|
||||||
|
mod ring_buffer;
|
||||||
|
|
||||||
|
use std::collections::HashMap;
|
||||||
|
|
||||||
|
use crossbeam_queue::ArrayQueue;
|
||||||
|
use ring_buffer::{Receiver, Sender};
|
||||||
|
|
||||||
pub mod error;
|
pub mod error;
|
||||||
|
use error::Error;
|
||||||
|
|
||||||
/// Public export as the oneshot channel is in the `Actor` trait signature
|
#[cfg(feature = "getrandom")]
|
||||||
pub use tokio::sync::oneshot;
|
pub fn get_random(buf: &mut [u8]) {
|
||||||
|
getrandom::getrandom(buf).unwrap()
|
||||||
use tokio::{sync::mpsc, task::JoinHandle};
|
|
||||||
use tokio_util::sync::CancellationToken;
|
|
||||||
|
|
||||||
use crate::error::{Result, convert_err};
|
|
||||||
|
|
||||||
const DEFAULT_CHANNEL_SIZE: usize = 100;
|
|
||||||
|
|
||||||
type ActorRequest<A> = (
|
|
||||||
<A as Actor>::Message,
|
|
||||||
oneshot::Sender<<A as Actor>::Response>,
|
|
||||||
);
|
|
||||||
|
|
||||||
/// Wrapper defining the transmission end of a Request/Response channel with an `Actor`
|
|
||||||
pub struct ActorRequestSender<A: Actor>(mpsc::Sender<ActorRequest<A>>);
|
|
||||||
|
|
||||||
impl<A: Actor> ActorRequestSender<A> {
|
|
||||||
pub async fn send(&self, request: A::Message) -> Result<A::Response> {
|
|
||||||
let (tx, rx) = oneshot::channel::<A::Response>();
|
|
||||||
self.0.send((request, tx)).await.map_err(convert_err)?;
|
|
||||||
|
|
||||||
rx.await.map_err(convert_err)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<A: Actor> From<mpsc::Sender<ActorRequest<A>>> for ActorRequestSender<A> {
|
/// The strategy for message processing is such:
|
||||||
fn from(value: mpsc::Sender<ActorRequest<A>>) -> Self {
|
/// if total_messages < WATERLEVEL:
|
||||||
Self(value)
|
/// process all
|
||||||
}
|
/// else
|
||||||
|
/// process total_messages // 2
|
||||||
|
const WATERLEVEL: usize = 10;
|
||||||
|
|
||||||
|
const DEFAULT_INBOX_CAPACITY: usize = 100;
|
||||||
|
|
||||||
|
pub trait Message: 'static + Sized + Clone + Send {}
|
||||||
|
pub type Envelope = Box<dyn std::any::Any + Send>;
|
||||||
|
|
||||||
|
pub trait ActorInterface: 'static + Send {
|
||||||
|
type Incoming: Message;
|
||||||
|
type Response: Message;
|
||||||
|
fn handle(&mut self, ctx: &Runtime, msg: Self::Incoming);
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<A: Actor> Clone for ActorRequestSender<A> {
|
pub type ActorAddress = u64;
|
||||||
fn clone(&self) -> Self {
|
|
||||||
Self(self.0.clone())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Combined 'JoinHandle' to await the actor process and 'Sender' for communication
|
pub struct Actor<A>
|
||||||
pub struct Handle<A>
|
|
||||||
where
|
where
|
||||||
A: Actor,
|
A: ActorInterface,
|
||||||
{
|
{
|
||||||
cancel_token: CancellationToken,
|
_addr: ActorAddress,
|
||||||
tx: ActorRequestSender<A>,
|
inbox: Receiver<A::Incoming>,
|
||||||
/// the task drops when the `JoinHandle` does, so be careful with the `Handle`
|
inner: A,
|
||||||
_handle: JoinHandle<Result<()>>,
|
|
||||||
// to prevent accidental swaps, strongly type the handle
|
|
||||||
_type: std::marker::PhantomData<A>,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<A: Actor> Handle<A> {
|
/// Trait for type-erased actors
|
||||||
/// Send a message to the spawned `Actor` task and get a response corresponding to the `Actor::Response` type
|
trait AnyActor: Send {
|
||||||
pub async fn send(&self, msg: A::Message) -> Result<A::Response> {
|
fn tick(&mut self, ctx: &Runtime);
|
||||||
self.tx.send(msg).await
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Get a cloned sender for messaging the `Actor` this handle is for
|
|
||||||
pub fn get_connection(&self) -> ActorRequestSender<A> {
|
|
||||||
self.tx.clone()
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<A: Actor> Drop for Handle<A> {
|
impl<A> AnyActor for Actor<A>
|
||||||
fn drop(&mut self) {
|
where
|
||||||
self.cancel_token.cancel();
|
A: ActorInterface,
|
||||||
}
|
{
|
||||||
}
|
fn tick(&mut self, ctx: &Runtime) {
|
||||||
|
let total_messages = self.inbox.len();
|
||||||
|
let messages_to_process = if total_messages < WATERLEVEL {
|
||||||
|
total_messages
|
||||||
|
} else {
|
||||||
|
total_messages >> 1
|
||||||
|
};
|
||||||
|
|
||||||
/// Primary trait defining an `Actor` capable of receiving, processing, and transmitting messages
|
for _ in 0..messages_to_process {
|
||||||
pub trait Actor: Send + Sized + 'static {
|
match self.inbox.try_recv() {
|
||||||
/// The type for messages received by this `Actor`
|
Some(msg) => self.inner.handle(ctx, msg),
|
||||||
type Message: Send;
|
None => unreachable!(
|
||||||
/// The type for responses given by this actor when called from `Handle::send(..)`
|
"We checked number of unprocessed messages in the queue ahead of processing"
|
||||||
type Response: Send;
|
),
|
||||||
|
}
|
||||||
/// Inner method that defines actor behavior
|
}
|
||||||
fn handle_message(&self, msg: Self::Message, tx: oneshot::Sender<Self::Response>);
|
}
|
||||||
|
}
|
||||||
/// Spawns the `Actor` utilizing the given runtime context
|
|
||||||
/// Only `tokio` runtime is accepted for now
|
pub struct Inbox<M: Message> {
|
||||||
fn spawn(self, ctx: &tokio::runtime::Runtime) -> Handle<Self> {
|
addr: ActorAddress,
|
||||||
let cancel_token = CancellationToken::new();
|
inner: Receiver<M>,
|
||||||
let cancel = cancel_token.clone();
|
}
|
||||||
|
|
||||||
let (tx, mut rx) =
|
impl<M: Message> Inbox<M> {
|
||||||
mpsc::channel::<(Self::Message, oneshot::Sender<Self::Response>)>(DEFAULT_CHANNEL_SIZE);
|
pub fn addr(&self) -> &ActorAddress {
|
||||||
let handle = ctx.spawn(async move {
|
&self.addr
|
||||||
let mut res = Ok(());
|
}
|
||||||
loop {
|
|
||||||
tokio::select! {
|
pub fn try_recv(&self) -> Option<M> {
|
||||||
_ = cancel.cancelled() => {
|
self.inner.try_recv()
|
||||||
break;
|
}
|
||||||
},
|
}
|
||||||
|
|
||||||
msg = rx.recv() => {
|
#[derive(Debug, Default)]
|
||||||
match msg {
|
pub enum RuntimeFlavor {
|
||||||
Some(m) => { self.handle_message(m.0, m.1); },
|
#[default]
|
||||||
None => {res = Err(format!("Sender handle was dropped without calling cancel!").into()); break; },
|
SingleThreaded,
|
||||||
}
|
Multithreaded(usize),
|
||||||
}
|
}
|
||||||
};
|
|
||||||
|
pub struct Runtime {
|
||||||
|
flavor: RuntimeFlavor,
|
||||||
|
router: Router,
|
||||||
|
router_inbox: Sender<RouterMessage>,
|
||||||
|
actor_queue: ArrayQueue<Box<dyn AnyActor>>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Runtime {
|
||||||
|
pub fn new(capacity: usize, flavor: Option<RuntimeFlavor>) -> Self {
|
||||||
|
let router = Router::new(DEFAULT_INBOX_CAPACITY);
|
||||||
|
let router_inbox = router.new_sender();
|
||||||
|
Self {
|
||||||
|
flavor: flavor.unwrap_or_default(),
|
||||||
|
router,
|
||||||
|
router_inbox,
|
||||||
|
actor_queue: ArrayQueue::new(capacity),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn spawn<A: ActorInterface>(&self, actor: A) -> Result<ActorAddress, Error> {
|
||||||
|
let addr = {
|
||||||
|
let mut bytes = u64::to_le_bytes(0);
|
||||||
|
get_random(&mut bytes);
|
||||||
|
u64::from_le_bytes(bytes)
|
||||||
|
};
|
||||||
|
let inbox = Receiver::<A::Incoming>::new(DEFAULT_INBOX_CAPACITY);
|
||||||
|
let sender = inbox.new_sender();
|
||||||
|
|
||||||
|
// Register the sender with the router
|
||||||
|
let _ = self
|
||||||
|
.router_inbox
|
||||||
|
.try_send(RouterMessage::AddAddr(addr, Box::new(sender)));
|
||||||
|
|
||||||
|
self.actor_queue
|
||||||
|
.push(Box::new(Actor {
|
||||||
|
_addr: addr,
|
||||||
|
inbox,
|
||||||
|
inner: actor,
|
||||||
|
}))
|
||||||
|
.map_err(|_| Error::from("Runtime error: Failed to spawn actor."))?;
|
||||||
|
|
||||||
|
Ok(addr)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn send_to<M: Message>(&self, addr: ActorAddress, msg: M) -> Result<(), ()> {
|
||||||
|
let envelope: Envelope = Box::new(msg);
|
||||||
|
self.router_inbox
|
||||||
|
.try_send(RouterMessage::SendToAddr {
|
||||||
|
addr,
|
||||||
|
msg: envelope,
|
||||||
|
})
|
||||||
|
.map_err(|_| ())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn tick(&mut self) {
|
||||||
|
// Pop actor, tick it, push it back
|
||||||
|
if let Some(mut actor) = self.actor_queue.pop() {
|
||||||
|
actor.tick(self);
|
||||||
|
let _ = self.actor_queue.push(actor);
|
||||||
|
}
|
||||||
|
|
||||||
|
match self.flavor {
|
||||||
|
RuntimeFlavor::Multithreaded(_) => (), // router has its own thread
|
||||||
|
RuntimeFlavor::SingleThreaded => self.router.tick(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn new_inbox<M: Message>(&self) -> Inbox<M> {
|
||||||
|
let addr = {
|
||||||
|
let mut bytes = u64::to_le_bytes(0);
|
||||||
|
get_random(&mut bytes);
|
||||||
|
u64::from_le_bytes(bytes)
|
||||||
|
};
|
||||||
|
let receiver = Receiver::<M>::new(DEFAULT_INBOX_CAPACITY);
|
||||||
|
let sender = receiver.new_sender();
|
||||||
|
// Register the sender with the router
|
||||||
|
let _ = self
|
||||||
|
.router_inbox
|
||||||
|
.try_send(RouterMessage::AddAddr(addr, Box::new(sender)));
|
||||||
|
Inbox {
|
||||||
|
addr,
|
||||||
|
inner: receiver,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub trait SenderT: Send {
|
||||||
|
fn try_send(&self, envelope: Envelope);
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<M: Message> SenderT for Sender<M> {
|
||||||
|
fn try_send(&self, envelope: Envelope) {
|
||||||
|
if let Ok(msg) = envelope.downcast::<M>() {
|
||||||
|
let _ = Sender::try_send(self, *msg);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Internal messages for the Router's own inbox
|
||||||
|
pub enum RouterMessage {
|
||||||
|
/// register addrs <addr> with sender <sender>
|
||||||
|
AddAddr(ActorAddress, Box<dyn SenderT>),
|
||||||
|
/// remove an actor from the address book
|
||||||
|
RemoveAddr(ActorAddress),
|
||||||
|
/// send <msg> to <addr>
|
||||||
|
SendToAddr { addr: ActorAddress, msg: Envelope },
|
||||||
|
}
|
||||||
|
|
||||||
|
struct Router {
|
||||||
|
directory: HashMap<ActorAddress, Box<dyn SenderT>>,
|
||||||
|
inbox: Receiver<RouterMessage>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Router {
|
||||||
|
pub fn new(cap: usize) -> Self {
|
||||||
|
Self {
|
||||||
|
directory: HashMap::new(),
|
||||||
|
inbox: Receiver::new(cap),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn tick(&mut self) {
|
||||||
|
let total_messages = self.inbox.len();
|
||||||
|
let messages_to_process = if total_messages < WATERLEVEL {
|
||||||
|
total_messages
|
||||||
|
} else {
|
||||||
|
total_messages >> 1
|
||||||
|
};
|
||||||
|
|
||||||
|
for _ in 0..messages_to_process {
|
||||||
|
match self.inbox.try_recv() {
|
||||||
|
Some(msg) => self.handle(msg),
|
||||||
|
None => unreachable!("We ran checks on total messages before processing."),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn new_sender(&self) -> Sender<RouterMessage> {
|
||||||
|
self.inbox.new_sender()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn handle(&mut self, msg: RouterMessage) {
|
||||||
|
match msg {
|
||||||
|
RouterMessage::AddAddr(addr, sender) => {
|
||||||
|
self.directory.insert(addr, sender);
|
||||||
|
}
|
||||||
|
RouterMessage::RemoveAddr(addr) => {
|
||||||
|
self.directory.remove(&addr);
|
||||||
|
}
|
||||||
|
RouterMessage::SendToAddr { addr, msg } => {
|
||||||
|
if let Some(sender) = self.directory.get(&addr) {
|
||||||
|
sender.try_send(msg);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
res
|
|
||||||
});
|
|
||||||
|
|
||||||
Handle {
|
|
||||||
cancel_token,
|
|
||||||
_handle: handle,
|
|
||||||
tx: tx.into(),
|
|
||||||
_type: std::marker::PhantomData::<Self>,
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
56
src/ring_buffer.rs
Normal file
56
src/ring_buffer.rs
Normal file
|
|
@ -0,0 +1,56 @@
|
||||||
|
pub use crossbeam_queue::ArrayQueue;
|
||||||
|
use std::sync::Arc;
|
||||||
|
|
||||||
|
/// The receiving end of a `crossbeam_queue::ArrayQueue`, a lock-free mpsc queue.
|
||||||
|
/// The queue is constructed by the `Receiver::new()` method.
|
||||||
|
/// Responsible for creating the `Sender` ends of itself.
|
||||||
|
///
|
||||||
|
/// Notably: The `Receiver` provides no guarentees that a sending end of the channel exists.
|
||||||
|
pub(crate) struct Receiver<T> {
|
||||||
|
queue: Arc<ArrayQueue<T>>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<T> Receiver<T> {
|
||||||
|
/// Constructs a new `ArrayQueue` with given capacity.
|
||||||
|
///
|
||||||
|
/// # Panics
|
||||||
|
/// Will panic if capacity is passed as 0
|
||||||
|
pub fn new(capacity: usize) -> Self {
|
||||||
|
Self {
|
||||||
|
queue: Arc::new(ArrayQueue::new(capacity)),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns the number of elements in the inner queue
|
||||||
|
pub fn len(&self) -> usize {
|
||||||
|
self.queue.len()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Attempt to retrieve a value from the queue. Returns `None` if empty
|
||||||
|
pub fn try_recv(&self) -> Option<T> {
|
||||||
|
self.queue.pop()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Construct a new `Sender` assosciated with this queue.
|
||||||
|
pub fn new_sender(&self) -> Sender<T> {
|
||||||
|
Sender {
|
||||||
|
queue: self.queue.clone(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The sending end of a `crossbeam_queue::ArrayQueue`, a lock free mpsc queue.
|
||||||
|
/// The queue is initialized via calling the corresponding `Receiver::<T>::new()` method,
|
||||||
|
/// and the sending end of the queue is constructed via calling `receiver.new_sender()`.
|
||||||
|
///
|
||||||
|
/// Notably: The `Sender` provides no guarentees that a receiving end of the channel exists.
|
||||||
|
pub(crate) struct Sender<T> {
|
||||||
|
queue: Arc<ArrayQueue<T>>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<T> Sender<T> {
|
||||||
|
/// Attempt to push a value to the queue. Returns Err(value) if the queue is full.
|
||||||
|
pub fn try_send(&self, value: T) -> Result<(), T> {
|
||||||
|
self.queue.push(value)
|
||||||
|
}
|
||||||
|
}
|
||||||
Loading…
Reference in a new issue