init
Skeletal actor framework. Somewhat unweildy, needs a message box, a better runtime, and different channels. However, hello world example works Signed-off-by: Zachery Aaron Shores-Chmielewski <zacheryasc@gmail.com>
This commit is contained in:
commit
88121780ce
7 changed files with 260 additions and 0 deletions
1
.gitignore
vendored
Normal file
1
.gitignore
vendored
Normal file
|
|
@ -0,0 +1 @@
|
||||||
|
/target
|
||||||
104
Cargo.lock
generated
Normal file
104
Cargo.lock
generated
Normal file
|
|
@ -0,0 +1,104 @@
|
||||||
|
# This file is automatically @generated by Cargo.
|
||||||
|
# It is not intended for manual editing.
|
||||||
|
version = 4
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "bytes"
|
||||||
|
version = "1.11.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "b35204fbdc0b3f4446b89fc1ac2cf84a8a68971995d0bf2e925ec7cd960f9cb3"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "futures-core"
|
||||||
|
version = "0.3.31"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "05f29059c0c2090612e8d742178b0580d2dc940c837851ad723096f87af6663e"
|
||||||
|
|
||||||
|
[[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 = [
|
||||||
|
"unicode-ident",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "quote"
|
||||||
|
version = "1.0.42"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "a338cc41d27e6cc6dce6cefc13a0729dfbb81c262b1f519331575dd80ef3067f"
|
||||||
|
dependencies = [
|
||||||
|
"proc-macro2",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "swactor"
|
||||||
|
version = "0.1.0"
|
||||||
|
dependencies = [
|
||||||
|
"tokio",
|
||||||
|
"tokio-util",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "syn"
|
||||||
|
version = "2.0.111"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "390cc9a294ab71bdb1aa2e99d13be9c753cd2d7bd6560c77118597410c4d2e87"
|
||||||
|
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"
|
||||||
11
Cargo.toml
Normal file
11
Cargo.toml
Normal file
|
|
@ -0,0 +1,11 @@
|
||||||
|
[package]
|
||||||
|
name = "swactor"
|
||||||
|
version = "0.1.0"
|
||||||
|
edition = "2024"
|
||||||
|
|
||||||
|
[lib]
|
||||||
|
crate-type = ["cdylib", "rlib"]
|
||||||
|
|
||||||
|
[dependencies]
|
||||||
|
tokio = { version = "1.48.0", features = ["rt", "macros"] }
|
||||||
|
tokio-util = "0.7.17"
|
||||||
2
README.md
Normal file
2
README.md
Normal file
|
|
@ -0,0 +1,2 @@
|
||||||
|
# about
|
||||||
|
Small wasm-compatible actor library
|
||||||
46
examples/hello.rs
Normal file
46
examples/hello.rs
Normal file
|
|
@ -0,0 +1,46 @@
|
||||||
|
use swactor::Actor;
|
||||||
|
use tokio::sync::oneshot;
|
||||||
|
|
||||||
|
pub enum GreeterMessage {
|
||||||
|
Name(String),
|
||||||
|
}
|
||||||
|
|
||||||
|
pub enum GreeterResponse {
|
||||||
|
Hello(String),
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct Greeter;
|
||||||
|
|
||||||
|
impl Actor for Greeter {
|
||||||
|
type Message = GreeterMessage;
|
||||||
|
type Response = GreeterResponse;
|
||||||
|
|
||||||
|
fn handle_message(&self, msg: Self::Message, tx: oneshot::Sender<Self::Response>) {
|
||||||
|
let rep = match msg {
|
||||||
|
GreeterMessage::Name(name) => GreeterResponse::Hello(format!("Hello, {name}!")),
|
||||||
|
};
|
||||||
|
|
||||||
|
if let Err(_) = tx.send(rep) {
|
||||||
|
// Greeter is not responsible for a dropped Receiver
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn main() {
|
||||||
|
let rt = tokio::runtime::Builder::new_current_thread()
|
||||||
|
.build()
|
||||||
|
.expect("failed to build runtime");
|
||||||
|
let greeter = Greeter::spawn(Greeter, &rt);
|
||||||
|
|
||||||
|
let response = rt
|
||||||
|
.block_on(async move {
|
||||||
|
greeter
|
||||||
|
.send(GreeterMessage::Name("world".to_string()))
|
||||||
|
.await
|
||||||
|
})
|
||||||
|
.expect("failed to get respose");
|
||||||
|
|
||||||
|
match response {
|
||||||
|
GreeterResponse::Hello(hello) => println!("{hello}"),
|
||||||
|
}
|
||||||
|
}
|
||||||
5
src/error.rs
Normal file
5
src/error.rs
Normal file
|
|
@ -0,0 +1,5 @@
|
||||||
|
pub type Error = Box<dyn std::error::Error + Send + Sync + 'static>;
|
||||||
|
pub type Result<T> = std::result::Result<T, Error>;
|
||||||
|
pub fn convert_err<E: std::fmt::Debug>(e: E) -> Error {
|
||||||
|
format!("{e:?}").into()
|
||||||
|
}
|
||||||
91
src/lib.rs
Normal file
91
src/lib.rs
Normal file
|
|
@ -0,0 +1,91 @@
|
||||||
|
pub mod error;
|
||||||
|
|
||||||
|
/// Public export as the oneshot channel is in the `Actor` trait signature
|
||||||
|
pub use tokio::sync::oneshot;
|
||||||
|
|
||||||
|
use tokio::{
|
||||||
|
sync::{mpsc},
|
||||||
|
task::JoinHandle,
|
||||||
|
};
|
||||||
|
use tokio_util::sync::CancellationToken;
|
||||||
|
|
||||||
|
use crate::error::{Result, convert_err};
|
||||||
|
|
||||||
|
|
||||||
|
const DEFAULT_CHANNEL_SIZE: usize = 100;
|
||||||
|
|
||||||
|
/// Combined 'JoinHandle' to await the actor process and 'Sender' for communication
|
||||||
|
pub struct Handle<A>
|
||||||
|
where
|
||||||
|
A: Actor,
|
||||||
|
{
|
||||||
|
cancel_token: CancellationToken,
|
||||||
|
tx: mpsc::Sender<(A::Message, oneshot::Sender<A::Response>)>,
|
||||||
|
_handle: JoinHandle<Result<()>>,
|
||||||
|
// to prevent accidental swaps, strongly type the handle
|
||||||
|
_type: std::marker::PhantomData<A>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<A: Actor> Handle<A> {
|
||||||
|
|
||||||
|
/// Send a message to the spawned `Actor` task and get a response corresponding to the `Actor::Response` type
|
||||||
|
pub async fn send(&self, msg: A::Message) -> Result<A::Response> {
|
||||||
|
let (tx, rx) = oneshot::channel::<A::Response>();
|
||||||
|
self.tx.send((msg, tx)).await.map_err(convert_err)?;
|
||||||
|
|
||||||
|
rx.await.map_err(convert_err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<A: Actor> Drop for Handle<A> {
|
||||||
|
fn drop(&mut self) {
|
||||||
|
self.cancel_token.cancel();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Primary trait defining an `Actor` capable of receiving, processing, and transmitting messages
|
||||||
|
pub trait Actor: Send + Sized + Unpin + 'static {
|
||||||
|
/// The type for messages received by this `Actor`
|
||||||
|
type Message: Send;
|
||||||
|
/// The type for responses given by this actor when called from `Handle::send(..)`
|
||||||
|
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
|
||||||
|
fn spawn(self, ctx: &tokio::runtime::Runtime) -> Handle<Self> {
|
||||||
|
let cancel_token = CancellationToken::new();
|
||||||
|
let cancel = cancel_token.clone();
|
||||||
|
|
||||||
|
let (tx, mut rx) =
|
||||||
|
mpsc::channel::<(Self::Message, oneshot::Sender<Self::Response>)>(DEFAULT_CHANNEL_SIZE);
|
||||||
|
let handle = ctx.spawn(async move {
|
||||||
|
let mut res = Ok(());
|
||||||
|
loop {
|
||||||
|
tokio::select! {
|
||||||
|
_ = cancel.cancelled() => {
|
||||||
|
break;
|
||||||
|
},
|
||||||
|
|
||||||
|
msg = rx.recv() => {
|
||||||
|
match msg {
|
||||||
|
Some(m) => { self.handle_message(m.0, m.1); },
|
||||||
|
None => {res = Err(format!("Sender handle was dropped without calling cancel!").into()); break; },
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
res
|
||||||
|
});
|
||||||
|
|
||||||
|
Handle {
|
||||||
|
cancel_token,
|
||||||
|
_handle: handle,
|
||||||
|
tx,
|
||||||
|
_type: std::marker::PhantomData::<Self>,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
Loading…
Reference in a new issue