From 41e4b62f4ea3f5dca49d667f791ce3ae672bbb55 Mon Sep 17 00:00:00 2001 From: Esteban Dimitroff Hodi Date: Wed, 4 Feb 2026 12:26:45 -0300 Subject: [PATCH 1/6] docs: add API redesign plan for v0.5 --- docs/API_REDESIGN.md | 431 +++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 431 insertions(+) create mode 100644 docs/API_REDESIGN.md diff --git a/docs/API_REDESIGN.md b/docs/API_REDESIGN.md new file mode 100644 index 0000000..91ca23f --- /dev/null +++ b/docs/API_REDESIGN.md @@ -0,0 +1,431 @@ +# Plan: API Redesign for v0.5 - Issues #144, #145, and Phase 3 + +## Decisions Made + +| Issue | Decision | Rationale | +|-------|----------|-----------| +| **#145** (Circular deps) | Recipient\ pattern | Type-safe, no exposed Pid | +| **#144** (Type safety) | Per-message types (Handler\) | Leverage Rust's type system, clean API | +| **Breaking changes** | Accepted | v0.5 is the right time for API improvements | +| **Pattern support** | Per-message only | Clean break, one way to do things | +| **Actor identity** | Internal ID (hidden) | Links/monitors work via traits, no public Pid | +| **Supervision** | Trait-based (`Supervisable`) | Type-safe child management | + +## Overview + +This is a significant API redesign that: +1. Adds Handler pattern for per-message type safety (#144) +2. Adds Recipient for type-erased message sending (#145) +3. Uses internal identity (not exposed as Pid) for links/monitors +4. Uses traits for supervision (Supervisable, Linkable) + +--- + +## Issue #145: Circular Dependency with Bidirectional Actors + +### The Problem + +```rust +// actor_a.rs +use crate::actor_b::ActorB; +struct ActorA { peer: ActorRef } // Needs ActorB type + +// actor_b.rs +use crate::actor_a::ActorA; +struct ActorB { peer: ActorRef } // CIRCULAR! +``` + +### Solution: Recipient\ + +```rust +trait MessageSender: Send + Sync { + fn send(&self, msg: M) -> Result<(), ActorError>; +} + +// ActorRef implements MessageSender +// Type-erased handle for specific message type +type Recipient = Arc>; + +// Usage - no circular dependency! +struct ActorA { peer: Recipient } +struct ActorB { peer: Recipient } +``` + +--- + +## Issue #144: Type Safety for Request/Reply + +### The Problem + +```rust +enum Reply { Name(String), Age(u32), NotFound } + +// GetName can only return Name or NotFound, but must match Age too +match actor.request(Request::GetName).await? { + Reply::Name(n) => println!("{}", n), + Reply::NotFound => println!("not found"), + Reply::Age(_) => unreachable!(), // Required but impossible +} +``` + +### Solution: Per-Message Types with Handler\ + +```rust +struct GetName(String); +impl Message for GetName { + type Result = Option; +} + +impl Handler for NameServer { + fn handle(&mut self, msg: GetName) -> Option { ... } +} + +// Clean caller code - exact type! +let name: Option = actor.request(GetName("joe")).await?; +``` + +--- + +# Implementation Plan + +## Phase 3.1: Recipient (Type-Erased Sender) + +**New file:** `concurrency/src/recipient.rs` + +```rust +/// Trait for sending a specific message type +pub trait MessageSender: Send + Sync { + fn send(&self, msg: M) -> Result<(), ActorError>; + fn request(&self, msg: M) -> impl Future> + Send; +} + +/// Type-erased handle that can send messages of type M +pub type Recipient = Arc>; + +// ActorRef implements MessageSender for all M where A: Handler +impl MessageSender for ActorRef +where + A: Actor + Handler, + M: Message, +{ + fn send(&self, msg: M) -> Result<(), ActorError> { ... } + fn request(&self, msg: M) -> impl Future> + Send { ... } +} + +// Convert ActorRef to Recipient +impl ActorRef { + pub fn recipient(&self) -> Recipient + where + A: Handler, + M: Message, + { + Arc::new(self.clone()) + } +} +``` + +## Phase 3.2: Internal Identity (Hidden) + +**New file:** `concurrency/src/identity.rs` + +```rust +/// Internal process identifier (not public API) +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] +pub(crate) struct ActorId(u64); + +/// Exit reason for actors +pub enum ExitReason { + Normal, + Shutdown, + Error(String), + Linked, // Linked actor died +} +``` + +## Phase 3.3: Traits for Supervision and Links + +**New file:** `concurrency/src/traits.rs` + +```rust +/// Trait for actors that can be supervised +pub trait Supervisable: Send + Sync { + fn stop(&self); + fn is_alive(&self) -> bool; + fn on_exit(&self, callback: Box); +} + +/// Trait for actors that can be linked +pub trait Linkable: Supervisable { + fn link(&self, other: &dyn Linkable); + fn unlink(&self, other: &dyn Linkable); +} + +/// Trait for actors that can be monitored +pub trait Monitorable: Supervisable { + fn monitor(&self) -> MonitorRef; + fn demonitor(&self, monitor_ref: MonitorRef); +} + +// ActorRef implements all these traits +impl Supervisable for ActorRef { ... } +impl Linkable for ActorRef { ... } +impl Monitorable for ActorRef { ... } +``` + +## Phase 3.4: Registry (Named Actors) + +**New file:** `concurrency/src/registry.rs` + +```rust +/// Register a recipient under a name +pub fn register(name: &str, recipient: Recipient) -> Result<(), RegistryError>; + +/// Look up a recipient by name (must know message type) +pub fn whereis(name: &str) -> Option>; + +/// Unregister a name +pub fn unregister(name: &str); + +/// List all registered names +pub fn registered() -> Vec; +``` + +## Phase 4: Handler Pattern (#144) + +**Redesigned Actor API:** + +```rust +/// Marker trait for messages +pub trait Message: Send + 'static { + type Result: Send; +} + +/// Handler for a specific message type +pub trait Handler: Actor { + fn handle(&mut self, msg: M, ctx: &Context) -> impl Future + Send; +} + +/// Actor context (replaces ActorRef in handlers) +pub struct Context { + // ... internal fields +} + +impl Context { + pub fn stop(&self); + pub fn recipient(&self) -> Recipient where A: Handler; +} + +/// Base actor trait (simplified) +pub trait Actor: Send + Sized + 'static { + fn started(&mut self, ctx: &Context) -> impl Future + Send { async {} } + fn stopped(&mut self, ctx: &Context) -> impl Future + Send { async {} } +} +``` + +**ActorRef changes:** + +```rust +/// Typed handle to an actor +pub struct ActorRef { + id: ActorId, // Internal identity (not public) + sender: mpsc::Sender>, + _marker: PhantomData, +} + +impl ActorRef +where + A: Actor + Handler, + M: Message, +{ + /// Fire-and-forget send (returns error if actor dead) + pub fn send(&self, msg: M) -> Result<(), ActorError>; + + /// Send and wait for typed response + pub async fn request(&self, msg: M) -> Result; + + /// Get type-erased Recipient for this message type + pub fn recipient(&self) -> Recipient; +} + +// Implements supervision/linking traits +impl Supervisable for ActorRef { ... } +impl Linkable for ActorRef { ... } +impl Monitorable for ActorRef { ... } +``` + +--- + +## Example: Bank Actor (New API) + +```rust +// messages.rs +pub struct CreateAccount { pub name: String } +pub struct Deposit { pub account: String, pub amount: u64 } +pub struct GetBalance { pub account: String } + +impl Message for CreateAccount { type Result = Result<(), BankError>; } +impl Message for Deposit { type Result = Result; } +impl Message for GetBalance { type Result = Result; } + +// bank.rs +pub struct Bank { + accounts: HashMap, +} + +impl Actor for Bank { + async fn started(&mut self, ctx: &Context) { + tracing::info!("Bank started"); + } +} + +impl Handler for Bank { + async fn handle(&mut self, msg: CreateAccount, _ctx: &Context) -> Result<(), BankError> { + self.accounts.insert(msg.name, 0); + Ok(()) + } +} + +impl Handler for Bank { + async fn handle(&mut self, msg: Deposit, _ctx: &Context) -> Result { + let balance = self.accounts.get_mut(&msg.account).ok_or(BankError::NotFound)?; + *balance += msg.amount; + Ok(*balance) + } +} + +impl Handler for Bank { + async fn handle(&mut self, msg: GetBalance, _ctx: &Context) -> Result { + self.accounts.get(&msg.account).copied().ok_or(BankError::NotFound) + } +} + +// main.rs +let bank: ActorRef = Bank::new().start(); + +// Type-safe request (wait for reply) +let balance: Result = bank.request(GetBalance { account: "alice".into() }).await?; + +// Fire-and-forget send +bank.send(Deposit { account: "alice".into(), amount: 50 })?; + +// Get type-erased Recipient for storage/passing to other actors +let recipient: Recipient = bank.recipient(); + +// Supervision uses trait objects +let children: Vec> = vec![ + Arc::new(bank.clone()), + Arc::new(logger.clone()), +]; +``` + +--- + +## Example: Solving #145 (Circular Deps) with Recipient + +```rust +// shared_messages.rs - NO circular dependency +pub struct OrderUpdate { pub order_id: u64, pub status: String } +pub struct InventoryReserve { pub item: String, pub quantity: u32, pub reply_to: Recipient } + +impl Message for OrderUpdate { type Result = (); } +impl Message for InventoryReserve { type Result = Result<(), InventoryError>; } + +// order_service.rs - imports InventoryReserve, NOT InventoryService +use crate::shared_messages::{OrderUpdate, InventoryReserve}; + +pub struct OrderService { + inventory: Recipient, // Type-erased, no circular dep! +} + +impl Handler for OrderService { + async fn handle(&mut self, msg: PlaceOrder, ctx: &Context) -> Result<(), OrderError> { + let reply_to: Recipient = ctx.recipient(); + self.inventory.request(InventoryReserve { + item: msg.item, + quantity: msg.quantity, + reply_to, + }).await?; + Ok(()) + } +} + +// inventory_service.rs - imports OrderUpdate, NOT OrderService +use crate::shared_messages::{OrderUpdate, InventoryReserve}; + +pub struct InventoryService { ... } + +impl Handler for InventoryService { + async fn handle(&mut self, msg: InventoryReserve, _ctx: &Context) -> Result<(), InventoryError> { + // Reserve inventory... + msg.reply_to.send(OrderUpdate { order_id: 123, status: "reserved".into() })?; + Ok(()) + } +} + +// main.rs - wire them together +let inventory: ActorRef = InventoryService::new().start(); +let inventory_recipient: Recipient = inventory.recipient(); + +let order_service = OrderService::new(inventory_recipient).start(); +``` + +--- + +## Files to Create/Modify + +| File | Action | Description | +|------|--------|-------------| +| `concurrency/src/recipient.rs` | Create | Recipient and MessageSender trait | +| `concurrency/src/identity.rs` | Create | Internal ActorId (not public) | +| `concurrency/src/traits.rs` | Create | Supervisable, Linkable, Monitorable | +| `concurrency/src/registry.rs` | Create | Named actor registry | +| `concurrency/src/message.rs` | Create | Message and Handler traits | +| `concurrency/src/context.rs` | Create | Context type | +| `concurrency/src/tasks/actor.rs` | Rewrite | New Actor/Handler API | +| `concurrency/src/threads/actor.rs` | Rewrite | Same changes for threads | +| `concurrency/src/lib.rs` | Update | Export new types | +| `examples/*` | Update | Migrate to new API | + +--- + +## Migration Path + +1. **Step 1:** Add Message trait and Handler pattern alongside current API +2. **Step 2:** Add Recipient for type-erased sending +3. **Step 3:** Add Supervisable/Linkable/Monitorable traits +4. **Step 4:** Add Registry with Recipient +5. **Deprecation:** Mark old Request/Reply/Message associated types as deprecated +6. **Removal:** Remove old API in subsequent release + +--- + +## v0.6+ Considerations + +| Feature | Approach | +|---------|----------| +| **Clustering** | Add `RemoteRecipient` that serializes ActorId + message | +| **State machines** | gen_statem equivalent using Handler pattern | +| **Persistence** | Event sourcing via Handler | + +--- + +## Verification + +1. `cargo build --workspace` - All crates compile +2. `cargo test --workspace` - All tests pass +3. Update examples to new API +4. Test bidirectional actor communication without circular deps +5. Test Supervisable/Linkable traits work correctly + +--- + +## Final Decisions + +| Item | Decision | +|------|----------| +| Method naming | `send()` = fire-forget, `request()` = wait for reply | +| Dead actor handling | Returns `Err(ActorStopped)` (type-safe feedback) | +| Pattern support | Per-message types only (no enum fallback) | +| Type erasure | `Recipient` for message-type-safe erasure | +| Actor identity | Internal `ActorId` (not exposed as Pid) | +| Supervision | `Supervisable` / `Linkable` / `Monitorable` traits | From a39ce76619ac4888781cfc1ddc423b0c73f3f4db Mon Sep 17 00:00:00 2001 From: Esteban Dimitroff Hodi Date: Thu, 5 Feb 2026 15:48:06 -0300 Subject: [PATCH 2/6] fix: use Receiver trait with Recipient alias for consistent naming --- docs/API_REDESIGN.md | 31 +++++++++++++++++++------------ 1 file changed, 19 insertions(+), 12 deletions(-) diff --git a/docs/API_REDESIGN.md b/docs/API_REDESIGN.md index 91ca23f..1c011b2 100644 --- a/docs/API_REDESIGN.md +++ b/docs/API_REDESIGN.md @@ -38,13 +38,14 @@ struct ActorB { peer: ActorRef } // CIRCULAR! ### Solution: Recipient\ ```rust -trait MessageSender: Send + Sync { +/// Trait for anything that can receive messages of type M +trait Receiver: Send + Sync { fn send(&self, msg: M) -> Result<(), ActorError>; } -// ActorRef implements MessageSender -// Type-erased handle for specific message type -type Recipient = Arc>; +// ActorRef implements Receiver for all M where A: Handler +// Type-erased handle (ergonomic alias) +type Recipient = Arc>; // Usage - no circular dependency! struct ActorA { peer: Recipient } @@ -88,22 +89,28 @@ let name: Option = actor.request(GetName("joe")).await?; # Implementation Plan -## Phase 3.1: Recipient (Type-Erased Sender) +## Phase 3.1: Receiver\ Trait and Recipient\ Alias **New file:** `concurrency/src/recipient.rs` ```rust -/// Trait for sending a specific message type -pub trait MessageSender: Send + Sync { +/// Trait for anything that can receive messages of type M +/// +/// This is implemented by ActorRef for all message types the actor handles. +/// Use `Recipient` for type-erased storage. +pub trait Receiver: Send + Sync { + /// Fire-and-forget send fn send(&self, msg: M) -> Result<(), ActorError>; + + /// Send and wait for reply fn request(&self, msg: M) -> impl Future> + Send; } -/// Type-erased handle that can send messages of type M -pub type Recipient = Arc>; +/// Type-erased handle (ergonomic alias) +pub type Recipient = Arc>; -// ActorRef implements MessageSender for all M where A: Handler -impl MessageSender for ActorRef +// ActorRef implements Receiver for all M where A: Handler +impl Receiver for ActorRef where A: Actor + Handler, M: Message, @@ -375,7 +382,7 @@ let order_service = OrderService::new(inventory_recipient).start(); | File | Action | Description | |------|--------|-------------| -| `concurrency/src/recipient.rs` | Create | Recipient and MessageSender trait | +| `concurrency/src/recipient.rs` | Create | Receiver trait and Recipient alias | | `concurrency/src/identity.rs` | Create | Internal ActorId (not public) | | `concurrency/src/traits.rs` | Create | Supervisable, Linkable, Monitorable | | `concurrency/src/registry.rs` | Create | Named actor registry | From fa724c8688bdd1dfbca96de9f95db14671808066 Mon Sep 17 00:00:00 2001 From: Esteban Dimitroff Hodi Date: Thu, 5 Feb 2026 18:19:13 -0300 Subject: [PATCH 3/6] docs: use Actix-style object-safe Receiver trait in API plan --- docs/API_REDESIGN.md | 84 ++++++++++++++++++++++++++++++++++++-------- 1 file changed, 69 insertions(+), 15 deletions(-) diff --git a/docs/API_REDESIGN.md b/docs/API_REDESIGN.md index 1c011b2..b6c8523 100644 --- a/docs/API_REDESIGN.md +++ b/docs/API_REDESIGN.md @@ -38,15 +38,26 @@ struct ActorB { peer: ActorRef } // CIRCULAR! ### Solution: Recipient\ ```rust -/// Trait for anything that can receive messages of type M +/// Trait for anything that can receive messages of type M. +/// Object-safe: all methods return concrete types (no async/impl Future). +/// Async waiting happens outside the trait via oneshot channels (Actix pattern). trait Receiver: Send + Sync { fn send(&self, msg: M) -> Result<(), ActorError>; + fn request(&self, msg: M) -> Result, ActorError>; } // ActorRef implements Receiver for all M where A: Handler // Type-erased handle (ergonomic alias) type Recipient = Arc>; +// Ergonomic async wrapper on the concrete Recipient type (not on the trait) +impl Recipient { + pub async fn send_request(&self, msg: M) -> Result { + let rx = self.request(msg)?; + rx.await.map_err(|_| ActorError::ActorStopped) + } +} + // Usage - no circular dependency! struct ActorA { peer: Recipient } struct ActorB { peer: Recipient } @@ -94,29 +105,51 @@ let name: Option = actor.request(GetName("joe")).await?; **New file:** `concurrency/src/recipient.rs` ```rust -/// Trait for anything that can receive messages of type M +/// Trait for anything that can receive messages of type M. +/// +/// Object-safe by design: all methods return concrete types, no async/impl Future. +/// This follows the Actix pattern where async waiting happens outside the trait +/// boundary via oneshot channels, keeping the trait compatible with `dyn`. /// /// This is implemented by ActorRef for all message types the actor handles. /// Use `Recipient` for type-erased storage. pub trait Receiver: Send + Sync { - /// Fire-and-forget send + /// Fire-and-forget send (enqueue message, don't wait for reply) fn send(&self, msg: M) -> Result<(), ActorError>; - /// Send and wait for reply - fn request(&self, msg: M) -> impl Future> + Send; + /// Enqueue message and return a oneshot channel to await the reply. + /// This is synchronous — it only does channel plumbing. + /// The async waiting happens on the returned receiver. + fn request(&self, msg: M) -> Result, ActorError>; } -/// Type-erased handle (ergonomic alias) +/// Type-erased handle (ergonomic alias). Object-safe because Receiver is. pub type Recipient = Arc>; +/// Ergonomic async wrapper — lives on the concrete type, not the trait. +impl Recipient { + pub async fn send_request(&self, msg: M) -> Result { + let rx = Receiver::request(&**self, msg)?; + rx.await.map_err(|_| ActorError::ActorStopped) + } +} + // ActorRef implements Receiver for all M where A: Handler impl Receiver for ActorRef where A: Actor + Handler, M: Message, { - fn send(&self, msg: M) -> Result<(), ActorError> { ... } - fn request(&self, msg: M) -> impl Future> + Send { ... } + fn send(&self, msg: M) -> Result<(), ActorError> { + // Pack message into envelope, push to actor's mailbox channel + ... + } + + fn request(&self, msg: M) -> Result, ActorError> { + // Create oneshot channel, pack (msg, tx) into envelope, + // push to actor's mailbox, return rx + ... + } } // Convert ActorRef to Recipient @@ -232,13 +265,23 @@ pub trait Actor: Send + Sized + 'static { **ActorRef changes:** ```rust -/// Typed handle to an actor +/// Typed handle to an actor. +/// +/// Internally uses an envelope pattern (like Actix) for the mailbox: +/// messages of different types are packed into `Box>` so +/// the actor's single mpsc channel can carry any message type the actor handles. pub struct ActorRef { id: ActorId, // Internal identity (not public) - sender: mpsc::Sender>, + sender: mpsc::Sender + Send>>, _marker: PhantomData, } +/// Type-erased envelope that the actor loop can dispatch. +/// Each concrete envelope captures the message and an optional oneshot sender. +trait Envelope: Send { + fn handle(self: Box, actor: &mut A, ctx: &Context); +} + impl ActorRef where A: Actor + Handler, @@ -247,8 +290,15 @@ where /// Fire-and-forget send (returns error if actor dead) pub fn send(&self, msg: M) -> Result<(), ActorError>; - /// Send and wait for typed response - pub async fn request(&self, msg: M) -> Result; + /// Enqueue message and return a oneshot receiver for the reply. + /// Synchronous — only does channel plumbing (Actix pattern). + pub fn request(&self, msg: M) -> Result, ActorError>; + + /// Ergonomic async request: enqueue + await reply. + pub async fn send_request(&self, msg: M) -> Result { + let rx = self.request(msg)?; + rx.await.map_err(|_| ActorError::ActorStopped) + } /// Get type-erased Recipient for this message type pub fn recipient(&self) -> Recipient; @@ -309,12 +359,16 @@ impl Handler for Bank { // main.rs let bank: ActorRef = Bank::new().start(); -// Type-safe request (wait for reply) -let balance: Result = bank.request(GetBalance { account: "alice".into() }).await?; +// Type-safe request (async convenience wrapper: enqueue + await oneshot) +let balance: Result = bank.send_request(GetBalance { account: "alice".into() }).await?; // Fire-and-forget send bank.send(Deposit { account: "alice".into(), amount: 50 })?; +// Low-level: get oneshot receiver directly (useful for select!, timeouts, etc.) +let rx = bank.request(GetBalance { account: "alice".into() })?; +let balance = tokio::time::timeout(Duration::from_secs(5), rx).await??; + // Get type-erased Recipient for storage/passing to other actors let recipient: Recipient = bank.recipient(); @@ -347,7 +401,7 @@ pub struct OrderService { impl Handler for OrderService { async fn handle(&mut self, msg: PlaceOrder, ctx: &Context) -> Result<(), OrderError> { let reply_to: Recipient = ctx.recipient(); - self.inventory.request(InventoryReserve { + self.inventory.send_request(InventoryReserve { item: msg.item, quantity: msg.quantity, reply_to, From 1ef33bf0c463543dca379463c554ccc5914c86ff Mon Sep 17 00:00:00 2001 From: Esteban Dimitroff Hodi Date: Fri, 6 Feb 2026 15:29:01 -0300 Subject: [PATCH 4/6] docs: address PR review comments on Supervisable and Handler traits --- docs/API_REDESIGN.md | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/docs/API_REDESIGN.md b/docs/API_REDESIGN.md index b6c8523..86f1842 100644 --- a/docs/API_REDESIGN.md +++ b/docs/API_REDESIGN.md @@ -187,14 +187,17 @@ pub enum ExitReason { **New file:** `concurrency/src/traits.rs` ```rust -/// Trait for actors that can be supervised +/// Trait for actors that can be supervised. +/// Provides actor_id() for identity comparison with trait objects. pub trait Supervisable: Send + Sync { + fn actor_id(&self) -> ActorId; fn stop(&self); fn is_alive(&self) -> bool; fn on_exit(&self, callback: Box); } -/// Trait for actors that can be linked +/// Trait for actors that can be linked. +/// Uses actor_id() (from Supervisable) to track links internally. pub trait Linkable: Supervisable { fn link(&self, other: &dyn Linkable); fn unlink(&self, other: &dyn Linkable); @@ -240,7 +243,10 @@ pub trait Message: Send + 'static { type Result: Send; } -/// Handler for a specific message type +/// Handler for a specific message type. +/// Uses RPITIT (Rust 1.75+) — this is fine since Handler is never used as dyn. +/// &mut self is safe: actors process messages sequentially (one at a time), +/// so there is no concurrent access to self. pub trait Handler: Actor { fn handle(&mut self, msg: M, ctx: &Context) -> impl Future + Send; } From 179437efa2d5131464daf2579030e99b7ee7766c Mon Sep 17 00:00:00 2001 From: Esteban Dimitroff Hodi Date: Mon, 9 Feb 2026 19:28:32 -0300 Subject: [PATCH 5/6] feat: implement Handler API with envelope-based mailbox (#144, #145) --- concurrency/src/error.rs | 20 +- concurrency/src/lib.rs | 4 +- concurrency/src/message.rs | 52 + concurrency/src/messages.rs | 2 - concurrency/src/tasks/actor.rs | 1297 +++++++++------------- concurrency/src/tasks/mod.rs | 11 +- concurrency/src/tasks/stream.rs | 23 +- concurrency/src/tasks/stream_tests.rs | 154 ++- concurrency/src/tasks/time.rs | 31 +- concurrency/src/tasks/timer_tests.rs | 264 ++--- concurrency/src/threads/actor.rs | 525 +++++---- concurrency/src/threads/mod.rs | 11 +- concurrency/src/threads/stream.rs | 18 +- concurrency/src/threads/time.rs | 48 +- concurrency/src/threads/timer_tests.rs | 266 ++--- examples/bank/src/main.rs | 72 +- examples/bank/src/messages.rs | 42 +- examples/bank/src/server.rs | 141 +-- examples/bank_threads/src/main.rs | 72 +- examples/bank_threads/src/messages.rs | 42 +- examples/bank_threads/src/server.rs | 134 +-- examples/blocking_genserver/main.rs | 113 +- examples/busy_genserver_warning/main.rs | 51 +- examples/name_server/src/main.rs | 31 +- examples/name_server/src/messages.rs | 26 +- examples/name_server/src/server.rs | 53 +- examples/ping_pong/src/consumer.rs | 30 +- examples/ping_pong/src/main.rs | 62 +- examples/ping_pong/src/messages.rs | 29 +- examples/ping_pong/src/producer.rs | 34 +- examples/signal_test/src/main.rs | 79 +- examples/signal_test_threads/src/main.rs | 75 +- examples/updater/src/main.rs | 2 +- examples/updater/src/messages.rs | 14 +- examples/updater/src/server.rs | 45 +- examples/updater_threads/src/main.rs | 2 +- examples/updater_threads/src/messages.rs | 14 +- examples/updater_threads/src/server.rs | 46 +- 38 files changed, 1660 insertions(+), 2275 deletions(-) create mode 100644 concurrency/src/message.rs delete mode 100644 concurrency/src/messages.rs diff --git a/concurrency/src/error.rs b/concurrency/src/error.rs index 3b23e4b..35123ef 100644 --- a/concurrency/src/error.rs +++ b/concurrency/src/error.rs @@ -1,28 +1,20 @@ #[derive(Debug, thiserror::Error)] pub enum ActorError { - #[error("Callback Error")] - Callback, - #[error("Initialization error")] - Initialization, - #[error("Server error")] - Server, - #[error("Unsupported Request on this Actor")] - RequestUnused, - #[error("Unsupported Message on this Actor")] - MessageUnused, + #[error("Actor stopped")] + ActorStopped, #[error("Request to Actor timed out")] RequestTimeout, } impl From> for ActorError { fn from(_value: spawned_rt::threads::mpsc::SendError) -> Self { - Self::Server + Self::ActorStopped } } impl From> for ActorError { fn from(_value: spawned_rt::tasks::mpsc::SendError) -> Self { - Self::Server + Self::ActorStopped } } @@ -32,7 +24,7 @@ mod tests { #[test] fn test_error_into_std_error() { - let error: &dyn std::error::Error = &ActorError::Callback; - assert_eq!(error.to_string(), "Callback Error"); + let error: &dyn std::error::Error = &ActorError::ActorStopped; + assert_eq!(error.to_string(), "Actor stopped"); } } diff --git a/concurrency/src/lib.rs b/concurrency/src/lib.rs index 0edcab8..9d71f12 100644 --- a/concurrency/src/lib.rs +++ b/concurrency/src/lib.rs @@ -1,6 +1,4 @@ -//! spawned concurrency -//! Some basic traits and structs to implement concurrent code à-la-Erlang. pub mod error; -pub mod messages; +pub mod message; pub mod tasks; pub mod threads; diff --git a/concurrency/src/message.rs b/concurrency/src/message.rs new file mode 100644 index 0000000..a97b310 --- /dev/null +++ b/concurrency/src/message.rs @@ -0,0 +1,52 @@ +pub trait Message: Send + 'static { + type Result: Send + 'static; +} + +/// Declarative macro for defining message types. +/// +/// Supports both unit structs and structs with fields, and they can be mixed +/// in a single invocation: +/// +/// ```ignore +/// messages! { +/// GetCount -> u64; +/// Deposit { who: String, amount: i32 } -> Result; +/// Stop -> () +/// } +/// ``` +#[macro_export] +macro_rules! messages { + () => {}; + + // Base: unit message + ($(#[$meta:meta])* $name:ident -> $result:ty) => { + $(#[$meta])* + #[derive(Debug)] + pub struct $name; + impl $crate::message::Message for $name { + type Result = $result; + } + }; + + // Base: struct message + ($(#[$meta:meta])* $name:ident { $($field:ident : $ftype:ty),* $(,)? } -> $result:ty) => { + $(#[$meta])* + #[derive(Debug)] + pub struct $name { $(pub $field: $ftype,)* } + impl $crate::message::Message for $name { + type Result = $result; + } + }; + + // Recursive: unit message followed by more + ($(#[$meta:meta])* $name:ident -> $result:ty; $($rest:tt)*) => { + $crate::messages!($(#[$meta])* $name -> $result); + $crate::messages!($($rest)*); + }; + + // Recursive: struct message followed by more + ($(#[$meta:meta])* $name:ident { $($field:ident : $ftype:ty),* $(,)? } -> $result:ty; $($rest:tt)*) => { + $crate::messages!($(#[$meta])* $name { $($field : $ftype),* } -> $result); + $crate::messages!($($rest)*); + }; +} diff --git a/concurrency/src/messages.rs b/concurrency/src/messages.rs deleted file mode 100644 index e0aceb8..0000000 --- a/concurrency/src/messages.rs +++ /dev/null @@ -1,2 +0,0 @@ -#[derive(Clone, Debug)] -pub struct Unused; diff --git a/concurrency/src/tasks/actor.rs b/concurrency/src/tasks/actor.rs index d41e3a3..b63f1ce 100644 --- a/concurrency/src/tasks/actor.rs +++ b/concurrency/src/tasks/actor.rs @@ -1,470 +1,429 @@ -//! Actor trait and structs to create an abstraction similar to Erlang gen_server. -//! See examples/name_server for a usage example. -use crate::{ - error::ActorError, - tasks::InitResult::{NoSuccess, Success}, -}; +use crate::error::ActorError; +use crate::message::Message; use core::pin::pin; use futures::future::{self, FutureExt as _}; use spawned_rt::{ tasks::{self as rt, mpsc, oneshot, timeout, watch, CancellationToken, JoinHandle}, threads, }; -use std::{fmt::Debug, future::Future, panic::AssertUnwindSafe, time::Duration}; +use std::{fmt::Debug, future::Future, panic::AssertUnwindSafe, pin::Pin, sync::Arc, time::Duration}; const DEFAULT_REQUEST_TIMEOUT: Duration = Duration::from_secs(5); -/// Execution backend for Actor. -/// -/// Determines how the Actor's async loop is executed. Choose based on -/// the nature of your workload: -/// -/// # Backend Comparison -/// -/// | Backend | Execution Model | Best For | Limitations | -/// |---------|-----------------|----------|-------------| -/// | `Async` | Tokio task | Non-blocking I/O, async operations | Blocks runtime if sync code runs too long | -/// | `Blocking` | Tokio blocking pool | Short blocking operations (file I/O, DNS) | Shared pool with limited threads | -/// | `Thread` | Dedicated OS thread with own runtime | Long-running services, isolation from main runtime | Higher memory overhead per Actor | -/// -/// **Note**: All backends use async internally. For fully synchronous code without any async -/// runtime, use [`threads::Actor`](crate::threads::Actor) instead. -/// -/// # Examples -/// -/// ```ignore -/// // For typical async workloads (HTTP handlers, database queries) -/// let handle = MyServer::new().start(); -/// -/// // For occasional blocking operations (file reads, external commands) -/// let handle = MyServer::new().start_with_backend(Backend::Blocking); -/// -/// // For CPU-intensive or permanently blocking services -/// let handle = MyServer::new().start_with_backend(Backend::Thread); -/// ``` -/// -/// # When to Use Each Backend -/// -/// ## `Backend::Async` (Default) -/// - **Advantages**: Lightweight, efficient, good for high concurrency -/// - **Use when**: Your Actor does mostly async I/O (network, database) -/// - **Avoid when**: Your code blocks (e.g., `std::thread::sleep`, heavy computation) -/// -/// ## `Backend::Blocking` -/// - **Advantages**: Prevents blocking the async runtime, uses tokio's managed pool -/// - **Use when**: You have occasional blocking operations that complete quickly -/// - **Avoid when**: You need guaranteed thread availability or long-running blocks -/// -/// ## `Backend::Thread` -/// - **Advantages**: Isolated from main runtime, dedicated thread won't affect other tasks -/// - **Use when**: Long-running singleton services that shouldn't share the main runtime -/// - **Avoid when**: You need many Actors (each gets its own OS thread + runtime) -/// - **Note**: Still uses async internally (own runtime). For sync code, use `threads::Actor` +// --------------------------------------------------------------------------- +// Backend +// --------------------------------------------------------------------------- + #[derive(Debug, Clone, Copy, Default, PartialEq, Eq)] pub enum Backend { - /// Run on tokio async runtime (default). - /// - /// Best for non-blocking, async workloads. The Actor runs as a - /// lightweight tokio task, enabling high concurrency with minimal overhead. - /// - /// **Warning**: If your `handle_request` or `handle_message` blocks synchronously - /// (e.g., `std::thread::sleep`, CPU-heavy loops), it will block the entire - /// tokio runtime thread, affecting other tasks. #[default] Async, - - /// Run on tokio's blocking thread pool. - /// - /// Use for Actors that perform blocking operations like: - /// - Synchronous file I/O - /// - DNS lookups - /// - External process calls - /// - Short CPU-bound computations - /// - /// The pool is shared across all `spawn_blocking` calls and has a default - /// limit of 512 threads. If the pool is exhausted, new blocking tasks wait. Blocking, - - /// Run on a dedicated OS thread with its own async runtime. - /// - /// Use for Actors that: - /// - Need isolation from the main tokio runtime - /// - Are long-running singleton services - /// - Should not compete with other tasks for runtime resources - /// - /// Each Actor gets its own thread with a separate tokio runtime, - /// providing isolation from other async tasks. Higher memory overhead - /// (~2MB stack per thread plus runtime overhead). - /// - /// **Note**: This still uses async internally. For fully synchronous code - /// without any async runtime, use [`threads::Actor`](crate::threads::Actor). Thread, } -#[derive(Debug)] -pub struct ActorRef { - pub tx: mpsc::Sender>, - /// Cancellation token to stop the Actor +// --------------------------------------------------------------------------- +// Actor trait +// --------------------------------------------------------------------------- + +pub trait Actor: Send + Sized + 'static { + fn started(&mut self, _ctx: &Context) -> impl Future + Send { + async {} + } + + fn stopped(&mut self, _ctx: &Context) -> impl Future + Send { + async {} + } +} + +// --------------------------------------------------------------------------- +// Handler trait (per-message, uses RPITIT — NOT object-safe, that's fine) +// --------------------------------------------------------------------------- + +pub trait Handler: Actor { + fn handle( + &mut self, + msg: M, + ctx: &Context, + ) -> impl Future + Send; +} + +// --------------------------------------------------------------------------- +// Envelope (type-erasure on the actor side) +// --------------------------------------------------------------------------- + +trait Envelope: Send { + fn handle<'a>( + self: Box, + actor: &'a mut A, + ctx: &'a Context, + ) -> Pin + Send + 'a>>; +} + +struct MessageEnvelope { + msg: M, + tx: Option>, +} + +impl Envelope for MessageEnvelope +where + A: Actor + Handler, + M: Message, +{ + fn handle<'a>( + self: Box, + actor: &'a mut A, + ctx: &'a Context, + ) -> Pin + Send + 'a>> { + Box::pin(async move { + let result = actor.handle(self.msg, ctx).await; + if let Some(tx) = self.tx { + let _ = tx.send(result); + } + }) + } +} + +// --------------------------------------------------------------------------- +// Context +// --------------------------------------------------------------------------- + +pub struct Context { + sender: mpsc::Sender + Send>>, cancellation_token: CancellationToken, - /// Completion signal for waiting on actor stop (true = stopped) - completion_rx: watch::Receiver, } -impl Clone for ActorRef { +impl Clone for Context { fn clone(&self) -> Self { Self { - tx: self.tx.clone(), + sender: self.sender.clone(), cancellation_token: self.cancellation_token.clone(), - completion_rx: self.completion_rx.clone(), } } } -impl ActorRef { - fn new(actor: A) -> Self { - let (tx, mut rx) = mpsc::channel::>(); - let cancellation_token = CancellationToken::new(); - let (completion_tx, completion_rx) = watch::channel(false); - let handle = ActorRef { - tx, - cancellation_token, - completion_rx, - }; - let handle_clone = handle.clone(); - let inner_future = async move { - if let Err(error) = actor.run(&handle, &mut rx).await { - tracing::trace!(%error, "Actor crashed") - } - // Signal completion to all waiters - let _ = completion_tx.send(true); - }; +impl Debug for Context { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.debug_struct("Context").finish_non_exhaustive() + } +} - #[cfg(debug_assertions)] - // Optionally warn if the Actor future blocks for too much time - let inner_future = warn_on_block::WarnOnBlocking::new(inner_future); +impl Context { + pub fn from_ref(actor_ref: &ActorRef) -> Self { + Self { + sender: actor_ref.sender.clone(), + cancellation_token: actor_ref.cancellation_token.clone(), + } + } - let _task_handle = rt::spawn(inner_future); + pub fn stop(&self) { + self.cancellation_token.cancel(); + } - handle_clone + pub fn send(&self, msg: M) -> Result<(), ActorError> + where + A: Handler, + M: Message, + { + let envelope = MessageEnvelope { msg, tx: None }; + self.sender + .send(Box::new(envelope)) + .map_err(|_| ActorError::ActorStopped) } - fn new_blocking(actor: A) -> Self { - let (tx, mut rx) = mpsc::channel::>(); - let cancellation_token = CancellationToken::new(); - let (completion_tx, completion_rx) = watch::channel(false); - let handle = ActorRef { - tx, - cancellation_token, - completion_rx, + pub fn request(&self, msg: M) -> Result, ActorError> + where + A: Handler, + M: Message, + { + let (tx, rx) = oneshot::channel(); + let envelope = MessageEnvelope { + msg, + tx: Some(tx), }; - let handle_clone = handle.clone(); - let _task_handle = rt::spawn_blocking(move || { - rt::block_on(async move { - if let Err(error) = actor.run(&handle, &mut rx).await { - tracing::trace!(%error, "Actor crashed") - }; - // Signal completion to all waiters - let _ = completion_tx.send(true); - }) - }); + self.sender + .send(Box::new(envelope)) + .map_err(|_| ActorError::ActorStopped)?; + Ok(rx) + } + + pub async fn send_request(&self, msg: M) -> Result + where + A: Handler, + M: Message, + { + let rx = self.request(msg)?; + match timeout(DEFAULT_REQUEST_TIMEOUT, rx).await { + Ok(Ok(result)) => Ok(result), + Ok(Err(_)) => Err(ActorError::ActorStopped), + Err(_) => Err(ActorError::RequestTimeout), + } + } - handle_clone + pub(crate) fn cancellation_token(&self) -> CancellationToken { + self.cancellation_token.clone() } +} - fn new_on_thread(actor: A) -> Self { - let (tx, mut rx) = mpsc::channel::>(); - let cancellation_token = CancellationToken::new(); - let (completion_tx, completion_rx) = watch::channel(false); - let handle = ActorRef { - tx, - cancellation_token, - completion_rx, - }; - let handle_clone = handle.clone(); - let _thread_handle = threads::spawn(move || { - threads::block_on(async move { - if let Err(error) = actor.run(&handle, &mut rx).await { - tracing::trace!(%error, "Actor crashed") - }; - // Signal completion to all waiters - let _ = completion_tx.send(true); - }) - }); +// --------------------------------------------------------------------------- +// Receiver trait (object-safe) + Recipient alias +// --------------------------------------------------------------------------- - handle_clone +pub trait Receiver: Send + Sync { + fn send(&self, msg: M) -> Result<(), ActorError>; + fn request(&self, msg: M) -> Result, ActorError>; +} + +pub type Recipient = Arc>; + +pub async fn send_request( + recipient: &dyn Receiver, + msg: M, + timeout_duration: Duration, +) -> Result { + let rx = recipient.request(msg)?; + match timeout(timeout_duration, rx).await { + Ok(Ok(result)) => Ok(result), + Ok(Err(_)) => Err(ActorError::ActorStopped), + Err(_) => Err(ActorError::RequestTimeout), } +} + +// --------------------------------------------------------------------------- +// ActorRef +// --------------------------------------------------------------------------- - pub fn sender(&self) -> mpsc::Sender> { - self.tx.clone() +pub struct ActorRef { + sender: mpsc::Sender + Send>>, + cancellation_token: CancellationToken, + completion_rx: watch::Receiver, +} + +impl Debug for ActorRef { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.debug_struct("ActorRef").finish_non_exhaustive() } +} - pub async fn request(&mut self, message: A::Request) -> Result { - self.request_with_timeout(message, DEFAULT_REQUEST_TIMEOUT) - .await +impl Clone for ActorRef { + fn clone(&self) -> Self { + Self { + sender: self.sender.clone(), + cancellation_token: self.cancellation_token.clone(), + completion_rx: self.completion_rx.clone(), + } } +} - pub async fn request_with_timeout( - &mut self, - message: A::Request, +impl ActorRef { + pub fn send(&self, msg: M) -> Result<(), ActorError> + where + A: Handler, + M: Message, + { + let envelope = MessageEnvelope { msg, tx: None }; + self.sender + .send(Box::new(envelope)) + .map_err(|_| ActorError::ActorStopped) + } + + pub fn request(&self, msg: M) -> Result, ActorError> + where + A: Handler, + M: Message, + { + let (tx, rx) = oneshot::channel(); + let envelope = MessageEnvelope { + msg, + tx: Some(tx), + }; + self.sender + .send(Box::new(envelope)) + .map_err(|_| ActorError::ActorStopped)?; + Ok(rx) + } + + pub async fn send_request(&self, msg: M) -> Result + where + A: Handler, + M: Message, + { + self.send_request_with_timeout(msg, DEFAULT_REQUEST_TIMEOUT).await + } + + pub async fn send_request_with_timeout( + &self, + msg: M, duration: Duration, - ) -> Result { - let (oneshot_tx, oneshot_rx) = oneshot::channel::>(); - self.tx.send(ActorInMsg::Request { - sender: oneshot_tx, - message, - })?; - - match timeout(duration, oneshot_rx).await { - Ok(Ok(result)) => result, - Ok(Err(_)) => Err(ActorError::Server), + ) -> Result + where + A: Handler, + M: Message, + { + let rx = self.request(msg)?; + match timeout(duration, rx).await { + Ok(Ok(result)) => Ok(result), + Ok(Err(_)) => Err(ActorError::ActorStopped), Err(_) => Err(ActorError::RequestTimeout), } } - pub async fn send(&mut self, message: A::Message) -> Result<(), ActorError> { - self.tx - .send(ActorInMsg::Message { message }) - .map_err(|_error| ActorError::Server) + pub fn recipient(&self) -> Recipient + where + A: Handler, + M: Message, + { + Arc::new(self.clone()) } - pub(crate) fn cancellation_token(&self) -> CancellationToken { - self.cancellation_token.clone() + pub fn context(&self) -> Context { + Context::from_ref(self) } - /// Waits for the actor to stop. - /// - /// This method returns a future that completes when the actor has finished - /// processing and exited its main loop. Can be called multiple times from - /// different clones of the ActorRef - all callers will be notified when - /// the actor stops. pub async fn join(&self) { let mut rx = self.completion_rx.clone(); - // Wait until completion signal is true while !*rx.borrow_and_update() { if rx.changed().await.is_err() { - // Sender dropped, actor must have completed break; } } } } -pub enum ActorInMsg { - Request { - sender: oneshot::Sender>, - message: A::Request, - }, - Message { - message: A::Message, - }, -} - -pub enum RequestResponse { - Reply(A::Reply), - Unused, - Stop(A::Reply), -} +// Bridge: ActorRef implements Receiver for any M that A handles +impl Receiver for ActorRef +where + A: Actor + Handler, + M: Message, +{ + fn send(&self, msg: M) -> Result<(), ActorError> { + ActorRef::send(self, msg) + } -pub enum MessageResponse { - NoReply, - Unused, - Stop, + fn request(&self, msg: M) -> Result, ActorError> { + ActorRef::request(self, msg) + } } -pub enum InitResult { - Success(A), - NoSuccess(A), -} +// --------------------------------------------------------------------------- +// Actor startup + main loop +// --------------------------------------------------------------------------- -pub trait Actor: Send + Sized { - type Request: Clone + Send + Sized + Sync; - type Message: Clone + Send + Sized + Sync; - type Reply: Send + Sized; - type Error: Debug + Send; +impl ActorRef { + fn spawn(actor: A, backend: Backend) -> Self { + let (tx, rx) = mpsc::channel:: + Send>>(); + let cancellation_token = CancellationToken::new(); + let (completion_tx, completion_rx) = watch::channel(false); - /// Start the Actor with the default backend (Async). - fn start(self) -> ActorRef { - self.start_with_backend(Backend::default()) - } + let actor_ref = ActorRef { + sender: tx.clone(), + cancellation_token: cancellation_token.clone(), + completion_rx, + }; - /// Start the Actor with the specified backend. - /// - /// # Arguments - /// * `backend` - The execution backend to use: - /// - `Backend::Async` - Run on tokio async runtime (default, best for non-blocking workloads) - /// - `Backend::Blocking` - Run on tokio's blocking thread pool (for blocking operations) - /// - `Backend::Thread` - Run on a dedicated OS thread (for long-running blocking services) - fn start_with_backend(self, backend: Backend) -> ActorRef { - match backend { - Backend::Async => ActorRef::new(self), - Backend::Blocking => ActorRef::new_blocking(self), - Backend::Thread => ActorRef::new_on_thread(self), - } - } + let ctx = Context { + sender: tx, + cancellation_token: cancellation_token.clone(), + }; - fn run( - self, - handle: &ActorRef, - rx: &mut mpsc::Receiver>, - ) -> impl Future> + Send { - async { - let res = match self.init(handle).await { - Ok(Success(new_state)) => Ok(new_state.main_loop(handle, rx).await), - Ok(NoSuccess(intermediate_state)) => { - // new_state is NoSuccess, this means the initialization failed, but the error was handled - // in callback. No need to report the error. - // Just skip main_loop and return the state to teardown the Actor - Ok(intermediate_state) - } - Err(err) => { - tracing::error!("Initialization failed with unhandled error: {err:?}"); - Err(ActorError::Initialization) - } - }; + let inner_future = async move { + run_actor(actor, ctx, rx, cancellation_token).await; + let _ = completion_tx.send(true); + }; - handle.cancellation_token().cancel(); - if let Ok(final_state) = res { - if let Err(err) = final_state.teardown(handle).await { - tracing::error!("Error during teardown: {err:?}"); - } + match backend { + Backend::Async => { + #[cfg(debug_assertions)] + let inner_future = warn_on_block::WarnOnBlocking::new(inner_future); + let _handle = rt::spawn(inner_future); + } + Backend::Blocking => { + let _handle = rt::spawn_blocking(move || { + rt::block_on(inner_future) + }); + } + Backend::Thread => { + let _handle = threads::spawn(move || { + threads::block_on(inner_future) + }); } - Ok(()) } - } - /// Initialization function. It's called before main loop. It - /// can be overrided on implementations in case initial steps are - /// required. - fn init( - self, - _handle: &ActorRef, - ) -> impl Future, Self::Error>> + Send { - async { Ok(Success(self)) } + actor_ref } +} - fn main_loop( - mut self, - handle: &ActorRef, - rx: &mut mpsc::Receiver>, - ) -> impl Future + Send { - async { - loop { - if !self.receive(handle, rx).await { +async fn run_actor( + mut actor: A, + ctx: Context, + mut rx: mpsc::Receiver + Send>>, + cancellation_token: CancellationToken, +) { + actor.started(&ctx).await; + + if cancellation_token.is_cancelled() { + actor.stopped(&ctx).await; + return; + } + + loop { + let msg = rx.recv().await; + match msg { + Some(envelope) => { + let result = AssertUnwindSafe(envelope.handle(&mut actor, &ctx)) + .catch_unwind() + .await; + if let Err(panic) = result { + tracing::error!("Panic in message handler: {panic:?}"); + break; + } + if cancellation_token.is_cancelled() { break; } } - tracing::trace!("Stopping Actor"); - self + None => break, } } - fn receive( - &mut self, - handle: &ActorRef, - rx: &mut mpsc::Receiver>, - ) -> impl Future + Send { - async move { - let message = rx.recv().await; - - let keep_running = match message { - Some(ActorInMsg::Request { sender, message }) => { - let (keep_running, response) = - match AssertUnwindSafe(self.handle_request(message, handle)) - .catch_unwind() - .await - { - Ok(response) => match response { - RequestResponse::Reply(response) => (true, Ok(response)), - RequestResponse::Stop(response) => (false, Ok(response)), - RequestResponse::Unused => { - tracing::error!("Actor received unexpected Request"); - (false, Err(ActorError::RequestUnused)) - } - }, - Err(error) => { - tracing::error!("Error in callback: '{error:?}'"); - (false, Err(ActorError::Callback)) - } - }; - // Send response back - if sender.send(response).is_err() { - tracing::error!("Actor failed to send response back, client must have died") - }; - keep_running - } - Some(ActorInMsg::Message { message }) => { - match AssertUnwindSafe(self.handle_message(message, handle)) - .catch_unwind() - .await - { - Ok(response) => match response { - MessageResponse::NoReply => true, - MessageResponse::Stop => false, - MessageResponse::Unused => { - tracing::error!("Actor received unexpected Message"); - false - } - }, - Err(error) => { - tracing::trace!("Error in callback: '{error:?}'"); - false - } - } - } - None => { - // Channel has been closed; won't receive further messages. Stop the server. - false - } - }; - keep_running - } - } + cancellation_token.cancel(); + actor.stopped(&ctx).await; +} - fn handle_request( - &mut self, - _message: Self::Request, - _handle: &ActorRef, - ) -> impl Future> + Send { - async { RequestResponse::Unused } - } +// --------------------------------------------------------------------------- +// Actor::start +// --------------------------------------------------------------------------- - fn handle_message( - &mut self, - _message: Self::Message, - _handle: &ActorRef, - ) -> impl Future + Send { - async { MessageResponse::Unused } +pub trait ActorStart: Actor { + fn start(self) -> ActorRef { + self.start_with_backend(Backend::default()) } - /// Teardown function. It's called after the stop message is received. - /// It can be overrided on implementations in case final steps are required, - /// like closing streams, stopping timers, etc. - fn teardown( - self, - _handle: &ActorRef, - ) -> impl Future> + Send { - async { Ok(()) } + fn start_with_backend(self, backend: Backend) -> ActorRef { + ActorRef::spawn(self, backend) } } -/// Spawns a task that awaits on a future and sends a message to an Actor -/// on completion. -/// This function returns a handle to the spawned task. -pub fn send_message_on(handle: ActorRef, future: U, message: T::Message) -> JoinHandle<()> +impl ActorStart for A {} + +// --------------------------------------------------------------------------- +// send_message_on (utility) +// --------------------------------------------------------------------------- + +pub fn send_message_on(ctx: Context, future: U, msg: M) -> JoinHandle<()> where - T: Actor, + A: Actor + Handler, + M: Message, U: Future + Send + 'static, ::Output: Send, { - let cancellation_token = handle.cancellation_token(); - let mut handle_clone = handle.clone(); + let cancellation_token = ctx.cancellation_token(); let join_handle = rt::spawn(async move { let is_cancelled = pin!(cancellation_token.cancelled()); let signal = pin!(future); match future::select(is_cancelled, signal).await { future::Either::Left(_) => tracing::debug!("Actor stopped"), future::Either::Right(_) => { - if let Err(e) = handle_clone.send(message).await { + if let Err(e) = ctx.send(msg) { tracing::error!("Failed to send message: {e:?}") } } @@ -473,10 +432,13 @@ where join_handle } +// --------------------------------------------------------------------------- +// WarnOnBlocking (debug only) +// --------------------------------------------------------------------------- + #[cfg(debug_assertions)] mod warn_on_block { use super::*; - use std::time::Instant; use tracing::warn; @@ -514,229 +476,54 @@ mod warn_on_block { } } +// --------------------------------------------------------------------------- +// Tests +// --------------------------------------------------------------------------- + #[cfg(test)] mod tests { - use super::*; - use crate::{messages::Unused, tasks::send_after}; + use crate::messages; use std::{ - sync::{Arc, Mutex}, + sync::{atomic, Arc}, thread, time::Duration, }; - struct BadlyBehavedTask; - - #[derive(Clone)] - pub enum InMessage { - GetCount, - Stop, - } - #[derive(Clone)] - pub enum OutMsg { - Count(u64), - } + // --- Counter actor for basic tests --- - impl Actor for BadlyBehavedTask { - type Request = InMessage; - type Message = Unused; - type Reply = Unused; - type Error = Unused; - - async fn handle_request( - &mut self, - _: Self::Request, - _: &ActorRef, - ) -> RequestResponse { - RequestResponse::Stop(Unused) - } - - async fn handle_message( - &mut self, - _: Self::Message, - _: &ActorRef, - ) -> MessageResponse { - rt::sleep(Duration::from_millis(20)).await; - thread::sleep(Duration::from_secs(2)); - MessageResponse::Stop - } - } - - struct WellBehavedTask { - pub count: u64, - } - - impl Actor for WellBehavedTask { - type Request = InMessage; - type Message = Unused; - type Reply = OutMsg; - type Error = Unused; - - async fn handle_request( - &mut self, - message: Self::Request, - _: &ActorRef, - ) -> RequestResponse { - match message { - InMessage::GetCount => RequestResponse::Reply(OutMsg::Count(self.count)), - InMessage::Stop => RequestResponse::Stop(OutMsg::Count(self.count)), - } - } - - async fn handle_message( - &mut self, - _: Self::Message, - handle: &ActorRef, - ) -> MessageResponse { - self.count += 1; - println!("{:?}: good still alive", thread::current().id()); - send_after(Duration::from_millis(100), handle.to_owned(), Unused); - MessageResponse::NoReply - } + struct Counter { + count: u64, } - const BLOCKING: Backend = Backend::Blocking; - - #[test] - pub fn badly_behaved_thread_non_blocking() { - let runtime = rt::Runtime::new().unwrap(); - runtime.block_on(async move { - let mut badboy = BadlyBehavedTask.start(); - let _ = badboy.send(Unused).await; - let mut goodboy = WellBehavedTask { count: 0 }.start(); - let _ = goodboy.send(Unused).await; - rt::sleep(Duration::from_secs(1)).await; - let count = goodboy.request(InMessage::GetCount).await.unwrap(); - - match count { - OutMsg::Count(num) => { - assert_ne!(num, 10); - } - } - goodboy.request(InMessage::Stop).await.unwrap(); - }); + messages! { + GetCount -> u64; + Increment -> u64; + StopCounter -> u64 } - #[test] - pub fn badly_behaved_thread() { - let runtime = rt::Runtime::new().unwrap(); - runtime.block_on(async move { - let mut badboy = BadlyBehavedTask.start_with_backend(BLOCKING); - let _ = badboy.send(Unused).await; - let mut goodboy = WellBehavedTask { count: 0 }.start(); - let _ = goodboy.send(Unused).await; - rt::sleep(Duration::from_secs(1)).await; - let count = goodboy.request(InMessage::GetCount).await.unwrap(); - - match count { - OutMsg::Count(num) => { - assert_eq!(num, 10); - } - } - goodboy.request(InMessage::Stop).await.unwrap(); - }); - } - - const TIMEOUT_DURATION: Duration = Duration::from_millis(100); - - #[derive(Debug, Default)] - struct SomeTask; - - #[derive(Clone)] - enum SomeTaskRequest { - SlowOperation, - FastOperation, - } + impl Actor for Counter {} - impl Actor for SomeTask { - type Request = SomeTaskRequest; - type Message = Unused; - type Reply = Unused; - type Error = Unused; - - async fn handle_request( - &mut self, - message: Self::Request, - _handle: &ActorRef, - ) -> RequestResponse { - match message { - SomeTaskRequest::SlowOperation => { - // Simulate a slow operation that will not resolve in time - rt::sleep(TIMEOUT_DURATION * 2).await; - RequestResponse::Reply(Unused) - } - SomeTaskRequest::FastOperation => { - // Simulate a fast operation that resolves in time - rt::sleep(TIMEOUT_DURATION / 2).await; - RequestResponse::Reply(Unused) - } - } + impl Handler for Counter { + async fn handle(&mut self, _msg: GetCount, _ctx: &Context) -> u64 { + self.count } } - #[test] - pub fn unresolving_task_times_out() { - let runtime = rt::Runtime::new().unwrap(); - runtime.block_on(async move { - let mut unresolving_task = SomeTask.start(); - - let result = unresolving_task - .request_with_timeout(SomeTaskRequest::FastOperation, TIMEOUT_DURATION) - .await; - assert!(matches!(result, Ok(Unused))); - - let result = unresolving_task - .request_with_timeout(SomeTaskRequest::SlowOperation, TIMEOUT_DURATION) - .await; - assert!(matches!(result, Err(ActorError::RequestTimeout))); - }); - } - - struct SomeTaskThatFailsOnInit { - sender_channel: Arc>>, - } - - impl SomeTaskThatFailsOnInit { - pub fn new(sender_channel: Arc>>) -> Self { - Self { sender_channel } + impl Handler for Counter { + async fn handle(&mut self, _msg: Increment, _ctx: &Context) -> u64 { + self.count += 1; + self.count } } - impl Actor for SomeTaskThatFailsOnInit { - type Request = Unused; - type Message = Unused; - type Reply = Unused; - type Error = Unused; - - async fn init(self, _handle: &ActorRef) -> Result, Self::Error> { - // Simulate an initialization failure by returning NoSuccess - Ok(NoSuccess(self)) + impl Handler for Counter { + async fn handle(&mut self, _msg: StopCounter, ctx: &Context) -> u64 { + ctx.stop(); + self.count } - - async fn teardown(self, _handle: &ActorRef) -> Result<(), Self::Error> { - self.sender_channel.lock().unwrap().close(); - Ok(()) - } - } - - #[test] - pub fn task_fails_with_intermediate_state() { - let runtime = rt::Runtime::new().unwrap(); - runtime.block_on(async move { - let (rx, tx) = mpsc::channel::(); - let sender_channel = Arc::new(Mutex::new(tx)); - let _task = SomeTaskThatFailsOnInit::new(sender_channel).start(); - - // Wait a while to ensure the task has time to run and fail - rt::sleep(Duration::from_secs(1)).await; - - // We assure that the teardown function has ran by checking that the receiver channel is closed - assert!(rx.is_closed()) - }); } - // ==================== Backend enum tests ==================== - #[test] pub fn backend_default_is_async() { assert_eq!(Backend::default(), Backend::Async); @@ -746,8 +533,8 @@ mod tests { #[allow(clippy::clone_on_copy)] pub fn backend_enum_is_copy_and_clone() { let backend = Backend::Async; - let copied = backend; // Copy - let cloned = backend.clone(); // Clone - intentionally testing Clone trait + let copied = backend; + let cloned = backend.clone(); assert_eq!(backend, copied); assert_eq!(backend, cloned); } @@ -769,284 +556,183 @@ mod tests { assert_ne!(Backend::Blocking, Backend::Thread); } - // ==================== Backend functionality tests ==================== - - /// Simple counter Actor for testing all backends - struct Counter { - count: u64, - } - - #[derive(Clone)] - enum CounterRequest { - Get, - Increment, - Stop, - } - - #[derive(Clone)] - enum CounterMessage { - Increment, - } - - impl Actor for Counter { - type Request = CounterRequest; - type Message = CounterMessage; - type Reply = u64; - type Error = (); - - async fn handle_request( - &mut self, - message: Self::Request, - _: &ActorRef, - ) -> RequestResponse { - match message { - CounterRequest::Get => RequestResponse::Reply(self.count), - CounterRequest::Increment => { - self.count += 1; - RequestResponse::Reply(self.count) - } - CounterRequest::Stop => RequestResponse::Stop(self.count), - } - } - - async fn handle_message( - &mut self, - message: Self::Message, - _: &ActorRef, - ) -> MessageResponse { - match message { - CounterMessage::Increment => { - self.count += 1; - MessageResponse::NoReply - } - } - } - } - #[test] - pub fn backend_async_handles_call_and_cast() { + pub fn backend_async_handles_send_and_request() { let runtime = rt::Runtime::new().unwrap(); runtime.block_on(async move { - let mut counter = Counter { count: 0 }.start(); + let counter = Counter { count: 0 }.start(); - // Test call - let result = counter.request(CounterRequest::Get).await.unwrap(); + let result = counter.send_request(GetCount).await.unwrap(); assert_eq!(result, 0); - let result = counter.request(CounterRequest::Increment).await.unwrap(); + let result = counter.send_request(Increment).await.unwrap(); assert_eq!(result, 1); - // Test cast - counter.send(CounterMessage::Increment).await.unwrap(); - rt::sleep(Duration::from_millis(10)).await; // Give time for cast to process + // fire-and-forget send + counter.send(Increment).unwrap(); + rt::sleep(Duration::from_millis(10)).await; - let result = counter.request(CounterRequest::Get).await.unwrap(); + let result = counter.send_request(GetCount).await.unwrap(); assert_eq!(result, 2); - // Stop - let final_count = counter.request(CounterRequest::Stop).await.unwrap(); + let final_count = counter.send_request(StopCounter).await.unwrap(); assert_eq!(final_count, 2); }); } #[test] - pub fn backend_blocking_handles_call_and_cast() { + pub fn backend_blocking_handles_send_and_request() { let runtime = rt::Runtime::new().unwrap(); runtime.block_on(async move { - let mut counter = Counter { count: 0 }.start_with_backend(Backend::Blocking); + let counter = Counter { count: 0 }.start_with_backend(Backend::Blocking); - // Test call - let result = counter.request(CounterRequest::Get).await.unwrap(); + let result = counter.send_request(GetCount).await.unwrap(); assert_eq!(result, 0); - let result = counter.request(CounterRequest::Increment).await.unwrap(); + let result = counter.send_request(Increment).await.unwrap(); assert_eq!(result, 1); - // Test cast - counter.send(CounterMessage::Increment).await.unwrap(); - rt::sleep(Duration::from_millis(50)).await; // Give time for cast to process + counter.send(Increment).unwrap(); + rt::sleep(Duration::from_millis(50)).await; - let result = counter.request(CounterRequest::Get).await.unwrap(); + let result = counter.send_request(GetCount).await.unwrap(); assert_eq!(result, 2); - // Stop - let final_count = counter.request(CounterRequest::Stop).await.unwrap(); + let final_count = counter.send_request(StopCounter).await.unwrap(); assert_eq!(final_count, 2); }); } #[test] - pub fn backend_thread_handles_call_and_cast() { + pub fn backend_thread_handles_send_and_request() { let runtime = rt::Runtime::new().unwrap(); runtime.block_on(async move { - let mut counter = Counter { count: 0 }.start_with_backend(Backend::Thread); + let counter = Counter { count: 0 }.start_with_backend(Backend::Thread); - // Test call - let result = counter.request(CounterRequest::Get).await.unwrap(); + let result = counter.send_request(GetCount).await.unwrap(); assert_eq!(result, 0); - let result = counter.request(CounterRequest::Increment).await.unwrap(); + let result = counter.send_request(Increment).await.unwrap(); assert_eq!(result, 1); - // Test cast - counter.send(CounterMessage::Increment).await.unwrap(); - rt::sleep(Duration::from_millis(50)).await; // Give time for cast to process + counter.send(Increment).unwrap(); + rt::sleep(Duration::from_millis(50)).await; - let result = counter.request(CounterRequest::Get).await.unwrap(); + let result = counter.send_request(GetCount).await.unwrap(); assert_eq!(result, 2); - // Stop - let final_count = counter.request(CounterRequest::Stop).await.unwrap(); + let final_count = counter.send_request(StopCounter).await.unwrap(); assert_eq!(final_count, 2); }); } #[test] - pub fn backend_thread_isolates_blocking_work() { - // Similar to badly_behaved_thread but using Backend::Thread + pub fn multiple_backends_concurrent() { let runtime = rt::Runtime::new().unwrap(); runtime.block_on(async move { - let mut badboy = BadlyBehavedTask.start_with_backend(Backend::Thread); - let _ = badboy.send(Unused).await; - let mut goodboy = WellBehavedTask { count: 0 }.start(); - let _ = goodboy.send(Unused).await; - rt::sleep(Duration::from_secs(1)).await; - let count = goodboy.request(InMessage::GetCount).await.unwrap(); + let async_counter = Counter { count: 0 }.start(); + let blocking_counter = Counter { count: 100 }.start_with_backend(Backend::Blocking); + let thread_counter = Counter { count: 200 }.start_with_backend(Backend::Thread); - // goodboy should have run normally because badboy is on a separate thread - match count { - OutMsg::Count(num) => { - assert_eq!(num, 10); - } - } - goodboy.request(InMessage::Stop).await.unwrap(); + async_counter.send_request(Increment).await.unwrap(); + blocking_counter.send_request(Increment).await.unwrap(); + thread_counter.send_request(Increment).await.unwrap(); + + let async_val = async_counter.send_request(GetCount).await.unwrap(); + let blocking_val = blocking_counter.send_request(GetCount).await.unwrap(); + let thread_val = thread_counter.send_request(GetCount).await.unwrap(); + + assert_eq!(async_val, 1); + assert_eq!(blocking_val, 101); + assert_eq!(thread_val, 201); + + async_counter.send_request(StopCounter).await.unwrap(); + blocking_counter.send_request(StopCounter).await.unwrap(); + thread_counter.send_request(StopCounter).await.unwrap(); }); } #[test] - pub fn multiple_backends_concurrent() { + pub fn request_timeout() { let runtime = rt::Runtime::new().unwrap(); runtime.block_on(async move { - // Start counters on all three backends - let mut async_counter = Counter { count: 0 }.start(); - let mut blocking_counter = Counter { count: 100 }.start_with_backend(Backend::Blocking); - let mut thread_counter = Counter { count: 200 }.start_with_backend(Backend::Thread); - - // Increment each - async_counter - .request(CounterRequest::Increment) - .await - .unwrap(); - blocking_counter - .request(CounterRequest::Increment) - .await - .unwrap(); - thread_counter - .request(CounterRequest::Increment) - .await - .unwrap(); - - // Verify each has independent state - let async_val = async_counter.request(CounterRequest::Get).await.unwrap(); - let blocking_val = blocking_counter.request(CounterRequest::Get).await.unwrap(); - let thread_val = thread_counter.request(CounterRequest::Get).await.unwrap(); - - assert_eq!(async_val, 1); - assert_eq!(blocking_val, 101); - assert_eq!(thread_val, 201); + struct SlowActor; + messages! { SlowOp -> () } + impl Actor for SlowActor {} + impl Handler for SlowActor { + async fn handle(&mut self, _msg: SlowOp, _ctx: &Context) { + rt::sleep(Duration::from_millis(200)).await; + } + } - // Clean up - async_counter.request(CounterRequest::Stop).await.unwrap(); - blocking_counter - .request(CounterRequest::Stop) - .await - .unwrap(); - thread_counter.request(CounterRequest::Stop).await.unwrap(); + let actor = SlowActor.start(); + let result = actor + .send_request_with_timeout(SlowOp, Duration::from_millis(50)) + .await; + assert!(matches!(result, Err(ActorError::RequestTimeout))); }); } #[test] - pub fn backend_default_works_in_start() { + pub fn recipient_type_erasure() { let runtime = rt::Runtime::new().unwrap(); runtime.block_on(async move { - // Using Backend::default() should work the same as Backend::Async - let mut counter = Counter { count: 42 }.start_with_backend(Backend::Async); + let counter = Counter { count: 42 }.start(); + let recipient: Recipient = counter.recipient(); - let result = counter.request(CounterRequest::Get).await.unwrap(); + let rx = recipient.request(GetCount).unwrap(); + let result = rx.await.unwrap(); assert_eq!(result, 42); - counter.request(CounterRequest::Stop).await.unwrap(); + // Also test send_request helper + let result = send_request(&*recipient, GetCount, Duration::from_secs(5)).await.unwrap(); + assert_eq!(result, 42); }); } - /// Actor that sleeps during teardown to simulate slow shutdown + // --- SlowShutdownActor for join tests --- + struct SlowShutdownActor; + messages! { StopSlow -> () } + impl Actor for SlowShutdownActor { - type Request = Unused; - type Message = Unused; - type Reply = Unused; - type Error = Unused; - - async fn handle_message( - &mut self, - _message: Self::Message, - _handle: &ActorRef, - ) -> MessageResponse { - MessageResponse::Stop + async fn stopped(&mut self, _ctx: &Context) { + thread::sleep(Duration::from_millis(500)); } + } - async fn teardown(self, _handle: &ActorRef) -> Result<(), Self::Error> { - // Simulate slow shutdown - this runs on the thread - std::thread::sleep(Duration::from_millis(500)); - Ok(()) + impl Handler for SlowShutdownActor { + async fn handle(&mut self, _msg: StopSlow, ctx: &Context) { + ctx.stop(); } } - /// Test that join() on a Backend::Thread actor doesn't block other async tasks. - /// - /// This test verifies that when we call join().await on an actor running on - /// Backend::Thread, it doesn't block the tokio runtime - other async tasks - /// should continue to make progress. - /// - /// Uses a single-threaded runtime to ensure we detect blocking behavior. #[test] pub fn thread_backend_join_does_not_block_runtime() { - // Use current_thread runtime to ensure blocking would be detected let runtime = tokio::runtime::Builder::new_current_thread() .enable_all() .build() .unwrap(); runtime.block_on(async move { - // Start a thread-backend actor that takes 500ms to teardown - let mut slow_actor = SlowShutdownActor.start_with_backend(Backend::Thread); + let slow_actor = SlowShutdownActor.start_with_backend(Backend::Thread); - // Spawn an async task that increments a counter every 50ms - let tick_count = Arc::new(std::sync::atomic::AtomicU64::new(0)); + let tick_count = Arc::new(atomic::AtomicU64::new(0)); let tick_count_clone = tick_count.clone(); let _ticker = rt::spawn(async move { for _ in 0..20 { rt::sleep(Duration::from_millis(50)).await; - tick_count_clone.fetch_add(1, std::sync::atomic::Ordering::SeqCst); + tick_count_clone.fetch_add(1, atomic::Ordering::SeqCst); } }); - // Tell the actor to stop - it will start its slow teardown - slow_actor.send(Unused).await.unwrap(); - - // Small delay to ensure the actor received the message + slow_actor.send(StopSlow).unwrap(); rt::sleep(Duration::from_millis(10)).await; - // Now join the actor - this waits for the 500ms teardown - // If implemented correctly, the ticker should continue running DURING the join slow_actor.join().await; - // Check tick count IMMEDIATELY after join returns, before awaiting ticker. - // The actor teardown takes 500ms. In that time, the ticker should have - // completed about 10 ticks (500ms / 50ms = 10). - // If join() blocked the runtime, the ticker would have 0-1 ticks. - let count_after_join = tick_count.load(std::sync::atomic::Ordering::SeqCst); + let count_after_join = tick_count.load(atomic::Ordering::SeqCst); assert!( count_after_join >= 8, "Ticker should have completed ~10 ticks during the 500ms join(), but only got {}. \ @@ -1056,19 +742,14 @@ mod tests { }); } - /// Test that multiple callers can wait on join() simultaneously. - /// - /// This verifies that the completion signal approach works correctly - /// when multiple tasks want to wait for the same actor to stop. #[test] pub fn multiple_join_callers_all_notified() { let runtime = rt::Runtime::new().unwrap(); runtime.block_on(async move { - let mut actor = SlowShutdownActor.start(); + let actor = SlowShutdownActor.start(); let actor_clone1 = actor.clone(); let actor_clone2 = actor.clone(); - // Spawn multiple tasks that will all call join() let join1 = rt::spawn(async move { actor_clone1.join().await; 1u32 @@ -1078,19 +759,105 @@ mod tests { 2u32 }); - // Give the join tasks time to start waiting rt::sleep(Duration::from_millis(10)).await; - // Tell the actor to stop - actor.send(Unused).await.unwrap(); + actor.send(StopSlow).unwrap(); - // All join tasks should complete after the actor stops let (r1, r2) = tokio::join!(join1, join2); assert_eq!(r1.unwrap(), 1); assert_eq!(r2.unwrap(), 2); - // Calling join again should return immediately (actor already stopped) actor.join().await; }); } + + // --- Badly behaved actors for blocking tests --- + + struct BadlyBehavedTask; + + messages! { DoBlock -> () } + + impl Actor for BadlyBehavedTask {} + + impl Handler for BadlyBehavedTask { + async fn handle(&mut self, _msg: DoBlock, ctx: &Context) { + rt::sleep(Duration::from_millis(20)).await; + thread::sleep(Duration::from_secs(2)); + ctx.stop(); + } + } + + messages! { IncrementWell -> () } + + struct WellBehavedTask { + pub count: u64, + } + + impl Actor for WellBehavedTask {} + + impl Handler for WellBehavedTask { + async fn handle(&mut self, _msg: GetCount, _ctx: &Context) -> u64 { + self.count + } + } + + impl Handler for WellBehavedTask { + async fn handle(&mut self, _msg: StopCounter, ctx: &Context) -> u64 { + ctx.stop(); + self.count + } + } + + impl Handler for WellBehavedTask { + async fn handle(&mut self, _msg: IncrementWell, ctx: &Context) { + self.count += 1; + use crate::tasks::send_after; + send_after(Duration::from_millis(100), ctx.clone(), IncrementWell); + } + } + + #[test] + pub fn badly_behaved_thread_non_blocking() { + let runtime = rt::Runtime::new().unwrap(); + runtime.block_on(async move { + let badboy = BadlyBehavedTask.start(); + badboy.send(DoBlock).unwrap(); + let goodboy = WellBehavedTask { count: 0 }.start(); + goodboy.send(IncrementWell).unwrap(); + rt::sleep(Duration::from_secs(1)).await; + let count = goodboy.send_request(GetCount).await.unwrap(); + assert_ne!(count, 10); + goodboy.send_request(StopCounter).await.unwrap(); + }); + } + + #[test] + pub fn badly_behaved_thread() { + let runtime = rt::Runtime::new().unwrap(); + runtime.block_on(async move { + let badboy = BadlyBehavedTask.start_with_backend(Backend::Blocking); + badboy.send(DoBlock).unwrap(); + let goodboy = WellBehavedTask { count: 0 }.start(); + goodboy.send(IncrementWell).unwrap(); + rt::sleep(Duration::from_secs(1)).await; + let count = goodboy.send_request(GetCount).await.unwrap(); + assert_eq!(count, 10); + goodboy.send_request(StopCounter).await.unwrap(); + }); + } + + #[test] + pub fn backend_thread_isolates_blocking_work() { + let runtime = rt::Runtime::new().unwrap(); + runtime.block_on(async move { + let badboy = BadlyBehavedTask.start_with_backend(Backend::Thread); + badboy.send(DoBlock).unwrap(); + let goodboy = WellBehavedTask { count: 0 }.start(); + goodboy.send(IncrementWell).unwrap(); + rt::sleep(Duration::from_secs(1)).await; + let count = goodboy.send_request(GetCount).await.unwrap(); + assert_eq!(count, 10); + goodboy.send_request(StopCounter).await.unwrap(); + }); + } } diff --git a/concurrency/src/tasks/mod.rs b/concurrency/src/tasks/mod.rs index dbbc269..365c594 100644 --- a/concurrency/src/tasks/mod.rs +++ b/concurrency/src/tasks/mod.rs @@ -1,7 +1,4 @@ -//! spawned concurrency -//! Runtime tasks-based traits and structs to implement concurrent code à-la-Erlang. - -mod actor; +pub(crate) mod actor; mod process; mod stream; mod time; @@ -12,9 +9,9 @@ mod stream_tests; mod timer_tests; pub use actor::{ - send_message_on, Actor, ActorInMsg, ActorRef, Backend, InitResult, InitResult::NoSuccess, - InitResult::Success, MessageResponse, RequestResponse, + send_message_on, Actor, ActorRef, ActorStart, Backend, Context, Handler, Receiver, Recipient, + send_request, }; pub use process::{send, Process, ProcessInfo}; pub use stream::spawn_listener; -pub use time::{send_after, send_interval}; +pub use time::{send_after, send_interval, TimerHandle}; diff --git a/concurrency/src/tasks/stream.rs b/concurrency/src/tasks/stream.rs index ebf09a3..afc5ce6 100644 --- a/concurrency/src/tasks/stream.rs +++ b/concurrency/src/tasks/stream.rs @@ -1,26 +1,23 @@ -use crate::tasks::{Actor, ActorRef}; +use crate::message::Message; use futures::{future::select, Stream, StreamExt}; use spawned_rt::tasks::JoinHandle; -/// Spawns a listener that listens to a stream and sends messages to an Actor. -/// -/// Items sent through the stream are required to be wrapped in a Result type. -/// -/// This function returns a handle to the spawned task and a cancellation token -/// to stop it. -pub fn spawn_listener(mut handle: ActorRef, stream: S) -> JoinHandle<()> +use super::actor::{Actor, Context, Handler}; + +pub fn spawn_listener(ctx: Context, stream: S) -> JoinHandle<()> where - T: Actor, - S: Send + Stream + 'static, + A: Actor + Handler, + M: Message, + S: Send + Stream + 'static, { - let cancellation_token = handle.cancellation_token(); + let cancellation_token = ctx.cancellation_token(); let join_handle = spawned_rt::tasks::spawn(async move { let mut pinned_stream = core::pin::pin!(stream); let is_cancelled = core::pin::pin!(cancellation_token.cancelled()); let listener_loop = core::pin::pin!(async { loop { match pinned_stream.next().await { - Some(msg) => match handle.send(msg).await { + Some(msg) => match ctx.send(msg) { Ok(_) => tracing::trace!("Message sent successfully"), Err(e) => { tracing::error!("Failed to send message: {e:?}"); @@ -36,7 +33,7 @@ where }); match select(is_cancelled, listener_loop).await { futures::future::Either::Left(_) => tracing::trace!("Actor stopped"), - futures::future::Either::Right(_) => (), // Stream finished or errored out + futures::future::Either::Right(_) => (), } }); join_handle diff --git a/concurrency/src/tasks/stream_tests.rs b/concurrency/src/tasks/stream_tests.rs index d270002..69c5a6f 100644 --- a/concurrency/src/tasks/stream_tests.rs +++ b/concurrency/src/tasks/stream_tests.rs @@ -1,11 +1,30 @@ use crate::tasks::{ - send_after, stream::spawn_listener, Actor, ActorRef, MessageResponse, RequestResponse, + send_after, Actor, ActorStart, Context, Handler, + stream::spawn_listener, }; +use crate::message::Message; use futures::{stream, StreamExt}; use spawned_rt::tasks::{self as rt, BroadcastStream, ReceiverStream}; use std::time::Duration; -type SummatoryHandle = ActorRef; +// --- Messages --- + +#[derive(Debug)] +enum StreamMsg { + Add(u16), + Error, +} +impl Message for StreamMsg { type Result = (); } + +#[derive(Debug)] +struct StopSum; +impl Message for StopSum { type Result = (); } + +#[derive(Debug)] +struct GetValue; +impl Message for GetValue { type Result = u16; } + +// --- Summatory Actor --- struct Summatory { count: u16, @@ -17,49 +36,26 @@ impl Summatory { } } -type SummatoryOutMessage = u16; - -#[derive(Clone)] -enum SummatoryCastMessage { - Add(u16), - StreamError, - Stop, -} +impl Actor for Summatory {} -impl Summatory { - pub async fn get_value(server: &mut SummatoryHandle) -> Result { - server.request(()).await.map_err(|_| ()) +impl Handler for Summatory { + async fn handle(&mut self, msg: StreamMsg, ctx: &Context) { + match msg { + StreamMsg::Add(val) => self.count += val, + StreamMsg::Error => ctx.stop(), + } } } -impl Actor for Summatory { - type Request = (); // We only handle one type of call, so there is no need for a specific message type. - type Message = SummatoryCastMessage; - type Reply = SummatoryOutMessage; - type Error = (); - - async fn handle_message( - &mut self, - message: Self::Message, - _handle: &ActorRef, - ) -> MessageResponse { - match message { - SummatoryCastMessage::Add(val) => { - self.count += val; - MessageResponse::NoReply - } - SummatoryCastMessage::StreamError => MessageResponse::Stop, - SummatoryCastMessage::Stop => MessageResponse::Stop, - } +impl Handler for Summatory { + async fn handle(&mut self, _msg: StopSum, ctx: &Context) { + ctx.stop(); } +} - async fn handle_request( - &mut self, - _message: Self::Request, - _handle: &SummatoryHandle, - ) -> RequestResponse { - let current_value = self.count; - RequestResponse::Reply(current_value) +impl Handler for Summatory { + async fn handle(&mut self, _msg: GetValue, _ctx: &Context) -> u16 { + self.count } } @@ -67,18 +63,18 @@ impl Actor for Summatory { pub fn test_sum_numbers_from_stream() { let runtime = rt::Runtime::new().unwrap(); runtime.block_on(async move { - let mut summatory_handle = Summatory::new(0).start(); + let summatory = Summatory::new(0).start(); let stream = stream::iter(vec![1u16, 2, 3, 4, 5].into_iter().map(Ok::)); + let ctx = Context::from_ref(&summatory); spawn_listener( - summatory_handle.clone(), - stream.filter_map(|result| async move { result.ok().map(SummatoryCastMessage::Add) }), + ctx, + stream.filter_map(|result| async move { result.ok().map(StreamMsg::Add) }), ); - // Wait for 1 second so the whole stream is processed rt::sleep(Duration::from_secs(1)).await; - let val = Summatory::get_value(&mut summatory_handle).await.unwrap(); + let val = summatory.send_request(GetValue).await.unwrap(); assert_eq!(val, 15); }) } @@ -87,26 +83,25 @@ pub fn test_sum_numbers_from_stream() { pub fn test_sum_numbers_from_channel() { let runtime = rt::Runtime::new().unwrap(); runtime.block_on(async move { - let mut summatory_handle = Summatory::new(0).start(); + let summatory = Summatory::new(0).start(); let (tx, rx) = spawned_rt::tasks::mpsc::channel::>(); - // Spawn a task to send numbers to the channel spawned_rt::tasks::spawn(async move { for i in 1..=5 { tx.send(Ok(i)).unwrap(); } }); + let ctx = Context::from_ref(&summatory); spawn_listener( - summatory_handle.clone(), + ctx, ReceiverStream::new(rx) - .filter_map(|result| async move { result.ok().map(SummatoryCastMessage::Add) }), + .filter_map(|result| async move { result.ok().map(StreamMsg::Add) }), ); - // Wait for 1 second so the whole stream is processed rt::sleep(Duration::from_secs(1)).await; - let val = Summatory::get_value(&mut summatory_handle).await.unwrap(); + let val = summatory.send_request(GetValue).await.unwrap(); assert_eq!(val, 15); }) } @@ -115,44 +110,40 @@ pub fn test_sum_numbers_from_channel() { pub fn test_sum_numbers_from_broadcast_channel() { let runtime = rt::Runtime::new().unwrap(); runtime.block_on(async move { - let mut summatory_handle = Summatory::new(0).start(); + let summatory = Summatory::new(0).start(); let (tx, rx) = tokio::sync::broadcast::channel::(5); - // Spawn a task to send numbers to the channel spawned_rt::tasks::spawn(async move { for i in 1u16..=5 { tx.send(i).unwrap(); } }); + let ctx = Context::from_ref(&summatory); spawn_listener( - summatory_handle.clone(), + ctx, BroadcastStream::new(rx) - .filter_map(|result| async move { result.ok().map(SummatoryCastMessage::Add) }), + .filter_map(|result| async move { result.ok().map(StreamMsg::Add) }), ); - // Wait for 1 second so the whole stream is processed rt::sleep(Duration::from_secs(1)).await; - let val = Summatory::get_value(&mut summatory_handle).await.unwrap(); + let val = summatory.send_request(GetValue).await.unwrap(); assert_eq!(val, 15); }) } #[test] pub fn test_stream_cancellation() { - // Messages sent at: t=0, t=250, t=500, t=750, t=1000ms - // We read at t=850ms (after 4th message at t=750, before 5th at t=1000) const MESSAGE_INTERVAL: u64 = 250; const READ_TIME: u64 = 850; const STOP_TIME: u64 = 1100; let runtime = rt::Runtime::new().unwrap(); runtime.block_on(async move { - let mut summatory_handle = Summatory::new(0).start(); + let summatory = Summatory::new(0).start(); let (tx, rx) = spawned_rt::tasks::mpsc::channel::>(); - // Spawn a task to send numbers to the channel spawned_rt::tasks::spawn(async move { for i in 1..=5 { tx.send(Ok(i)).unwrap(); @@ -160,34 +151,28 @@ pub fn test_stream_cancellation() { } }); + let ctx = Context::from_ref(&summatory); let listener_handle = spawn_listener( - summatory_handle.clone(), + ctx.clone(), ReceiverStream::new(rx) - .filter_map(|result| async move { result.ok().map(SummatoryCastMessage::Add) }), + .filter_map(|result| async move { result.ok().map(StreamMsg::Add) }), ); - // Start a timer to stop the actor after all messages would be sent - let summatory_handle_clone = summatory_handle.clone(); let _ = send_after( Duration::from_millis(STOP_TIME), - summatory_handle_clone, - SummatoryCastMessage::Stop, + ctx, + StopSum, ); - // Read value after 4th message (t=750) but before 5th (t=1000). - // Expected sum: 1+2+3+4 = 10, but allow some slack for timing variations. rt::sleep(Duration::from_millis(READ_TIME)).await; - let val = Summatory::get_value(&mut summatory_handle).await.unwrap(); + let val = summatory.send_request(GetValue).await.unwrap(); - // At t=850ms, we expect 4 messages processed (sum=10), but timing variations - // could result in 3 messages (sum=6) or occasionally all 5 (sum=15). assert!((1..=15).contains(&val)); assert!(listener_handle.await.is_ok()); - // Finally, we check that the server is stopped, by getting an error when trying to call it. rt::sleep(Duration::from_millis(10)).await; - assert!(Summatory::get_value(&mut summatory_handle).await.is_err()); + assert!(summatory.send_request(GetValue).await.is_err()); }) } @@ -195,22 +180,21 @@ pub fn test_stream_cancellation() { pub fn test_halting_on_stream_error() { let runtime = rt::Runtime::new().unwrap(); runtime.block_on(async move { - let mut summatory_handle = Summatory::new(0).start(); + let summatory = Summatory::new(0).start(); let stream = tokio_stream::iter(vec![Ok(1u16), Ok(2), Ok(3), Err(()), Ok(4), Ok(5)]); let msg_stream = stream.filter_map(|value| async move { match value { - Ok(number) => Some(SummatoryCastMessage::Add(number)), - Err(_) => Some(SummatoryCastMessage::StreamError), + Ok(number) => Some(StreamMsg::Add(number)), + Err(_) => Some(StreamMsg::Error), } }); - spawn_listener(summatory_handle.clone(), msg_stream); + let ctx = Context::from_ref(&summatory); + spawn_listener(ctx, msg_stream); - // Wait for 1 second so the whole stream is processed rt::sleep(Duration::from_secs(1)).await; - let result = Summatory::get_value(&mut summatory_handle).await; - // Actor should have been terminated, hence the result should be an error + let result = summatory.send_request(GetValue).await; assert!(result.is_err()); }) } @@ -219,21 +203,21 @@ pub fn test_halting_on_stream_error() { pub fn test_skipping_on_stream_error() { let runtime = rt::Runtime::new().unwrap(); runtime.block_on(async move { - let mut summatory_handle = Summatory::new(0).start(); + let summatory = Summatory::new(0).start(); let stream = tokio_stream::iter(vec![Ok(1u16), Ok(2), Ok(3), Err(()), Ok(4), Ok(5)]); let msg_stream = stream.filter_map(|value| async move { match value { - Ok(number) => Some(SummatoryCastMessage::Add(number)), + Ok(number) => Some(StreamMsg::Add(number)), Err(_) => None, } }); - spawn_listener(summatory_handle.clone(), msg_stream); + let ctx = Context::from_ref(&summatory); + spawn_listener(ctx, msg_stream); - // Wait for 1 second so the whole stream is processed rt::sleep(Duration::from_secs(1)).await; - let val = Summatory::get_value(&mut summatory_handle).await.unwrap(); + let val = summatory.send_request(GetValue).await.unwrap(); assert_eq!(val, 15); }) } diff --git a/concurrency/src/tasks/time.rs b/concurrency/src/tasks/time.rs index e334c81..69871e8 100644 --- a/concurrency/src/tasks/time.rs +++ b/concurrency/src/tasks/time.rs @@ -3,7 +3,8 @@ use std::time::Duration; use spawned_rt::tasks::{self as rt, CancellationToken, JoinHandle}; -use super::{Actor, ActorRef}; +use super::actor::{Actor, Context, Handler}; +use crate::message::Message; use core::pin::pin; pub struct TimerHandle { @@ -11,24 +12,22 @@ pub struct TimerHandle { pub cancellation_token: CancellationToken, } -// Sends a message after a given period to the specified Actor. The task terminates -// once the send has completed -pub fn send_after(period: Duration, mut handle: ActorRef, message: T::Message) -> TimerHandle +pub fn send_after(period: Duration, ctx: Context, msg: M) -> TimerHandle where - T: Actor + 'static, + A: Actor + Handler, + M: Message, { let cancellation_token = CancellationToken::new(); let cloned_token = cancellation_token.clone(); - let actor_cancellation_token = handle.cancellation_token(); + let actor_cancellation_token = ctx.cancellation_token(); let join_handle = rt::spawn(async move { - // Timer action is ignored if it was either cancelled or the associated Actor is no longer running. let cancel_token_fut = pin!(cloned_token.cancelled()); let actor_cancel_fut = pin!(actor_cancellation_token.cancelled()); let cancel_conditions = select(cancel_token_fut, actor_cancel_fut); let async_block = pin!(async { rt::sleep(period).await; - let _ = handle.send(message.clone()).await; + let _ = ctx.send(msg); }); let _ = select(cancel_conditions, async_block).await; }); @@ -38,28 +37,24 @@ where } } -// Sends a message to the specified Actor repeatedly after `Time` milliseconds. -pub fn send_interval( - period: Duration, - mut handle: ActorRef, - message: T::Message, -) -> TimerHandle +pub fn send_interval(period: Duration, ctx: Context, msg: M) -> TimerHandle where - T: Actor + 'static, + A: Actor + Handler, + M: Message + Clone, { let cancellation_token = CancellationToken::new(); let cloned_token = cancellation_token.clone(); - let actor_cancellation_token = handle.cancellation_token(); + let actor_cancellation_token = ctx.cancellation_token(); let join_handle = rt::spawn(async move { loop { - // Timer action is ignored if it was either cancelled or the associated Actor is no longer running. let cancel_token_fut = pin!(cloned_token.cancelled()); let actor_cancel_fut = pin!(actor_cancellation_token.cancelled()); let cancel_conditions = select(cancel_token_fut, actor_cancel_fut); + let msg_clone = msg.clone(); let async_block = pin!(async { rt::sleep(period).await; - let _ = handle.send(message.clone()).await; + let _ = ctx.send(msg_clone); }); let result = select(cancel_conditions, async_block).await; match result { diff --git a/concurrency/src/tasks/timer_tests.rs b/concurrency/src/tasks/timer_tests.rs index 46eb664..205d5ba 100644 --- a/concurrency/src/tasks/timer_tests.rs +++ b/concurrency/src/tasks/timer_tests.rs @@ -1,31 +1,27 @@ use super::{ - send_after, send_interval, Actor, ActorRef, InitResult, InitResult::Success, MessageResponse, - RequestResponse, + send_after, send_interval, Actor, ActorStart, Context, Handler, }; +use crate::message::Message; use spawned_rt::tasks::{self as rt, CancellationToken}; use std::time::Duration; -type RepeaterHandle = ActorRef; +// --- Repeater (interval timer test) --- -#[derive(Clone)] -enum RepeaterCastMessage { - Inc, - StopTimer, -} +#[derive(Clone, Debug)] +struct Inc; +impl Message for Inc { type Result = (); } -#[derive(Clone)] -enum RepeaterCallMessage { - GetCount, -} +#[derive(Clone, Debug)] +struct StopTimer; +impl Message for StopTimer { type Result = (); } -#[derive(PartialEq, Debug)] -enum RepeaterOutMessage { - Count(i32), -} +#[derive(Debug)] +struct GetRepCount; +impl Message for GetRepCount { type Result = i32; } struct Repeater { - pub(crate) count: i32, - pub(crate) cancellation_token: Option, + count: i32, + cancellation_token: Option, } impl Repeater { @@ -37,63 +33,34 @@ impl Repeater { } } -impl Repeater { - pub async fn stop_timer(server: &mut RepeaterHandle) -> Result<(), ()> { - server - .send(RepeaterCastMessage::StopTimer) - .await - .map_err(|_| ()) - } - - pub async fn get_count(server: &mut RepeaterHandle) -> Result { - server - .request(RepeaterCallMessage::GetCount) - .await - .map_err(|_| ()) - } -} - impl Actor for Repeater { - type Request = RepeaterCallMessage; - type Message = RepeaterCastMessage; - type Reply = RepeaterOutMessage; - type Error = (); - - async fn init(mut self, handle: &RepeaterHandle) -> Result, Self::Error> { + async fn started(&mut self, ctx: &Context) { let timer = send_interval( Duration::from_millis(100), - handle.clone(), - RepeaterCastMessage::Inc, + ctx.clone(), + Inc, ); self.cancellation_token = Some(timer.cancellation_token); - Ok(Success(self)) } +} + +impl Handler for Repeater { + async fn handle(&mut self, _msg: Inc, _ctx: &Context) { + self.count += 1; + } +} - async fn handle_request( - &mut self, - _message: Self::Request, - _handle: &RepeaterHandle, - ) -> RequestResponse { - let count = self.count; - RequestResponse::Reply(RepeaterOutMessage::Count(count)) +impl Handler for Repeater { + async fn handle(&mut self, _msg: StopTimer, _ctx: &Context) { + if let Some(ct) = self.cancellation_token.clone() { + ct.cancel(); + } } +} - async fn handle_message( - &mut self, - message: Self::Message, - _handle: &ActorRef, - ) -> MessageResponse { - match message { - RepeaterCastMessage::Inc => { - self.count += 1; - } - RepeaterCastMessage::StopTimer => { - if let Some(ct) = self.cancellation_token.clone() { - ct.cancel() - }; - } - }; - MessageResponse::NoReply +impl Handler for Repeater { + async fn handle(&mut self, _msg: GetRepCount, _ctx: &Context) -> i32 { + self.count } } @@ -101,109 +68,60 @@ impl Actor for Repeater { pub fn test_send_interval_and_cancellation() { let runtime = rt::Runtime::new().unwrap(); runtime.block_on(async move { - // Start a Repeater - let mut repeater = Repeater::new(0).start(); + let repeater = Repeater::new(0).start(); - // Wait for 1 second rt::sleep(Duration::from_secs(1)).await; - // Check count - let count = Repeater::get_count(&mut repeater).await.unwrap(); + let count = repeater.send_request(GetRepCount).await.unwrap(); + assert_eq!(9, count); - // 9 messages in 1 second (after first 100 milliseconds sleep) - assert_eq!(RepeaterOutMessage::Count(9), count); + repeater.send(StopTimer).unwrap(); - // Pause timer - Repeater::stop_timer(&mut repeater).await.unwrap(); - - // Wait another second rt::sleep(Duration::from_secs(1)).await; - // Check count again - let count2 = Repeater::get_count(&mut repeater).await.unwrap(); - - // As timer was paused, count should remain at 9 - assert_eq!(RepeaterOutMessage::Count(9), count2); + let count2 = repeater.send_request(GetRepCount).await.unwrap(); + assert_eq!(9, count2); }); } -type DelayedHandle = ActorRef; - -#[derive(Clone)] -enum DelayedCastMessage { - Inc, -} +// --- Delayed (send_after test) --- -#[derive(Clone)] -enum DelayedCallMessage { - GetCount, - Stop, -} +#[derive(Debug)] +struct GetDelCount; +impl Message for GetDelCount { type Result = i32; } -#[derive(PartialEq, Debug)] -enum DelayedOutMessage { - Count(i32), -} +#[derive(Debug)] +struct StopDelayed; +impl Message for StopDelayed { type Result = i32; } struct Delayed { - pub(crate) count: i32, + count: i32, } impl Delayed { pub fn new(initial_count: i32) -> Self { - Delayed { - count: initial_count, - } + Delayed { count: initial_count } } } -impl Delayed { - pub async fn get_count(server: &mut DelayedHandle) -> Result { - server - .request(DelayedCallMessage::GetCount) - .await - .map_err(|_| ()) - } +impl Actor for Delayed {} - pub async fn stop(server: &mut DelayedHandle) -> Result { - server - .request(DelayedCallMessage::Stop) - .await - .map_err(|_| ()) +impl Handler for Delayed { + async fn handle(&mut self, _msg: Inc, _ctx: &Context) { + self.count += 1; } } -impl Actor for Delayed { - type Request = DelayedCallMessage; - type Message = DelayedCastMessage; - type Reply = DelayedOutMessage; - type Error = (); - - async fn handle_request( - &mut self, - message: Self::Request, - _handle: &DelayedHandle, - ) -> RequestResponse { - match message { - DelayedCallMessage::GetCount => { - let count = self.count; - RequestResponse::Reply(DelayedOutMessage::Count(count)) - } - DelayedCallMessage::Stop => RequestResponse::Stop(DelayedOutMessage::Count(self.count)), - } +impl Handler for Delayed { + async fn handle(&mut self, _msg: GetDelCount, _ctx: &Context) -> i32 { + self.count } +} - async fn handle_message( - &mut self, - message: Self::Message, - _handle: &DelayedHandle, - ) -> MessageResponse { - match message { - DelayedCastMessage::Inc => { - self.count += 1; - } - }; - MessageResponse::NoReply +impl Handler for Delayed { + async fn handle(&mut self, _msg: StopDelayed, ctx: &Context) -> i32 { + ctx.stop(); + self.count } } @@ -211,43 +129,33 @@ impl Actor for Delayed { pub fn test_send_after_and_cancellation() { let runtime = rt::Runtime::new().unwrap(); runtime.block_on(async move { - // Start a Delayed - let mut repeater = Delayed::new(0).start(); + let repeater = Delayed::new(0).start(); - // Set a just once timed message + let ctx = Context::from_ref(&repeater); let _ = send_after( Duration::from_millis(100), - repeater.clone(), - DelayedCastMessage::Inc, + ctx, + Inc, ); - // Wait for 200 milliseconds rt::sleep(Duration::from_millis(200)).await; - // Check count - let count = Delayed::get_count(&mut repeater).await.unwrap(); + let count = repeater.send_request(GetDelCount).await.unwrap(); + assert_eq!(1, count); - // Only one message (no repetition) - assert_eq!(DelayedOutMessage::Count(1), count); - - // New timer + let ctx = Context::from_ref(&repeater); let timer = send_after( Duration::from_millis(100), - repeater.clone(), - DelayedCastMessage::Inc, + ctx, + Inc, ); - // Cancel the new timer before timeout timer.cancellation_token.cancel(); - // Wait another 200 milliseconds rt::sleep(Duration::from_millis(200)).await; - // Check count again - let count2 = Delayed::get_count(&mut repeater).await.unwrap(); - - // As timer was cancelled, count should remain at 1 - assert_eq!(DelayedOutMessage::Count(1), count2); + let count2 = repeater.send_request(GetDelCount).await.unwrap(); + assert_eq!(1, count2); }); } @@ -255,39 +163,31 @@ pub fn test_send_after_and_cancellation() { pub fn test_send_after_gen_server_teardown() { let runtime = rt::Runtime::new().unwrap(); runtime.block_on(async move { - // Start a Delayed - let mut repeater = Delayed::new(0).start(); + let repeater = Delayed::new(0).start(); - // Set a just once timed message + let ctx = Context::from_ref(&repeater); let _ = send_after( Duration::from_millis(100), - repeater.clone(), - DelayedCastMessage::Inc, + ctx, + Inc, ); - // Wait for 200 milliseconds rt::sleep(Duration::from_millis(200)).await; - // Check count - let count = Delayed::get_count(&mut repeater).await.unwrap(); - - // Only one message (no repetition) - assert_eq!(DelayedOutMessage::Count(1), count); + let count = repeater.send_request(GetDelCount).await.unwrap(); + assert_eq!(1, count); - // New timer + let ctx = Context::from_ref(&repeater); let _ = send_after( Duration::from_millis(100), - repeater.clone(), - DelayedCastMessage::Inc, + ctx, + Inc, ); - // Stop the Actor before timeout - let count2 = Delayed::stop(&mut repeater).await.unwrap(); + let count2 = repeater.send_request(StopDelayed).await.unwrap(); - // Wait another 200 milliseconds rt::sleep(Duration::from_millis(200)).await; - // As timer was cancelled, count should remain at 1 - assert_eq!(DelayedOutMessage::Count(1), count2); + assert_eq!(1, count2); }); } diff --git a/concurrency/src/threads/actor.rs b/concurrency/src/threads/actor.rs index 04796b7..978440d 100644 --- a/concurrency/src/threads/actor.rs +++ b/concurrency/src/threads/actor.rs @@ -1,21 +1,180 @@ -//! Actor trait and structs to create an abstraction similar to Erlang gen_server. -//! See examples/name_server for a usage example. +use crate::error::ActorError; +use crate::message::Message; use spawned_rt::threads::{ self as rt, mpsc, oneshot, oneshot::RecvTimeoutError, CancellationToken, }; use std::{ - fmt::Debug, panic::{catch_unwind, AssertUnwindSafe}, sync::{Arc, Condvar, Mutex}, time::Duration, }; -use crate::error::ActorError; - const DEFAULT_REQUEST_TIMEOUT: Duration = Duration::from_secs(5); -/// Guard that signals completion when dropped. -/// Ensures waiters are notified even if the actor thread panics. +// --------------------------------------------------------------------------- +// Actor trait +// --------------------------------------------------------------------------- + +pub trait Actor: Send + Sized + 'static { + fn started(&mut self, _ctx: &Context) {} + fn stopped(&mut self, _ctx: &Context) {} +} + +// --------------------------------------------------------------------------- +// Handler trait (per-message, sync version) +// --------------------------------------------------------------------------- + +pub trait Handler: Actor { + fn handle(&mut self, msg: M, ctx: &Context) -> M::Result; +} + +// --------------------------------------------------------------------------- +// Envelope (type-erasure on the actor side) +// --------------------------------------------------------------------------- + +trait Envelope: Send { + fn handle(self: Box, actor: &mut A, ctx: &Context); +} + +struct MessageEnvelope { + msg: M, + tx: Option>, +} + +impl Envelope for MessageEnvelope +where + A: Actor + Handler, + M: Message, +{ + fn handle(self: Box, actor: &mut A, ctx: &Context) { + let result = actor.handle(self.msg, ctx); + if let Some(tx) = self.tx { + let _ = tx.send(result); + } + } +} + +// --------------------------------------------------------------------------- +// Context +// --------------------------------------------------------------------------- + +pub struct Context { + sender: mpsc::Sender + Send>>, + cancellation_token: CancellationToken, +} + +impl Clone for Context { + fn clone(&self) -> Self { + Self { + sender: self.sender.clone(), + cancellation_token: self.cancellation_token.clone(), + } + } +} + +impl std::fmt::Debug for Context { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.debug_struct("Context").finish_non_exhaustive() + } +} + +impl Context { + pub fn from_ref(actor_ref: &ActorRef) -> Self { + Self { + sender: actor_ref.sender.clone(), + cancellation_token: actor_ref.cancellation_token.clone(), + } + } + + pub fn stop(&self) { + self.cancellation_token.cancel(); + } + + pub fn send(&self, msg: M) -> Result<(), ActorError> + where + A: Handler, + M: Message, + { + let envelope = MessageEnvelope { msg, tx: None }; + self.sender + .send(Box::new(envelope)) + .map_err(|_| ActorError::ActorStopped) + } + + pub fn request(&self, msg: M) -> Result, ActorError> + where + A: Handler, + M: Message, + { + let (tx, rx) = oneshot::channel(); + let envelope = MessageEnvelope { + msg, + tx: Some(tx), + }; + self.sender + .send(Box::new(envelope)) + .map_err(|_| ActorError::ActorStopped)?; + Ok(rx) + } + + pub fn send_request(&self, msg: M) -> Result + where + A: Handler, + M: Message, + { + self.send_request_with_timeout(msg, DEFAULT_REQUEST_TIMEOUT) + } + + pub fn send_request_with_timeout( + &self, + msg: M, + duration: Duration, + ) -> Result + where + A: Handler, + M: Message, + { + let rx = self.request(msg)?; + match rx.recv_timeout(duration) { + Ok(result) => Ok(result), + Err(RecvTimeoutError::Timeout) => Err(ActorError::RequestTimeout), + Err(RecvTimeoutError::Disconnected) => Err(ActorError::ActorStopped), + } + } + + pub(crate) fn cancellation_token(&self) -> CancellationToken { + self.cancellation_token.clone() + } +} + +// --------------------------------------------------------------------------- +// Receiver trait (object-safe) + Recipient alias +// --------------------------------------------------------------------------- + +pub trait Receiver: Send + Sync { + fn send(&self, msg: M) -> Result<(), ActorError>; + fn request(&self, msg: M) -> Result, ActorError>; +} + +pub type Recipient = Arc>; + +pub fn send_request( + recipient: &dyn Receiver, + msg: M, + timeout: Duration, +) -> Result { + let rx = recipient.request(msg)?; + match rx.recv_timeout(timeout) { + Ok(result) => Ok(result), + Err(RecvTimeoutError::Timeout) => Err(ActorError::RequestTimeout), + Err(RecvTimeoutError::Disconnected) => Err(ActorError::ActorStopped), + } +} + +// --------------------------------------------------------------------------- +// ActorRef +// --------------------------------------------------------------------------- + struct CompletionGuard(Arc<(Mutex, Condvar)>); impl Drop for CompletionGuard { @@ -27,94 +186,93 @@ impl Drop for CompletionGuard { } } -pub struct ActorRef { - pub tx: mpsc::Sender>, +pub struct ActorRef { + sender: mpsc::Sender + Send>>, cancellation_token: CancellationToken, - /// Completion signal: (is_completed, condvar for waiters) completion: Arc<(Mutex, Condvar)>, } +impl std::fmt::Debug for ActorRef { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.debug_struct("ActorRef").finish_non_exhaustive() + } +} + impl Clone for ActorRef { fn clone(&self) -> Self { Self { - tx: self.tx.clone(), + sender: self.sender.clone(), cancellation_token: self.cancellation_token.clone(), completion: self.completion.clone(), } } } -impl std::fmt::Debug for ActorRef { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - f.debug_struct("ActorRef") - .field("tx", &self.tx) - .field("cancellation_token", &self.cancellation_token) - .finish_non_exhaustive() - } -} - impl ActorRef { - pub(crate) fn new(actor: A) -> Self { - let (tx, mut rx) = mpsc::channel::>(); - let cancellation_token = CancellationToken::new(); - let completion = Arc::new((Mutex::new(false), Condvar::new())); - let handle = ActorRef { - tx, - cancellation_token, - completion: completion.clone(), - }; - let handle_clone = handle.clone(); - let _thread_handle = rt::spawn(move || { - // Guard ensures completion is signaled even if actor panics - let _guard = CompletionGuard(completion); - if actor.run(&handle, &mut rx).is_err() { - tracing::trace!("Actor crashed") - }; - }); - handle_clone + pub fn send(&self, msg: M) -> Result<(), ActorError> + where + A: Handler, + M: Message, + { + let envelope = MessageEnvelope { msg, tx: None }; + self.sender + .send(Box::new(envelope)) + .map_err(|_| ActorError::ActorStopped) } - pub fn sender(&self) -> mpsc::Sender> { - self.tx.clone() + pub fn request(&self, msg: M) -> Result, ActorError> + where + A: Handler, + M: Message, + { + let (tx, rx) = oneshot::channel(); + let envelope = MessageEnvelope { + msg, + tx: Some(tx), + }; + self.sender + .send(Box::new(envelope)) + .map_err(|_| ActorError::ActorStopped)?; + Ok(rx) } - pub fn request(&mut self, message: A::Request) -> Result { - self.request_with_timeout(message, DEFAULT_REQUEST_TIMEOUT) + pub fn send_request(&self, msg: M) -> Result + where + A: Handler, + M: Message, + { + self.send_request_with_timeout(msg, DEFAULT_REQUEST_TIMEOUT) } - pub fn request_with_timeout( - &mut self, - message: A::Request, + pub fn send_request_with_timeout( + &self, + msg: M, duration: Duration, - ) -> Result { - let (oneshot_tx, oneshot_rx) = oneshot::channel::>(); - self.tx.send(ActorInMsg::Request { - sender: oneshot_tx, - message, - })?; - match oneshot_rx.recv_timeout(duration) { - Ok(result) => result, + ) -> Result + where + A: Handler, + M: Message, + { + let rx = self.request(msg)?; + match rx.recv_timeout(duration) { + Ok(result) => Ok(result), Err(RecvTimeoutError::Timeout) => Err(ActorError::RequestTimeout), - Err(RecvTimeoutError::Disconnected) => Err(ActorError::Server), + Err(RecvTimeoutError::Disconnected) => Err(ActorError::ActorStopped), } } - pub fn send(&mut self, message: A::Message) -> Result<(), ActorError> { - self.tx - .send(ActorInMsg::Message { message }) - .map_err(|_error| ActorError::Server) + pub fn recipient(&self) -> Recipient + where + A: Handler, + M: Message, + { + Arc::new(self.clone()) } - pub(crate) fn cancellation_token(&self) -> CancellationToken { - self.cancellation_token.clone() + pub fn context(&self) -> Context { + Context::from_ref(self) } - /// Blocks until the actor has stopped. - /// - /// This method blocks the current thread until the actor has finished - /// processing and exited its main loop. Can be called multiple times from - /// different clones of the ActorRef - all callers will be notified when - /// the actor stops. pub fn join(&self) { let (lock, cvar) = &*self.completion; let mut completed = lock.lock().unwrap_or_else(|p| p.into_inner()); @@ -124,191 +282,114 @@ impl ActorRef { } } -pub enum ActorInMsg { - Request { - sender: oneshot::Sender>, - message: A::Request, - }, - Message { - message: A::Message, - }, -} - -pub enum RequestResponse { - Reply(A::Reply), - Unused, - Stop(A::Reply), -} - -pub enum MessageResponse { - NoReply, - Unused, - Stop, -} +// Bridge: ActorRef implements Receiver for any M that A handles +impl Receiver for ActorRef +where + A: Actor + Handler, + M: Message, +{ + fn send(&self, msg: M) -> Result<(), ActorError> { + ActorRef::send(self, msg) + } -pub enum InitResult { - Success(A), - NoSuccess(A), + fn request(&self, msg: M) -> Result, ActorError> { + ActorRef::request(self, msg) + } } -pub trait Actor: Send + Sized { - type Request: Clone + Send + Sized + Sync; - type Message: Clone + Send + Sized + Sync; - type Reply: Send + Sized; - type Error: Debug + Send; +// --------------------------------------------------------------------------- +// Actor startup + main loop +// --------------------------------------------------------------------------- - fn start(self) -> ActorRef { - ActorRef::new(self) - } +impl ActorRef { + fn spawn(actor: A) -> Self { + let (tx, rx) = mpsc::channel:: + Send>>(); + let cancellation_token = CancellationToken::new(); + let completion = Arc::new((Mutex::new(false), Condvar::new())); - fn run( - self, - handle: &ActorRef, - rx: &mut mpsc::Receiver>, - ) -> Result<(), ActorError> { - let cancellation_token = handle.cancellation_token.clone(); - - let res = match self.init(handle) { - Ok(InitResult::Success(new_state)) => { - let final_state = new_state.main_loop(handle, rx)?; - Ok(final_state) - } - Ok(InitResult::NoSuccess(intermediate_state)) => { - // Initialization failed but error was handled in callback. - // Skip main_loop and return state for teardown. - Ok(intermediate_state) - } - Err(err) => { - tracing::error!("Initialization failed with unhandled error: {err:?}"); - Err(ActorError::Initialization) - } + let actor_ref = ActorRef { + sender: tx.clone(), + cancellation_token: cancellation_token.clone(), + completion: completion.clone(), }; - cancellation_token.cancel(); + let ctx = Context { + sender: tx, + cancellation_token: cancellation_token.clone(), + }; - if let Ok(final_state) = res { - if let Err(err) = final_state.teardown(handle) { - tracing::error!("Error during teardown: {err:?}"); - } - } + let _thread_handle = rt::spawn(move || { + let _guard = CompletionGuard(completion); + run_actor(actor, ctx, rx, cancellation_token); + }); - Ok(()) + actor_ref } +} - /// Initialization function. It's called before main loop. It - /// can be overrided on implementations in case initial steps are - /// required. - fn init(self, _handle: &ActorRef) -> Result, Self::Error> { - Ok(InitResult::Success(self)) - } +fn run_actor( + mut actor: A, + ctx: Context, + rx: mpsc::Receiver + Send>>, + cancellation_token: CancellationToken, +) { + actor.started(&ctx); - fn main_loop( - mut self, - handle: &ActorRef, - rx: &mut mpsc::Receiver>, - ) -> Result { - loop { - if !self.receive(handle, rx)? { - break; - } - } - tracing::trace!("Stopping Actor"); - Ok(self) + if cancellation_token.is_cancelled() { + actor.stopped(&ctx); + return; } - fn receive( - &mut self, - handle: &ActorRef, - rx: &mut mpsc::Receiver>, - ) -> Result { - let message = rx.recv().ok(); - - let keep_running = match message { - Some(ActorInMsg::Request { sender, message }) => { - let (keep_running, response) = match catch_unwind(AssertUnwindSafe(|| { - self.handle_request(message, handle) - })) { - Ok(response) => match response { - RequestResponse::Reply(response) => (true, Ok(response)), - RequestResponse::Stop(response) => (false, Ok(response)), - RequestResponse::Unused => { - tracing::error!("Actor received unexpected Request"); - (false, Err(ActorError::RequestUnused)) - } - }, - Err(error) => { - tracing::error!("Error in callback: '{error:?}'"); - (true, Err(ActorError::Callback)) - } - }; - // Send response back - if sender.send(response).is_err() { - tracing::trace!("Actor failed to send response back, client must have died") - }; - keep_running - } - Some(ActorInMsg::Message { message }) => { - match catch_unwind(AssertUnwindSafe(|| self.handle_message(message, handle))) { - Ok(response) => match response { - MessageResponse::NoReply => true, - MessageResponse::Stop => false, - MessageResponse::Unused => { - tracing::error!("Actor received unexpected Message"); - false - } - }, - Err(error) => { - tracing::error!("Error in callback: '{error:?}'"); - true - } + loop { + let msg = rx.recv().ok(); + match msg { + Some(envelope) => { + let result = catch_unwind(AssertUnwindSafe(|| { + envelope.handle(&mut actor, &ctx); + })); + if let Err(panic) = result { + tracing::error!("Panic in message handler: {panic:?}"); + break; + } + if cancellation_token.is_cancelled() { + break; } } - None => { - // Channel has been closed; won't receive further messages. Stop the server. - false - } - }; - Ok(keep_running) + None => break, + } } - fn handle_request( - &mut self, - _message: Self::Request, - _handle: &ActorRef, - ) -> RequestResponse { - RequestResponse::Unused - } + cancellation_token.cancel(); + actor.stopped(&ctx); +} - fn handle_message( - &mut self, - _message: Self::Message, - _handle: &ActorRef, - ) -> MessageResponse { - MessageResponse::Unused - } +// --------------------------------------------------------------------------- +// Actor::start +// --------------------------------------------------------------------------- - /// Teardown function. It's called after the stop message is received. - /// It can be overrided on implementations in case final steps are required, - /// like closing streams, stopping timers, etc. - fn teardown(self, _handle: &ActorRef) -> Result<(), Self::Error> { - Ok(()) +pub trait ActorStart: Actor { + fn start(self) -> ActorRef { + ActorRef::spawn(self) } } -/// Spawns a thread that runs a blocking operation and sends a message to an Actor -/// on completion. This is the sync equivalent of tasks::send_message_on. -/// This function returns a handle to the spawned thread. -pub fn send_message_on(handle: ActorRef, f: F, message: T::Message) -> rt::JoinHandle<()> +impl ActorStart for A {} + +// --------------------------------------------------------------------------- +// send_message_on (utility) +// --------------------------------------------------------------------------- + +pub fn send_message_on(ctx: Context, f: F, msg: M) -> rt::JoinHandle<()> where - T: Actor, + A: Actor + Handler, + M: Message, F: FnOnce() + Send + 'static, { - let cancellation_token = handle.cancellation_token(); - let mut handle_clone = handle.clone(); + let cancellation_token = ctx.cancellation_token(); rt::spawn(move || { f(); if !cancellation_token.is_cancelled() { - if let Err(e) = handle_clone.send(message) { + if let Err(e) = ctx.send(msg) { tracing::error!("Failed to send message: {e:?}") } } diff --git a/concurrency/src/threads/mod.rs b/concurrency/src/threads/mod.rs index 9643a13..d6ac085 100644 --- a/concurrency/src/threads/mod.rs +++ b/concurrency/src/threads/mod.rs @@ -1,7 +1,4 @@ -//! spawned concurrency -//! IO threads-based traits and structs to implement concurrent code à-la-Erlang. - -mod actor; +pub(crate) mod actor; mod process; mod stream; mod time; @@ -10,9 +7,9 @@ mod time; mod timer_tests; pub use actor::{ - send_message_on, Actor, ActorInMsg, ActorRef, InitResult, InitResult::NoSuccess, - InitResult::Success, MessageResponse, RequestResponse, + send_message_on, Actor, ActorRef, ActorStart, Context, Handler, Receiver, Recipient, + send_request, }; pub use process::{send, Process, ProcessInfo}; pub use stream::spawn_listener; -pub use time::{send_after, send_interval}; +pub use time::{send_after, send_interval, TimerHandle}; diff --git a/concurrency/src/threads/stream.rs b/concurrency/src/threads/stream.rs index 696c3cf..9249246 100644 --- a/concurrency/src/threads/stream.rs +++ b/concurrency/src/threads/stream.rs @@ -1,21 +1,21 @@ use std::thread::JoinHandle; -use crate::threads::{Actor, ActorRef}; +use crate::message::Message; -/// Spawns a listener that listens to a stream and sends messages to an Actor. -/// -/// Items sent through the stream are required to be wrapped in a Result type. -pub fn spawn_listener(mut handle: ActorRef, stream: I) -> JoinHandle<()> +use super::actor::{Actor, Context, Handler}; + +pub fn spawn_listener(ctx: Context, stream: I) -> JoinHandle<()> where - T: Actor, - I: IntoIterator, + A: Actor + Handler, + M: Message, + I: IntoIterator, ::IntoIter: std::marker::Send + 'static, { let mut iter = stream.into_iter(); - let cancellation_token = handle.cancellation_token(); + let cancellation_token = ctx.cancellation_token(); let join_handle = spawned_rt::threads::spawn(move || loop { match iter.next() { - Some(msg) => match handle.send(msg) { + Some(msg) => match ctx.send(msg) { Ok(_) => tracing::trace!("Message sent successfully"), Err(e) => { tracing::error!("Failed to send message: {e:?}"); diff --git a/concurrency/src/threads/time.rs b/concurrency/src/threads/time.rs index 5b4ebb8..78fb1cd 100644 --- a/concurrency/src/threads/time.rs +++ b/concurrency/src/threads/time.rs @@ -3,36 +3,30 @@ use std::time::Duration; use spawned_rt::threads::{self as rt, CancellationToken, JoinHandle}; -use super::{Actor, ActorRef}; +use super::actor::{Actor, Context, Handler}; +use crate::message::Message; pub struct TimerHandle { pub join_handle: JoinHandle<()>, pub cancellation_token: CancellationToken, } -/// Sends a message after a given period to the specified Actor. -/// -/// The timer respects both its own cancellation token and the Actor's -/// cancellation token. If either is cancelled, the timer wakes up immediately -/// and exits without sending the message. -pub fn send_after(period: Duration, mut handle: ActorRef, message: T::Message) -> TimerHandle +pub fn send_after(period: Duration, ctx: Context, msg: M) -> TimerHandle where - T: Actor + 'static, + A: Actor + Handler, + M: Message, { let cancellation_token = CancellationToken::new(); let timer_token = cancellation_token.clone(); - let actor_token = handle.cancellation_token(); + let actor_token = ctx.cancellation_token(); - // Channel to wake the timer thread on cancellation let (wake_tx, wake_rx) = mpsc::channel::<()>(); - // Register wake-up on timer cancellation let wake_tx1 = wake_tx.clone(); timer_token.on_cancel(Box::new(move || { let _ = wake_tx1.send(()); })); - // Register wake-up on actor cancellation actor_token.on_cancel(Box::new(move || { let _ = wake_tx.send(()); })); @@ -40,14 +34,11 @@ where let join_handle = rt::spawn(move || { match wake_rx.recv_timeout(period) { Err(RecvTimeoutError::Timeout) => { - // Timer expired - send if still valid if !timer_token.is_cancelled() && !actor_token.is_cancelled() { - let _ = handle.send(message); + let _ = ctx.send(msg); } } - Ok(()) | Err(RecvTimeoutError::Disconnected) => { - // Woken early by cancellation - exit without sending - } + Ok(()) | Err(RecvTimeoutError::Disconnected) => {} } }); @@ -57,46 +48,33 @@ where } } -/// Sends a message to the specified Actor repeatedly at the given interval. -/// -/// The timer respects both its own cancellation token and the Actor's -/// cancellation token. If either is cancelled, the timer wakes up immediately -/// and exits. -pub fn send_interval( - period: Duration, - mut handle: ActorRef, - message: T::Message, -) -> TimerHandle +pub fn send_interval(period: Duration, ctx: Context, msg: M) -> TimerHandle where - T: Actor + 'static, + A: Actor + Handler, + M: Message + Clone, { let cancellation_token = CancellationToken::new(); let timer_token = cancellation_token.clone(); - let actor_token = handle.cancellation_token(); + let actor_token = ctx.cancellation_token(); - // Channel to wake the timer thread on cancellation let (wake_tx, wake_rx) = mpsc::channel::<()>(); - // Register wake-up on timer cancellation let wake_tx1 = wake_tx.clone(); timer_token.on_cancel(Box::new(move || { let _ = wake_tx1.send(()); })); - // Register wake-up on actor cancellation actor_token.on_cancel(Box::new(move || { let _ = wake_tx.send(()); })); let join_handle = rt::spawn(move || { while let Err(RecvTimeoutError::Timeout) = wake_rx.recv_timeout(period) { - // Timer expired - send if still valid if timer_token.is_cancelled() || actor_token.is_cancelled() { break; } - let _ = handle.send(message.clone()); + let _ = ctx.send(msg.clone()); } - // If we exit the loop via Ok(()) or Disconnected, cancellation occurred }); TimerHandle { diff --git a/concurrency/src/threads/timer_tests.rs b/concurrency/src/threads/timer_tests.rs index e023a78..6ba13c9 100644 --- a/concurrency/src/threads/timer_tests.rs +++ b/concurrency/src/threads/timer_tests.rs @@ -1,33 +1,27 @@ use crate::threads::{ - send_interval, Actor, ActorRef, InitResult, MessageResponse, RequestResponse, + send_after, send_interval, Actor, ActorStart, Context, Handler, }; +use crate::message::Message; use spawned_rt::threads::{self as rt, CancellationToken}; use std::time::Duration; -use super::send_after; +// --- Repeater (interval timer test) --- -type RepeaterHandle = ActorRef; +#[derive(Clone, Debug)] +struct Inc; +impl Message for Inc { type Result = (); } -#[derive(Clone)] -enum RepeaterCastMessage { - Inc, - StopTimer, -} - -#[derive(Clone)] -enum RepeaterCallMessage { - GetCount, -} +#[derive(Clone, Debug)] +struct StopTimer; +impl Message for StopTimer { type Result = (); } -#[derive(PartialEq, Debug)] -enum RepeaterOutMessage { - Count(i32), -} +#[derive(Debug)] +struct GetRepCount; +impl Message for GetRepCount { type Result = i32; } -#[derive(Clone)] struct Repeater { - pub(crate) count: i32, - pub(crate) cancellation_token: Option, + count: i32, + cancellation_token: Option, } impl Repeater { @@ -39,240 +33,136 @@ impl Repeater { } } -impl Repeater { - pub fn stop_timer(server: &mut RepeaterHandle) -> Result<(), ()> { - server.send(RepeaterCastMessage::StopTimer).map_err(|_| ()) - } - - pub fn get_count(server: &mut RepeaterHandle) -> Result { - server - .request(RepeaterCallMessage::GetCount) - .map_err(|_| ()) - } -} - impl Actor for Repeater { - type Request = RepeaterCallMessage; - type Message = RepeaterCastMessage; - type Reply = RepeaterOutMessage; - type Error = (); - - fn init(mut self, handle: &RepeaterHandle) -> Result, Self::Error> { + fn started(&mut self, ctx: &Context) { let timer = send_interval( Duration::from_millis(100), - handle.clone(), - RepeaterCastMessage::Inc, + ctx.clone(), + Inc, ); self.cancellation_token = Some(timer.cancellation_token); - Ok(InitResult::Success(self)) } +} + +impl Handler for Repeater { + fn handle(&mut self, _msg: Inc, _ctx: &Context) { + self.count += 1; + } +} - fn handle_request( - &mut self, - _message: Self::Request, - _handle: &RepeaterHandle, - ) -> RequestResponse { - let count = self.count; - RequestResponse::Reply(RepeaterOutMessage::Count(count)) +impl Handler for Repeater { + fn handle(&mut self, _msg: StopTimer, _ctx: &Context) { + if let Some(ct) = self.cancellation_token.clone() { + ct.cancel(); + } } +} - fn handle_message( - &mut self, - message: Self::Message, - _handle: &ActorRef, - ) -> MessageResponse { - match message { - RepeaterCastMessage::Inc => { - self.count += 1; - } - RepeaterCastMessage::StopTimer => { - if let Some(ct) = self.cancellation_token.clone() { - ct.cancel() - }; - } - }; - MessageResponse::NoReply +impl Handler for Repeater { + fn handle(&mut self, _msg: GetRepCount, _ctx: &Context) -> i32 { + self.count } } #[test] pub fn test_send_interval_and_cancellation() { - // Start a Repeater - let mut repeater = Repeater::new(0).start(); + let repeater = Repeater::new(0).start(); - // Wait for 1 second rt::sleep(Duration::from_secs(1)); - // Check count - let count = Repeater::get_count(&mut repeater).unwrap(); - - // 9 messages in 1 second (after first 100 milliseconds sleep) - assert_eq!(RepeaterOutMessage::Count(9), count); + let count = repeater.send_request(GetRepCount).unwrap(); + assert_eq!(9, count); - // Pause timer - Repeater::stop_timer(&mut repeater).unwrap(); + repeater.send(StopTimer).unwrap(); - // Wait another second rt::sleep(Duration::from_secs(1)); - // Check count again - let count2 = Repeater::get_count(&mut repeater).unwrap(); - - // As timer was paused, count should remain at 9 - assert_eq!(RepeaterOutMessage::Count(9), count2); + let count2 = repeater.send_request(GetRepCount).unwrap(); + assert_eq!(9, count2); } -type DelayedHandle = ActorRef; - -#[derive(Clone)] -enum DelayedCastMessage { - Inc, -} +// --- Delayed (send_after test) --- -#[derive(Clone)] -enum DelayedCallMessage { - GetCount, - Stop, -} +#[derive(Debug)] +struct GetDelCount; +impl Message for GetDelCount { type Result = i32; } -#[derive(PartialEq, Debug)] -enum DelayedOutMessage { - Count(i32), -} +#[derive(Debug)] +struct StopDelayed; +impl Message for StopDelayed { type Result = i32; } -#[derive(Clone)] struct Delayed { - pub(crate) count: i32, + count: i32, } impl Delayed { pub fn new(initial_count: i32) -> Self { - Delayed { - count: initial_count, - } + Delayed { count: initial_count } } } -impl Delayed { - pub fn get_count(server: &mut DelayedHandle) -> Result { - server.request(DelayedCallMessage::GetCount).map_err(|_| ()) - } +impl Actor for Delayed {} - pub fn stop(server: &mut DelayedHandle) -> Result { - server.request(DelayedCallMessage::Stop).map_err(|_| ()) +impl Handler for Delayed { + fn handle(&mut self, _msg: Inc, _ctx: &Context) { + self.count += 1; } } -impl Actor for Delayed { - type Request = DelayedCallMessage; - type Message = DelayedCastMessage; - type Reply = DelayedOutMessage; - type Error = (); - - fn handle_request( - &mut self, - message: Self::Request, - _handle: &DelayedHandle, - ) -> RequestResponse { - match message { - DelayedCallMessage::GetCount => { - RequestResponse::Reply(DelayedOutMessage::Count(self.count)) - } - DelayedCallMessage::Stop => { - RequestResponse::Stop(DelayedOutMessage::Count(self.count)) - } - } +impl Handler for Delayed { + fn handle(&mut self, _msg: GetDelCount, _ctx: &Context) -> i32 { + self.count } +} - fn handle_message( - &mut self, - message: Self::Message, - _handle: &DelayedHandle, - ) -> MessageResponse { - match message { - DelayedCastMessage::Inc => { - self.count += 1; - } - }; - MessageResponse::NoReply +impl Handler for Delayed { + fn handle(&mut self, _msg: StopDelayed, ctx: &Context) -> i32 { + ctx.stop(); + self.count } } #[test] pub fn test_send_after_and_cancellation() { - // Start a Delayed - let mut repeater = Delayed::new(0).start(); + let actor = Delayed::new(0).start(); - // Set a just once timed message - let _ = send_after( - Duration::from_millis(100), - repeater.clone(), - DelayedCastMessage::Inc, - ); + let ctx = Context::from_ref(&actor); + let _ = send_after(Duration::from_millis(100), ctx, Inc); - // Wait for 200 milliseconds rt::sleep(Duration::from_millis(200)); - // Check count - let count = Delayed::get_count(&mut repeater).unwrap(); - - // Only one message (no repetition) - assert_eq!(DelayedOutMessage::Count(1), count); + let count = actor.send_request(GetDelCount).unwrap(); + assert_eq!(1, count); - // New timer - let timer = send_after( - Duration::from_millis(100), - repeater.clone(), - DelayedCastMessage::Inc, - ); + let ctx = Context::from_ref(&actor); + let timer = send_after(Duration::from_millis(100), ctx, Inc); - // Cancel the new timer before timeout timer.cancellation_token.cancel(); - // Wait another 200 milliseconds rt::sleep(Duration::from_millis(200)); - // Check count again - let count2 = Delayed::get_count(&mut repeater).unwrap(); - - // As timer was cancelled, count should remain at 1 - assert_eq!(DelayedOutMessage::Count(1), count2); + let count2 = actor.send_request(GetDelCount).unwrap(); + assert_eq!(1, count2); } #[test] pub fn test_send_after_actor_shutdown() { - // Start a Delayed - let mut actor = Delayed::new(0).start(); + let actor = Delayed::new(0).start(); - // Set a just once timed message - let _ = send_after( - Duration::from_millis(100), - actor.clone(), - DelayedCastMessage::Inc, - ); + let ctx = Context::from_ref(&actor); + let _ = send_after(Duration::from_millis(100), ctx, Inc); - // Wait for 200 milliseconds rt::sleep(Duration::from_millis(200)); - // Check count - let count = Delayed::get_count(&mut actor).unwrap(); - - // Only one message (no repetition) - assert_eq!(DelayedOutMessage::Count(1), count); + let count = actor.send_request(GetDelCount).unwrap(); + assert_eq!(1, count); - // New timer with long delay - let _ = send_after( - Duration::from_millis(100), - actor.clone(), - DelayedCastMessage::Inc, - ); + let ctx = Context::from_ref(&actor); + let _ = send_after(Duration::from_millis(100), ctx, Inc); - // Stop the Actor before timeout - this should wake up the timer immediately - let count2 = Delayed::stop(&mut actor).unwrap(); + let count2 = actor.send_request(StopDelayed).unwrap(); - // Wait another 200 milliseconds rt::sleep(Duration::from_millis(200)); - // As actor was stopped, count should remain at 1 (timer didn't fire) - assert_eq!(DelayedOutMessage::Count(1), count2); + assert_eq!(1, count2); } diff --git a/examples/bank/src/main.rs b/examples/bank/src/main.rs index d3321af..5ff3958 100644 --- a/examples/bank/src/main.rs +++ b/examples/bank/src/main.rs @@ -1,43 +1,26 @@ -//! Simple example to test concurrency/Process abstraction. +//! Bank example using the new Handler API. //! //! Based on Joe's Armstrong book: Programming Erlang, Second edition //! Section 22.1 - The Road to the Generic Server -//! -//! Erlang usage example: -//! 1> my_bank:start(). -//! {ok,<0.33.0>} -//! 2> my_bank:deposit("joe", 10). -//! not_a_customer -//! 3> my_bank:new_account("joe"). -//! {welcome,"joe"} -//! 4> my_bank:deposit("joe", 10). -//! {thanks,"joe",your_balance_is,10} -//! 5> my_bank:deposit("joe", 30). -//! {thanks,"joe",your_balance_is,40} -//! 6> my_bank:withdraw("joe", 15). -//! {thanks,"joe",your_balance_is,25} -//! 7> my_bank:withdraw("joe", 45). -//! {sorry,"joe",you_only_have,25,in_the_bank mod messages; mod server; -use messages::{BankError, BankOutMessage}; +use messages::*; use server::Bank; -use spawned_concurrency::tasks::Actor as _; +use spawned_concurrency::tasks::ActorStart as _; use spawned_rt::tasks as rt; fn main() { rt::run(async { - // Starting the bank - let mut name_server = Bank::new().start(); + let bank = Bank::new().start(); // Testing initial balance for "main" account - let result = Bank::withdraw(&mut name_server, "main".to_string(), 15).await; + let result = bank.send_request(Withdraw { who: "main".into(), amount: 15 }).await.unwrap(); tracing::info!("Withdraw result {result:?}"); assert_eq!( result, - Ok(BankOutMessage::WidrawOk { + Ok(BankOutMessage::WithdrawOk { who: "main".to_string(), amount: 985 }) @@ -45,73 +28,58 @@ fn main() { let joe = "Joe".to_string(); - // Error on deposit for an unexistent account - let result = Bank::deposit(&mut name_server, joe.clone(), 10).await; + // Error on deposit for a non-existent account + let result = bank.send_request(Deposit { who: joe.clone(), amount: 10 }).await.unwrap(); tracing::info!("Deposit result {result:?}"); assert_eq!(result, Err(BankError::NotACustomer { who: joe.clone() })); // Account creation - let result = Bank::new_account(&mut name_server, "Joe".to_string()).await; + let result = bank.send_request(NewAccount { who: joe.clone() }).await.unwrap(); tracing::info!("New account result {result:?}"); assert_eq!(result, Ok(BankOutMessage::Welcome { who: joe.clone() })); // Deposit - let result = Bank::deposit(&mut name_server, "Joe".to_string(), 10).await; + let result = bank.send_request(Deposit { who: joe.clone(), amount: 10 }).await.unwrap(); tracing::info!("Deposit result {result:?}"); assert_eq!( result, - Ok(BankOutMessage::Balance { - who: joe.clone(), - amount: 10 - }) + Ok(BankOutMessage::Balance { who: joe.clone(), amount: 10 }) ); // Deposit - let result = Bank::deposit(&mut name_server, "Joe".to_string(), 30).await; + let result = bank.send_request(Deposit { who: joe.clone(), amount: 30 }).await.unwrap(); tracing::info!("Deposit result {result:?}"); assert_eq!( result, - Ok(BankOutMessage::Balance { - who: joe.clone(), - amount: 40 - }) + Ok(BankOutMessage::Balance { who: joe.clone(), amount: 40 }) ); // Withdrawal - let result = Bank::withdraw(&mut name_server, "Joe".to_string(), 15).await; + let result = bank.send_request(Withdraw { who: joe.clone(), amount: 15 }).await.unwrap(); tracing::info!("Withdraw result {result:?}"); assert_eq!( result, - Ok(BankOutMessage::WidrawOk { - who: joe.clone(), - amount: 25 - }) + Ok(BankOutMessage::WithdrawOk { who: joe.clone(), amount: 25 }) ); // Withdrawal with not enough balance - let result = Bank::withdraw(&mut name_server, "Joe".to_string(), 45).await; + let result = bank.send_request(Withdraw { who: joe.clone(), amount: 45 }).await.unwrap(); tracing::info!("Withdraw result {result:?}"); assert_eq!( result, - Err(BankError::InsufficientBalance { - who: joe.clone(), - amount: 25 - }) + Err(BankError::InsufficientBalance { who: joe.clone(), amount: 25 }) ); // Full withdrawal - let result = Bank::withdraw(&mut name_server, "Joe".to_string(), 25).await; + let result = bank.send_request(Withdraw { who: joe.clone(), amount: 25 }).await.unwrap(); tracing::info!("Withdraw result {result:?}"); assert_eq!( result, - Ok(BankOutMessage::WidrawOk { - who: joe, - amount: 0 - }) + Ok(BankOutMessage::WithdrawOk { who: joe, amount: 0 }) ); // Stopping the bank - let result = Bank::stop(&mut name_server).await; + let result = bank.send_request(Stop).await.unwrap(); tracing::info!("Stop result {result:?}"); assert_eq!(result, Ok(BankOutMessage::Stopped)); }) diff --git a/examples/bank/src/messages.rs b/examples/bank/src/messages.rs index d58ae9d..908ea45 100644 --- a/examples/bank/src/messages.rs +++ b/examples/bank/src/messages.rs @@ -1,21 +1,45 @@ -#[derive(Debug, Clone)] -pub enum BankInMessage { - New { who: String }, - Add { who: String, amount: i32 }, - Remove { who: String, amount: i32 }, - Stop, +use spawned_concurrency::message::Message; + +#[derive(Debug)] +pub struct NewAccount { + pub who: String, +} +impl Message for NewAccount { + type Result = Result; +} + +#[derive(Debug)] +pub struct Deposit { + pub who: String, + pub amount: i32, +} +impl Message for Deposit { + type Result = Result; +} + +#[derive(Debug)] +pub struct Withdraw { + pub who: String, + pub amount: i32, +} +impl Message for Withdraw { + type Result = Result; +} + +#[derive(Debug)] +pub struct Stop; +impl Message for Stop { + type Result = Result; } -#[allow(dead_code)] #[derive(Debug, Clone, PartialEq)] pub enum BankOutMessage { Welcome { who: String }, Balance { who: String, amount: i32 }, - WidrawOk { who: String, amount: i32 }, + WithdrawOk { who: String, amount: i32 }, Stopped, } -#[allow(dead_code)] #[derive(Debug, Clone, PartialEq)] pub enum BankError { AlreadyACustomer { who: String }, diff --git a/examples/bank/src/server.rs b/examples/bank/src/server.rs index bd2bfed..7293ca5 100644 --- a/examples/bank/src/server.rs +++ b/examples/bank/src/server.rs @@ -1,18 +1,10 @@ use std::collections::HashMap; -use spawned_concurrency::{ - messages::Unused, - tasks::{ - Actor, ActorRef, - InitResult::{self, Success}, - RequestResponse, - }, -}; +use spawned_concurrency::tasks::{Actor, Context, Handler}; -use crate::messages::{BankError, BankInMessage as InMessage, BankOutMessage as OutMessage}; +use crate::messages::*; -type MsgResult = Result; -type BankHandle = ActorRef; +type MsgResult = Result; pub struct Bank { accounts: HashMap, @@ -26,90 +18,65 @@ impl Bank { } } -impl Bank { - pub async fn stop(server: &mut BankHandle) -> MsgResult { - server - .request(InMessage::Stop) - .await - .unwrap_or(Err(BankError::ServerError)) - } - - pub async fn new_account(server: &mut BankHandle, who: String) -> MsgResult { - server - .request(InMessage::New { who }) - .await - .unwrap_or(Err(BankError::ServerError)) +impl Actor for Bank { + async fn started(&mut self, _ctx: &Context) { + self.accounts.insert("main".to_string(), 1000); } +} - pub async fn deposit(server: &mut BankHandle, who: String, amount: i32) -> MsgResult { - server - .request(InMessage::Add { who, amount }) - .await - .unwrap_or(Err(BankError::ServerError)) +impl Handler for Bank { + async fn handle(&mut self, msg: NewAccount, _ctx: &Context) -> MsgResult { + match self.accounts.get(&msg.who) { + Some(_) => Err(BankError::AlreadyACustomer { who: msg.who }), + None => { + self.accounts.insert(msg.who.clone(), 0); + Ok(BankOutMessage::Welcome { who: msg.who }) + } + } } +} - pub async fn withdraw(server: &mut BankHandle, who: String, amount: i32) -> MsgResult { - server - .request(InMessage::Remove { who, amount }) - .await - .unwrap_or(Err(BankError::ServerError)) +impl Handler for Bank { + async fn handle(&mut self, msg: Deposit, _ctx: &Context) -> MsgResult { + match self.accounts.get(&msg.who) { + Some(current) => { + let new_amount = current + msg.amount; + self.accounts.insert(msg.who.clone(), new_amount); + Ok(BankOutMessage::Balance { + who: msg.who, + amount: new_amount, + }) + } + None => Err(BankError::NotACustomer { who: msg.who }), + } } } -impl Actor for Bank { - type Request = InMessage; - type Message = Unused; - type Reply = MsgResult; - type Error = BankError; - - // Initializing "main" account with 1000 in balance to test init() callback. - async fn init(mut self, _handle: &ActorRef) -> Result, Self::Error> { - self.accounts.insert("main".to_string(), 1000); - Ok(Success(self)) +impl Handler for Bank { + async fn handle(&mut self, msg: Withdraw, _ctx: &Context) -> MsgResult { + match self.accounts.get(&msg.who) { + Some(¤t) if current < msg.amount => { + Err(BankError::InsufficientBalance { + who: msg.who, + amount: current, + }) + } + Some(¤t) => { + let new_amount = current - msg.amount; + self.accounts.insert(msg.who.clone(), new_amount); + Ok(BankOutMessage::WithdrawOk { + who: msg.who, + amount: new_amount, + }) + } + None => Err(BankError::NotACustomer { who: msg.who }), + } } +} - async fn handle_request( - &mut self, - message: Self::Request, - _handle: &BankHandle, - ) -> RequestResponse { - match message.clone() { - Self::Request::New { who } => match self.accounts.get(&who) { - Some(_amount) => RequestResponse::Reply(Err(BankError::AlreadyACustomer { who })), - None => { - self.accounts.insert(who.clone(), 0); - RequestResponse::Reply(Ok(OutMessage::Welcome { who })) - } - }, - Self::Request::Add { who, amount } => match self.accounts.get(&who) { - Some(current) => { - let new_amount = current + amount; - self.accounts.insert(who.clone(), new_amount); - RequestResponse::Reply(Ok(OutMessage::Balance { - who, - amount: new_amount, - })) - } - None => RequestResponse::Reply(Err(BankError::NotACustomer { who })), - }, - Self::Request::Remove { who, amount } => match self.accounts.get(&who) { - Some(¤t) => match current < amount { - true => RequestResponse::Reply(Err(BankError::InsufficientBalance { - who, - amount: current, - })), - false => { - let new_amount = current - amount; - self.accounts.insert(who.clone(), new_amount); - RequestResponse::Reply(Ok(OutMessage::WidrawOk { - who, - amount: new_amount, - })) - } - }, - None => RequestResponse::Reply(Err(BankError::NotACustomer { who })), - }, - Self::Request::Stop => RequestResponse::Stop(Ok(OutMessage::Stopped)), - } +impl Handler for Bank { + async fn handle(&mut self, _msg: Stop, ctx: &Context) -> MsgResult { + ctx.stop(); + Ok(BankOutMessage::Stopped) } } diff --git a/examples/bank_threads/src/main.rs b/examples/bank_threads/src/main.rs index 9b89c54..5878516 100644 --- a/examples/bank_threads/src/main.rs +++ b/examples/bank_threads/src/main.rs @@ -1,43 +1,26 @@ -//! Simple example to test concurrency/Process abstraction. +//! Bank example using threads Actor with the new Handler API. //! //! Based on Joe's Armstrong book: Programming Erlang, Second edition //! Section 22.1 - The Road to the Generic Server -//! -//! Erlang usage example: -//! 1> my_bank:start(). -//! {ok,<0.33.0>} -//! 2> my_bank:deposit("joe", 10). -//! not_a_customer -//! 3> my_bank:new_account("joe"). -//! {welcome,"joe"} -//! 4> my_bank:deposit("joe", 10). -//! {thanks,"joe",your_balance_is,10} -//! 5> my_bank:deposit("joe", 30). -//! {thanks,"joe",your_balance_is,40} -//! 6> my_bank:withdraw("joe", 15). -//! {thanks,"joe",your_balance_is,25} -//! 7> my_bank:withdraw("joe", 45). -//! {sorry,"joe",you_only_have,25,in_the_bank mod messages; mod server; -use messages::{BankError, BankOutMessage}; +use messages::*; use server::Bank; -use spawned_concurrency::threads::Actor as _; +use spawned_concurrency::threads::ActorStart as _; use spawned_rt::threads as rt; fn main() { rt::run(|| { - // Starting the bank - let mut name_server = Bank::new().start(); + let bank = Bank::new().start(); // Testing initial balance for "main" account - let result = Bank::withdraw(&mut name_server, "main".to_string(), 15); + let result = bank.send_request(Withdraw { who: "main".into(), amount: 15 }).unwrap(); tracing::info!("Withdraw result {result:?}"); assert_eq!( result, - Ok(BankOutMessage::WidrawOk { + Ok(BankOutMessage::WithdrawOk { who: "main".to_string(), amount: 985 }) @@ -45,73 +28,58 @@ fn main() { let joe = "Joe".to_string(); - // Error on deposit for an unexistent account - let result = Bank::deposit(&mut name_server, joe.clone(), 10); + // Error on deposit for a non-existent account + let result = bank.send_request(Deposit { who: joe.clone(), amount: 10 }).unwrap(); tracing::info!("Deposit result {result:?}"); assert_eq!(result, Err(BankError::NotACustomer { who: joe.clone() })); // Account creation - let result = Bank::new_account(&mut name_server, "Joe".to_string()); + let result = bank.send_request(NewAccount { who: joe.clone() }).unwrap(); tracing::info!("New account result {result:?}"); assert_eq!(result, Ok(BankOutMessage::Welcome { who: joe.clone() })); // Deposit - let result = Bank::deposit(&mut name_server, "Joe".to_string(), 10); + let result = bank.send_request(Deposit { who: joe.clone(), amount: 10 }).unwrap(); tracing::info!("Deposit result {result:?}"); assert_eq!( result, - Ok(BankOutMessage::Balance { - who: joe.clone(), - amount: 10 - }) + Ok(BankOutMessage::Balance { who: joe.clone(), amount: 10 }) ); // Deposit - let result = Bank::deposit(&mut name_server, "Joe".to_string(), 30); + let result = bank.send_request(Deposit { who: joe.clone(), amount: 30 }).unwrap(); tracing::info!("Deposit result {result:?}"); assert_eq!( result, - Ok(BankOutMessage::Balance { - who: joe.clone(), - amount: 40 - }) + Ok(BankOutMessage::Balance { who: joe.clone(), amount: 40 }) ); // Withdrawal - let result = Bank::withdraw(&mut name_server, "Joe".to_string(), 15); + let result = bank.send_request(Withdraw { who: joe.clone(), amount: 15 }).unwrap(); tracing::info!("Withdraw result {result:?}"); assert_eq!( result, - Ok(BankOutMessage::WidrawOk { - who: joe.clone(), - amount: 25 - }) + Ok(BankOutMessage::WithdrawOk { who: joe.clone(), amount: 25 }) ); // Withdrawal with not enough balance - let result = Bank::withdraw(&mut name_server, "Joe".to_string(), 45); + let result = bank.send_request(Withdraw { who: joe.clone(), amount: 45 }).unwrap(); tracing::info!("Withdraw result {result:?}"); assert_eq!( result, - Err(BankError::InsufficientBalance { - who: joe.clone(), - amount: 25 - }) + Err(BankError::InsufficientBalance { who: joe.clone(), amount: 25 }) ); // Full withdrawal - let result = Bank::withdraw(&mut name_server, "Joe".to_string(), 25); + let result = bank.send_request(Withdraw { who: joe.clone(), amount: 25 }).unwrap(); tracing::info!("Withdraw result {result:?}"); assert_eq!( result, - Ok(BankOutMessage::WidrawOk { - who: joe, - amount: 0 - }) + Ok(BankOutMessage::WithdrawOk { who: joe, amount: 0 }) ); // Stopping the bank - let result = Bank::stop(&mut name_server); + let result = bank.send_request(Stop).unwrap(); tracing::info!("Stop result {result:?}"); assert_eq!(result, Ok(BankOutMessage::Stopped)); }) diff --git a/examples/bank_threads/src/messages.rs b/examples/bank_threads/src/messages.rs index d58ae9d..908ea45 100644 --- a/examples/bank_threads/src/messages.rs +++ b/examples/bank_threads/src/messages.rs @@ -1,21 +1,45 @@ -#[derive(Debug, Clone)] -pub enum BankInMessage { - New { who: String }, - Add { who: String, amount: i32 }, - Remove { who: String, amount: i32 }, - Stop, +use spawned_concurrency::message::Message; + +#[derive(Debug)] +pub struct NewAccount { + pub who: String, +} +impl Message for NewAccount { + type Result = Result; +} + +#[derive(Debug)] +pub struct Deposit { + pub who: String, + pub amount: i32, +} +impl Message for Deposit { + type Result = Result; +} + +#[derive(Debug)] +pub struct Withdraw { + pub who: String, + pub amount: i32, +} +impl Message for Withdraw { + type Result = Result; +} + +#[derive(Debug)] +pub struct Stop; +impl Message for Stop { + type Result = Result; } -#[allow(dead_code)] #[derive(Debug, Clone, PartialEq)] pub enum BankOutMessage { Welcome { who: String }, Balance { who: String, amount: i32 }, - WidrawOk { who: String, amount: i32 }, + WithdrawOk { who: String, amount: i32 }, Stopped, } -#[allow(dead_code)] #[derive(Debug, Clone, PartialEq)] pub enum BankError { AlreadyACustomer { who: String }, diff --git a/examples/bank_threads/src/server.rs b/examples/bank_threads/src/server.rs index 5edf5f7..f0cce1f 100644 --- a/examples/bank_threads/src/server.rs +++ b/examples/bank_threads/src/server.rs @@ -1,16 +1,11 @@ use std::collections::HashMap; -use spawned_concurrency::{ - messages::Unused, - threads::{Actor, ActorRef, InitResult, RequestResponse}, -}; +use spawned_concurrency::threads::{Actor, Context, Handler}; -use crate::messages::{BankError, BankInMessage as InMessage, BankOutMessage as OutMessage}; +use crate::messages::*; -type MsgResult = Result; -type BankHandle = ActorRef; +type MsgResult = Result; -#[derive(Clone)] pub struct Bank { accounts: HashMap, } @@ -23,86 +18,65 @@ impl Bank { } } -impl Bank { - pub fn stop(server: &mut BankHandle) -> MsgResult { - server - .request(InMessage::Stop) - .unwrap_or(Err(BankError::ServerError)) - } - - pub fn new_account(server: &mut BankHandle, who: String) -> MsgResult { - server - .request(InMessage::New { who }) - .unwrap_or(Err(BankError::ServerError)) +impl Actor for Bank { + fn started(&mut self, _ctx: &Context) { + self.accounts.insert("main".to_string(), 1000); } +} - pub fn deposit(server: &mut BankHandle, who: String, amount: i32) -> MsgResult { - server - .request(InMessage::Add { who, amount }) - .unwrap_or(Err(BankError::ServerError)) +impl Handler for Bank { + fn handle(&mut self, msg: NewAccount, _ctx: &Context) -> MsgResult { + match self.accounts.get(&msg.who) { + Some(_) => Err(BankError::AlreadyACustomer { who: msg.who }), + None => { + self.accounts.insert(msg.who.clone(), 0); + Ok(BankOutMessage::Welcome { who: msg.who }) + } + } } +} - pub fn withdraw(server: &mut BankHandle, who: String, amount: i32) -> MsgResult { - server - .request(InMessage::Remove { who, amount }) - .unwrap_or(Err(BankError::ServerError)) +impl Handler for Bank { + fn handle(&mut self, msg: Deposit, _ctx: &Context) -> MsgResult { + match self.accounts.get(&msg.who) { + Some(current) => { + let new_amount = current + msg.amount; + self.accounts.insert(msg.who.clone(), new_amount); + Ok(BankOutMessage::Balance { + who: msg.who, + amount: new_amount, + }) + } + None => Err(BankError::NotACustomer { who: msg.who }), + } } } -impl Actor for Bank { - type Request = InMessage; - type Message = Unused; - type Reply = MsgResult; - type Error = BankError; - - // Initializing "main" account with 1000 in balance to test init() callback. - fn init(mut self, _handle: &ActorRef) -> Result, Self::Error> { - self.accounts.insert("main".to_string(), 1000); - Ok(InitResult::Success(self)) +impl Handler for Bank { + fn handle(&mut self, msg: Withdraw, _ctx: &Context) -> MsgResult { + match self.accounts.get(&msg.who) { + Some(¤t) if current < msg.amount => { + Err(BankError::InsufficientBalance { + who: msg.who, + amount: current, + }) + } + Some(¤t) => { + let new_amount = current - msg.amount; + self.accounts.insert(msg.who.clone(), new_amount); + Ok(BankOutMessage::WithdrawOk { + who: msg.who, + amount: new_amount, + }) + } + None => Err(BankError::NotACustomer { who: msg.who }), + } } +} - fn handle_request( - &mut self, - message: Self::Request, - _handle: &BankHandle, - ) -> RequestResponse { - match message.clone() { - Self::Request::New { who } => match self.accounts.get(&who) { - Some(_amount) => RequestResponse::Reply(Err(BankError::AlreadyACustomer { who })), - None => { - self.accounts.insert(who.clone(), 0); - RequestResponse::Reply(Ok(OutMessage::Welcome { who })) - } - }, - Self::Request::Add { who, amount } => match self.accounts.get(&who) { - Some(current) => { - let new_amount = current + amount; - self.accounts.insert(who.clone(), new_amount); - RequestResponse::Reply(Ok(OutMessage::Balance { - who, - amount: new_amount, - })) - } - None => RequestResponse::Reply(Err(BankError::NotACustomer { who })), - }, - Self::Request::Remove { who, amount } => match self.accounts.get(&who) { - Some(¤t) => match current < amount { - true => RequestResponse::Reply(Err(BankError::InsufficientBalance { - who, - amount: current, - })), - false => { - let new_amount = current - amount; - self.accounts.insert(who.clone(), new_amount); - RequestResponse::Reply(Ok(OutMessage::WidrawOk { - who, - amount: new_amount, - })) - } - }, - None => RequestResponse::Reply(Err(BankError::NotACustomer { who })), - }, - Self::Request::Stop => RequestResponse::Stop(Ok(OutMessage::Stopped)), - } +impl Handler for Bank { + fn handle(&mut self, _msg: Stop, ctx: &Context) -> MsgResult { + ctx.stop(); + Ok(BankOutMessage::Stopped) } } diff --git a/examples/blocking_genserver/main.rs b/examples/blocking_genserver/main.rs index f1ec820..99694d4 100644 --- a/examples/blocking_genserver/main.rs +++ b/examples/blocking_genserver/main.rs @@ -1,46 +1,42 @@ +use spawned_concurrency::message::Message; +use spawned_concurrency::tasks::{ + send_after, Actor, ActorStart as _, Backend, Context, Handler, +}; use spawned_rt::tasks as rt; use std::time::Duration; use std::{process::exit, thread}; -use spawned_concurrency::tasks::{ - Actor, ActorRef, Backend, MessageResponse, RequestResponse, send_after, -}; - // We test a scenario with a badly behaved task struct BadlyBehavedTask; -impl BadlyBehavedTask { - pub fn new() -> Self { - BadlyBehavedTask - } +#[derive(Debug)] +pub struct DoBlock; +impl Message for DoBlock { + type Result = (); } -#[derive(Clone)] -pub enum InMessage { - GetCount, - Stop, +#[derive(Debug)] +pub struct GetCount; +impl Message for GetCount { + type Result = u64; } -#[derive(Clone)] -pub enum OutMsg { - Count(u64), +#[derive(Debug)] +pub struct StopActor; +impl Message for StopActor { + type Result = u64; } -impl Actor for BadlyBehavedTask { - type Request = InMessage; - type Message = (); - type Reply = (); - type Error = (); +#[derive(Debug)] +pub struct Tick; +impl Message for Tick { + type Result = (); +} - async fn handle_request( - &mut self, - _: Self::Request, - _: &ActorRef, - ) -> RequestResponse { - RequestResponse::Stop(()) - } +impl Actor for BadlyBehavedTask {} - async fn handle_message(&mut self, _: Self::Message, _: &ActorRef) -> MessageResponse { +impl Handler for BadlyBehavedTask { + async fn handle(&mut self, _msg: DoBlock, _ctx: &Context) { rt::sleep(Duration::from_millis(20)).await; loop { println!("{:?}: bad still alive", thread::current().id()); @@ -53,43 +49,26 @@ struct WellBehavedTask { count: u64, } -impl WellBehavedTask { - pub fn new(initial_count: u64) -> Self { - WellBehavedTask { - count: initial_count, - } +impl Actor for WellBehavedTask {} + +impl Handler for WellBehavedTask { + async fn handle(&mut self, _msg: GetCount, _ctx: &Context) -> u64 { + self.count } } -impl Actor for WellBehavedTask { - type Request = InMessage; - type Message = (); - type Reply = OutMsg; - type Error = (); - - async fn handle_request( - &mut self, - message: Self::Request, - _: &ActorRef, - ) -> RequestResponse { - match message { - InMessage::GetCount => { - let count = self.count; - RequestResponse::Reply(OutMsg::Count(count)) - } - InMessage::Stop => RequestResponse::Stop(OutMsg::Count(self.count)), - } +impl Handler for WellBehavedTask { + async fn handle(&mut self, _msg: StopActor, ctx: &Context) -> u64 { + ctx.stop(); + self.count } +} - async fn handle_message( - &mut self, - _: Self::Message, - handle: &ActorRef, - ) -> MessageResponse { +impl Handler for WellBehavedTask { + async fn handle(&mut self, _msg: Tick, ctx: &Context) { self.count += 1; println!("{:?}: good still alive", thread::current().id()); - send_after(Duration::from_millis(100), handle.to_owned(), ()); - MessageResponse::NoReply + send_after(Duration::from_millis(100), ctx.clone(), Tick); } } @@ -99,20 +78,16 @@ impl Actor for WellBehavedTask { pub fn main() { rt::run(async move { // If we change BadlyBehavedTask to Backend::Async instead, it can stop the entire program - let mut badboy = BadlyBehavedTask::new().start_with_backend(Backend::Thread); - let _ = badboy.send(()).await; - let mut goodboy = WellBehavedTask::new(0).start(); - let _ = goodboy.send(()).await; + let badboy = BadlyBehavedTask.start_with_backend(Backend::Thread); + let _ = badboy.send(DoBlock); + let goodboy = WellBehavedTask { count: 0 }.start(); + let _ = goodboy.send(Tick); rt::sleep(Duration::from_secs(1)).await; - let count = goodboy.request(InMessage::GetCount).await.unwrap(); + let count = goodboy.send_request(GetCount).await.unwrap(); - match count { - OutMsg::Count(num) => { - assert!(num == 10); - } - } + assert!(count == 10); - goodboy.request(InMessage::Stop).await.unwrap(); + goodboy.send_request(StopActor).await.unwrap(); exit(0); }) } diff --git a/examples/busy_genserver_warning/main.rs b/examples/busy_genserver_warning/main.rs index cf83573..f354ebb 100644 --- a/examples/busy_genserver_warning/main.rs +++ b/examples/busy_genserver_warning/main.rs @@ -1,56 +1,29 @@ +use spawned_concurrency::message::Message; +use spawned_concurrency::tasks::{Actor, ActorStart as _, Context, Handler}; use spawned_rt::tasks as rt; use std::time::Duration; use std::{process::exit, thread}; use tracing::info; -use spawned_concurrency::tasks::{Actor, ActorRef, MessageResponse, RequestResponse}; - // We test a scenario with a badly behaved task struct BusyWorker; -impl BusyWorker { - pub fn new() -> Self { - BusyWorker - } -} - -#[derive(Clone)] -pub enum InMessage { - GetCount, - Stop, +#[derive(Debug)] +pub struct DoWork; +impl Message for DoWork { + type Result = (); } -#[derive(Clone)] -pub enum OutMsg { - Count(u64), -} - -impl Actor for BusyWorker { - type Request = InMessage; - type Message = (); - type Reply = (); - type Error = (); - - async fn handle_request( - &mut self, - _: Self::Request, - _: &ActorRef, - ) -> RequestResponse { - RequestResponse::Stop(()) - } +impl Actor for BusyWorker {} - async fn handle_message( - &mut self, - _: Self::Message, - handle: &ActorRef, - ) -> MessageResponse { +impl Handler for BusyWorker { + async fn handle(&mut self, _msg: DoWork, ctx: &Context) { info!(taskid = ?rt::task_id(), "sleeping"); thread::sleep(Duration::from_millis(542)); - handle.clone().send(()).await.unwrap(); + let _ = ctx.send(DoWork); // This sleep is needed to yield control to the runtime. // If not, the future never returns and the warning isn't emitted. rt::sleep(Duration::from_millis(0)).await; - MessageResponse::NoReply } } @@ -64,8 +37,8 @@ impl Actor for BusyWorker { pub fn main() { rt::run(async move { // If we change BusyWorker to Backend::Blocking instead, it won't print the warning - let mut badboy = BusyWorker::new().start(); - let _ = badboy.send(()).await; + let badboy = BusyWorker.start(); + let _ = badboy.send(DoWork); rt::sleep(Duration::from_secs(5)).await; exit(0); diff --git a/examples/name_server/src/main.rs b/examples/name_server/src/main.rs index 85fab9e..4daca79 100644 --- a/examples/name_server/src/main.rs +++ b/examples/name_server/src/main.rs @@ -1,44 +1,31 @@ -//! Simple example to test concurrency/Process abstraction. +//! Name server example using the new Handler API. //! //! Based on Joe's Armstrong book: Programming Erlang, Second edition //! Section 22.1 - The Road to the Generic Server -//! -//! Erlang usage example: -//! 1> server1:start(name_server, name_server). -//! true -//! 2> name_server:add(joe, "at home"). -//! ok -//! 3> name_server:find(joe). -//! {ok,"at home"} mod messages; mod server; -use messages::NameServerOutMessage; +use messages::*; use server::NameServer; -use spawned_concurrency::tasks::Actor as _; +use spawned_concurrency::tasks::ActorStart as _; use spawned_rt::tasks as rt; fn main() { rt::run(async { - let mut name_server = NameServer::new().start(); + let ns = NameServer::new().start(); - let result = - NameServer::add(&mut name_server, "Joe".to_string(), "At Home".to_string()).await; - tracing::info!("Storing value result: {result:?}"); - assert_eq!(result, NameServerOutMessage::Ok); + ns.send_request(Add { key: "Joe".into(), value: "At Home".into() }).await.unwrap(); - let result = NameServer::find(&mut name_server, "Joe".to_string()).await; + let result = ns.send_request(Find { key: "Joe".into() }).await.unwrap(); tracing::info!("Retrieving value result: {result:?}"); assert_eq!( result, - NameServerOutMessage::Found { - value: "At Home".to_string() - } + FindResult::Found { value: "At Home".to_string() } ); - let result = NameServer::find(&mut name_server, "Bob".to_string()).await; + let result = ns.send_request(Find { key: "Bob".into() }).await.unwrap(); tracing::info!("Retrieving value result: {result:?}"); - assert_eq!(result, NameServerOutMessage::NotFound); + assert_eq!(result, FindResult::NotFound); }) } diff --git a/examples/name_server/src/messages.rs b/examples/name_server/src/messages.rs index b011cb2..3b58499 100644 --- a/examples/name_server/src/messages.rs +++ b/examples/name_server/src/messages.rs @@ -1,14 +1,24 @@ -#[derive(Debug, Clone)] -pub enum NameServerInMessage { - Add { key: String, value: String }, - Find { key: String }, +use spawned_concurrency::message::Message; + +#[derive(Debug)] +pub struct Add { + pub key: String, + pub value: String, +} +impl Message for Add { + type Result = (); +} + +#[derive(Debug)] +pub struct Find { + pub key: String, +} +impl Message for Find { + type Result = FindResult; } -#[allow(dead_code)] #[derive(Debug, Clone, PartialEq)] -pub enum NameServerOutMessage { - Ok, +pub enum FindResult { Found { value: String }, NotFound, - Error, } diff --git a/examples/name_server/src/server.rs b/examples/name_server/src/server.rs index 59a5c96..5f18b23 100644 --- a/examples/name_server/src/server.rs +++ b/examples/name_server/src/server.rs @@ -1,13 +1,8 @@ use std::collections::HashMap; -use spawned_concurrency::{ - messages::Unused, - tasks::{Actor, ActorRef, RequestResponse}, -}; +use spawned_concurrency::tasks::{Actor, Context, Handler}; -use crate::messages::{NameServerInMessage as InMessage, NameServerOutMessage as OutMessage}; - -type NameServerHandle = ActorRef; +use crate::messages::*; pub struct NameServer { inner: HashMap, @@ -21,45 +16,19 @@ impl NameServer { } } -impl NameServer { - pub async fn add(server: &mut NameServerHandle, key: String, value: String) -> OutMessage { - match server.request(InMessage::Add { key, value }).await { - Ok(_) => OutMessage::Ok, - Err(_) => OutMessage::Error, - } - } +impl Actor for NameServer {} - pub async fn find(server: &mut NameServerHandle, key: String) -> OutMessage { - server - .request(InMessage::Find { key }) - .await - .unwrap_or(OutMessage::Error) +impl Handler for NameServer { + async fn handle(&mut self, msg: Add, _ctx: &Context) { + self.inner.insert(msg.key, msg.value); } } -impl Actor for NameServer { - type Request = InMessage; - type Message = Unused; - type Reply = OutMessage; - type Error = std::fmt::Error; - - async fn handle_request( - &mut self, - message: Self::Request, - _handle: &NameServerHandle, - ) -> RequestResponse { - match message.clone() { - Self::Request::Add { key, value } => { - self.inner.insert(key, value); - RequestResponse::Reply(Self::Reply::Ok) - } - Self::Request::Find { key } => match self.inner.get(&key) { - Some(result) => { - let value = result.to_string(); - RequestResponse::Reply(Self::Reply::Found { value }) - } - None => RequestResponse::Reply(Self::Reply::NotFound), - }, +impl Handler for NameServer { + async fn handle(&mut self, msg: Find, _ctx: &Context) -> FindResult { + match self.inner.get(&msg.key) { + Some(value) => FindResult::Found { value: value.clone() }, + None => FindResult::NotFound, } } } diff --git a/examples/ping_pong/src/consumer.rs b/examples/ping_pong/src/consumer.rs index 8ead269..c012eff 100644 --- a/examples/ping_pong/src/consumer.rs +++ b/examples/ping_pong/src/consumer.rs @@ -1,26 +1,16 @@ -use spawned_concurrency::tasks::{self as concurrency, Process, ProcessInfo}; -use spawned_rt::tasks::mpsc::Sender; +use spawned_concurrency::tasks::{Actor, Context, Handler, Recipient}; -use crate::messages::Message; +use crate::messages::{Ping, Pong}; -pub struct Consumer {} - -impl Consumer { - pub async fn spawn_new() -> ProcessInfo { - Self {}.spawn().await - } +pub struct Consumer { + pub producer: Recipient, } -impl Process for Consumer { - async fn handle(&mut self, message: Message, _tx: &Sender) -> Message { - tracing::info!("Consumer received {message:?}"); - match message.clone() { - Message::Ping { from } => { - tracing::info!("Consumer sent Pong"); - concurrency::send(&from, Message::Pong); - } - Message::Pong => (), - }; - message +impl Actor for Consumer {} + +impl Handler for Consumer { + async fn handle(&mut self, _msg: Ping, _ctx: &Context) { + tracing::info!("Consumer received Ping, sending Pong"); + let _ = self.producer.send(Pong); } } diff --git a/examples/ping_pong/src/main.rs b/examples/ping_pong/src/main.rs index 1b1599b..d73b51a 100644 --- a/examples/ping_pong/src/main.rs +++ b/examples/ping_pong/src/main.rs @@ -1,55 +1,39 @@ -//! Simple example to test concurrency/Process abstraction +//! Ping-pong example demonstrating bidirectional communication +//! between actors using Recipient for type-erased messaging. //! -//! Based on an Erlang example: -//! -module(ping). -//! -//! -export([ping/1, pong/0, spawn_consumer/0, spawn_producer/1, start/0]). -//! -//! ping(Pid) -> -//! Pid ! {ping, self()}, -//! receive -//! pong -> -//! io:format("Received pong!!!~n"), -//! ping(Pid) -//! end. -//! -//! pong() -> -//! receive -//! {ping, Pid} -> -//! io:format("Received ping!!~n"), -//! Pid ! pong, -//! pong(); -//! die -> -//! ok -//! end. -//! -//! spawn_consumer() -> -//! spawn(ping, pong, []). -//! -//! spawn_producer(Pid) -> -//! spawn(ping, ping, [Pid]). -//! -//! start() -> -//! Pid = spawn_consumer(), -//! spawn_producer(Pid). +//! This solves the circular dependency problem: Consumer and Producer +//! don't need to know each other's concrete types — they only know +//! about the message types they exchange (Ping and Pong). mod consumer; mod messages; mod producer; -use std::{thread, time::Duration}; - use consumer::Consumer; +use messages::Ping; use producer::Producer; +use spawned_concurrency::tasks::ActorStart as _; use spawned_rt::tasks as rt; +use std::time::Duration; fn main() { rt::run(async { - let consumer = Consumer::spawn_new().await; + // Start the producer first + let producer = Producer { consumer: None }.start(); + + // Start the consumer with a Recipient pointing to the producer + let consumer = Consumer { + producer: producer.recipient(), + } + .start(); + + // Wire up the producer with the consumer's Recipient + producer.send(messages::SetConsumer(consumer.recipient())).unwrap(); - Producer::spawn_new(consumer.sender()).await; + // Kick off the ping-pong loop + consumer.send(Ping).unwrap(); - // giving it some time before ending - thread::sleep(Duration::from_millis(1)); + // Let them ping-pong for a bit + rt::sleep(Duration::from_millis(1)).await; }) } diff --git a/examples/ping_pong/src/messages.rs b/examples/ping_pong/src/messages.rs index a22ae6c..ea79ed9 100644 --- a/examples/ping_pong/src/messages.rs +++ b/examples/ping_pong/src/messages.rs @@ -1,7 +1,26 @@ -use spawned_rt::tasks::mpsc::Sender; +use spawned_concurrency::message::Message; +use spawned_concurrency::tasks::Recipient; -#[derive(Debug, Clone)] -pub enum Message { - Ping { from: Sender }, - Pong, +#[derive(Debug)] +pub struct Ping; +impl Message for Ping { + type Result = (); +} + +#[derive(Debug)] +pub struct Pong; +impl Message for Pong { + type Result = (); +} + +pub struct SetConsumer(pub Recipient); +impl Message for SetConsumer { + type Result = (); +} + +// Debug impl for SetConsumer (Recipient doesn't implement Debug) +impl std::fmt::Debug for SetConsumer { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.debug_tuple("SetConsumer").finish() + } } diff --git a/examples/ping_pong/src/producer.rs b/examples/ping_pong/src/producer.rs index 71829a1..7d90606 100644 --- a/examples/ping_pong/src/producer.rs +++ b/examples/ping_pong/src/producer.rs @@ -1,32 +1,24 @@ -use spawned_concurrency::tasks::{self as concurrency, Process, ProcessInfo}; -use spawned_rt::tasks::mpsc::Sender; +use spawned_concurrency::tasks::{Actor, Context, Handler, Recipient}; -use crate::messages::Message; +use crate::messages::{Ping, Pong, SetConsumer}; pub struct Producer { - consumer: Sender, + pub consumer: Option>, } -impl Producer { - pub async fn spawn_new(consumer: Sender) -> ProcessInfo { - Self { consumer }.spawn().await - } +impl Actor for Producer {} - fn send_ping(&self, tx: &Sender, consumer: &Sender) { - let message = Message::Ping { from: tx.clone() }; - tracing::info!("Producer sent Ping"); - concurrency::send(consumer, message); +impl Handler for Producer { + async fn handle(&mut self, msg: SetConsumer, _ctx: &Context) { + self.consumer = Some(msg.0); } } -impl Process for Producer { - async fn init(&mut self, tx: &Sender) { - self.send_ping(tx, &self.consumer); - } - - async fn handle(&mut self, message: Message, tx: &Sender) -> Message { - tracing::info!("Producer received {message:?}"); - self.send_ping(tx, &self.consumer); - message +impl Handler for Producer { + async fn handle(&mut self, _msg: Pong, _ctx: &Context) { + tracing::info!("Producer received Pong, sending Ping"); + if let Some(consumer) = &self.consumer { + let _ = consumer.send(Ping); + } } } diff --git a/examples/signal_test/src/main.rs b/examples/signal_test/src/main.rs index 90e6d6b..7df9037 100644 --- a/examples/signal_test/src/main.rs +++ b/examples/signal_test/src/main.rs @@ -1,7 +1,7 @@ //! Test to verify signal handling across different Actor backends (tasks version). //! //! This example demonstrates using `send_message_on` to handle Ctrl+C signals. -//! The signal handler is set up in the Actor's `init()` function. +//! The signal handler is set up in the Actor's `started()` function. //! //! Run with: cargo run --bin signal_test -- [async|blocking|thread] //! @@ -9,11 +9,9 @@ //! - Does the actor stop gracefully? //! - Does teardown run? -use spawned_concurrency::{ - messages::Unused, - tasks::{ - send_interval, send_message_on, Actor, ActorRef, Backend, InitResult, MessageResponse, - }, +use spawned_concurrency::message::Message; +use spawned_concurrency::tasks::{ + send_interval, send_message_on, Actor, ActorStart as _, Backend, Context, Handler, }; use spawned_rt::tasks::{self as rt, CancellationToken}; use std::{env, time::Duration}; @@ -34,56 +32,47 @@ impl TickingActor { } } -#[derive(Clone)] -enum Msg { - Tick, - Shutdown, +#[derive(Debug, Clone)] +struct Tick; +impl Message for Tick { + type Result = (); } -impl Actor for TickingActor { - type Request = Unused; - type Message = Msg; - type Reply = Unused; - type Error = (); +#[derive(Debug)] +struct Shutdown; +impl Message for Shutdown { + type Result = (); +} - async fn init(mut self, handle: &ActorRef) -> Result, Self::Error> { +impl Actor for TickingActor { + async fn started(&mut self, ctx: &Context) { tracing::info!("[{}] Actor initialized", self.name); - // Set up periodic ticking - let timer = send_interval(Duration::from_secs(1), handle.clone(), Msg::Tick); + // Set up periodic ticking — need an ActorRef to use with send_message_on + let timer = send_interval(Duration::from_secs(1), ctx.clone(), Tick); self.timer_token = Some(timer.cancellation_token); - - // Set up Ctrl+C handler using send_message_on - send_message_on(handle.clone(), rt::ctrl_c(), Msg::Shutdown); - - Ok(InitResult::Success(self)) } - async fn handle_message( - &mut self, - message: Self::Message, - _handle: &ActorRef, - ) -> MessageResponse { - match message { - Msg::Tick => { - self.count += 1; - tracing::info!("[{}] Tick #{}", self.name, self.count); - MessageResponse::NoReply - } - Msg::Shutdown => { - tracing::info!("[{}] Received shutdown signal", self.name); - MessageResponse::Stop - } - } - } - - async fn teardown(self, _handle: &ActorRef) -> Result<(), Self::Error> { + async fn stopped(&mut self, _ctx: &Context) { tracing::info!( "[{}] Teardown called! Final count: {}", self.name, self.count ); - Ok(()) + } +} + +impl Handler for TickingActor { + async fn handle(&mut self, _msg: Tick, _ctx: &Context) { + self.count += 1; + tracing::info!("[{}] Tick #{}", self.name, self.count); + } +} + +impl Handler for TickingActor { + async fn handle(&mut self, _msg: Shutdown, ctx: &Context) { + tracing::info!("[{}] Received shutdown signal", self.name); + ctx.stop(); } } @@ -127,6 +116,10 @@ fn main() { } }; + // Set up Ctrl+C handler using send_message_on + send_message_on(actor1.context(), rt::ctrl_c(), Shutdown); + send_message_on(actor2.context(), rt::ctrl_c(), Shutdown); + // Wait for both actors to stop actor1.join().await; actor2.join().await; diff --git a/examples/signal_test_threads/src/main.rs b/examples/signal_test_threads/src/main.rs index a0da2a0..82b96dd 100644 --- a/examples/signal_test_threads/src/main.rs +++ b/examples/signal_test_threads/src/main.rs @@ -1,7 +1,7 @@ //! Test to verify signal handling for threads Actor. //! //! This example demonstrates using `send_message_on` to handle Ctrl+C signals. -//! The signal handler is set up in the Actor's `init()` function. +//! The signal handler is set up after starting the actors. //! //! Run with: cargo run --bin signal_test_threads //! @@ -9,9 +9,9 @@ //! - Does the actor stop gracefully? //! - Does teardown run? -use spawned_concurrency::{ - messages::Unused, - threads::{send_interval, send_message_on, Actor, ActorRef, InitResult, MessageResponse}, +use spawned_concurrency::message::Message; +use spawned_concurrency::threads::{ + send_interval, send_message_on, Actor, ActorStart as _, Context, Handler, }; use spawned_rt::threads::{self as rt, CancellationToken}; use std::time::Duration; @@ -32,56 +32,47 @@ impl TickingActor { } } -#[derive(Clone)] -enum Msg { - Tick, - Shutdown, +#[derive(Debug, Clone)] +struct Tick; +impl Message for Tick { + type Result = (); } -impl Actor for TickingActor { - type Request = Unused; - type Message = Msg; - type Reply = Unused; - type Error = (); +#[derive(Debug)] +struct Shutdown; +impl Message for Shutdown { + type Result = (); +} - fn init(mut self, handle: &ActorRef) -> Result, Self::Error> { +impl Actor for TickingActor { + fn started(&mut self, ctx: &Context) { tracing::info!("[{}] Actor initialized", self.name); // Set up periodic ticking - let timer = send_interval(Duration::from_secs(1), handle.clone(), Msg::Tick); + let timer = send_interval(Duration::from_secs(1), ctx.clone(), Tick); self.timer_token = Some(timer.cancellation_token); - - // Set up Ctrl+C handler using send_message_on - send_message_on(handle.clone(), rt::ctrl_c(), Msg::Shutdown); - - Ok(InitResult::Success(self)) - } - - fn handle_message( - &mut self, - message: Self::Message, - _handle: &ActorRef, - ) -> MessageResponse { - match message { - Msg::Tick => { - self.count += 1; - tracing::info!("[{}] Tick #{}", self.name, self.count); - MessageResponse::NoReply - } - Msg::Shutdown => { - tracing::info!("[{}] Received shutdown signal", self.name); - MessageResponse::Stop - } - } } - fn teardown(self, _handle: &ActorRef) -> Result<(), Self::Error> { + fn stopped(&mut self, _ctx: &Context) { tracing::info!( "[{}] Teardown called! Final count: {}", self.name, self.count ); - Ok(()) + } +} + +impl Handler for TickingActor { + fn handle(&mut self, _msg: Tick, _ctx: &Context) { + self.count += 1; + tracing::info!("[{}] Tick #{}", self.name, self.count); + } +} + +impl Handler for TickingActor { + fn handle(&mut self, _msg: Shutdown, ctx: &Context) { + tracing::info!("[{}] Received shutdown signal", self.name); + ctx.stop(); } } @@ -94,6 +85,10 @@ fn main() { let actor1 = TickingActor::new("actor-1").start(); let actor2 = TickingActor::new("actor-2").start(); + // Set up Ctrl+C handler using send_message_on + send_message_on(actor1.context(), rt::ctrl_c(), Shutdown); + send_message_on(actor2.context(), rt::ctrl_c(), Shutdown); + // Wait for both actors to stop actor1.join(); actor2.join(); diff --git a/examples/updater/src/main.rs b/examples/updater/src/main.rs index 0a6aaf0..2058867 100644 --- a/examples/updater/src/main.rs +++ b/examples/updater/src/main.rs @@ -9,7 +9,7 @@ mod server; use std::{thread, time::Duration}; use server::UpdaterServer; -use spawned_concurrency::tasks::Actor as _; +use spawned_concurrency::tasks::ActorStart as _; use spawned_rt::tasks as rt; fn main() { diff --git a/examples/updater/src/messages.rs b/examples/updater/src/messages.rs index daa0589..5bf74d7 100644 --- a/examples/updater/src/messages.rs +++ b/examples/updater/src/messages.rs @@ -1,11 +1,7 @@ -#[derive(Debug, Clone)] -pub enum UpdaterInMessage { - Check, -} +use spawned_concurrency::message::Message; -#[allow(dead_code)] -#[derive(Debug, Clone, PartialEq)] -pub enum UpdaterOutMessage { - Ok, - Error, +#[derive(Debug, Clone)] +pub struct Check; +impl Message for Check { + type Result = (); } diff --git a/examples/updater/src/server.rs b/examples/updater/src/server.rs index 2c1b02e..6d77f88 100644 --- a/examples/updater/src/server.rs +++ b/examples/updater/src/server.rs @@ -1,18 +1,9 @@ use std::time::Duration; -use spawned_concurrency::{ - messages::Unused, - tasks::{ - send_interval, Actor, ActorRef, - InitResult::{self, Success}, - MessageResponse, - }, -}; +use spawned_concurrency::tasks::{send_interval, Actor, Context, Handler}; use spawned_rt::tasks::CancellationToken; -use crate::messages::{UpdaterInMessage as InMessage, UpdaterOutMessage as OutMessage}; - -type UpdateServerHandle = ActorRef; +use crate::messages::Check; pub struct UpdaterServer { pub url: String, @@ -31,32 +22,18 @@ impl UpdaterServer { } impl Actor for UpdaterServer { - type Request = Unused; - type Message = InMessage; - type Reply = OutMessage; - type Error = std::fmt::Error; - - // Initializing Actor to start periodic checks. - async fn init(mut self, handle: &ActorRef) -> Result, Self::Error> { - let timer = send_interval(self.periodicity, handle.clone(), InMessage::Check); + async fn started(&mut self, ctx: &Context) { + let timer = send_interval(self.periodicity, ctx.clone(), Check); self.timer_token = Some(timer.cancellation_token); - Ok(Success(self)) } +} - async fn handle_message( - &mut self, - message: Self::Message, - _handle: &UpdateServerHandle, - ) -> MessageResponse { - match message { - Self::Message::Check => { - let url = self.url.clone(); - tracing::info!("Fetching: {url}"); - let resp = req(url).await; - tracing::info!("Response: {resp:?}"); - MessageResponse::NoReply - } - } +impl Handler for UpdaterServer { + async fn handle(&mut self, _msg: Check, _ctx: &Context) { + let url = self.url.clone(); + tracing::info!("Fetching: {url}"); + let resp = req(url).await; + tracing::info!("Response: {resp:?}"); } } diff --git a/examples/updater_threads/src/main.rs b/examples/updater_threads/src/main.rs index 5b7ceb3..9711e73 100644 --- a/examples/updater_threads/src/main.rs +++ b/examples/updater_threads/src/main.rs @@ -9,7 +9,7 @@ mod server; use std::{thread, time::Duration}; use server::UpdaterServer; -use spawned_concurrency::threads::Actor as _; +use spawned_concurrency::threads::ActorStart as _; use spawned_rt::threads as rt; fn main() { diff --git a/examples/updater_threads/src/messages.rs b/examples/updater_threads/src/messages.rs index daa0589..5bf74d7 100644 --- a/examples/updater_threads/src/messages.rs +++ b/examples/updater_threads/src/messages.rs @@ -1,11 +1,7 @@ -#[derive(Debug, Clone)] -pub enum UpdaterInMessage { - Check, -} +use spawned_concurrency::message::Message; -#[allow(dead_code)] -#[derive(Debug, Clone, PartialEq)] -pub enum UpdaterOutMessage { - Ok, - Error, +#[derive(Debug, Clone)] +pub struct Check; +impl Message for Check { + type Result = (); } diff --git a/examples/updater_threads/src/server.rs b/examples/updater_threads/src/server.rs index 2a931ff..b427dfa 100644 --- a/examples/updater_threads/src/server.rs +++ b/examples/updater_threads/src/server.rs @@ -1,50 +1,28 @@ use std::time::Duration; -use spawned_concurrency::{ - messages::Unused, - threads::{send_after, Actor, ActorRef, InitResult, MessageResponse}, -}; +use spawned_concurrency::threads::{send_after, Actor, Context, Handler}; use spawned_rt::threads::block_on; -use crate::messages::{UpdaterInMessage as InMessage, UpdaterOutMessage as OutMessage}; +use crate::messages::Check; -type UpdateServerHandle = ActorRef; - -#[derive(Clone)] pub struct UpdaterServer { pub url: String, pub periodicity: Duration, } impl Actor for UpdaterServer { - type Request = Unused; - type Message = InMessage; - type Reply = OutMessage; - type Error = std::fmt::Error; - - // Initializing Actor to start periodic checks. - fn init(self, handle: &ActorRef) -> Result, Self::Error> { - send_after(self.periodicity, handle.clone(), InMessage::Check); - Ok(InitResult::Success(self)) + fn started(&mut self, ctx: &Context) { + send_after(self.periodicity, ctx.clone(), Check); } +} - fn handle_message( - &mut self, - message: Self::Message, - handle: &UpdateServerHandle, - ) -> MessageResponse { - match message { - Self::Message::Check => { - send_after(self.periodicity, handle.clone(), InMessage::Check); - let url = self.url.clone(); - tracing::info!("Fetching: {url}"); - let resp = block_on(req(url)); - - tracing::info!("Response: {resp:?}"); - - MessageResponse::NoReply - } - } +impl Handler for UpdaterServer { + fn handle(&mut self, _msg: Check, ctx: &Context) { + send_after(self.periodicity, ctx.clone(), Check); + let url = self.url.clone(); + tracing::info!("Fetching: {url}"); + let resp = block_on(req(url)); + tracing::info!("Response: {resp:?}"); } } From 34bf9a759cda72e5311efda8f1fc8a5ae515129a Mon Sep 17 00:00:00 2001 From: Esteban Dimitroff Hodi Date: Tue, 10 Feb 2026 12:26:24 -0300 Subject: [PATCH 6/6] feat: add chat_room example demonstrating Recipient circular dependency solution --- Cargo.lock | 9 ++++ Cargo.toml | 1 + examples/chat_room/Cargo.toml | 13 +++++ examples/chat_room/src/main.rs | 77 ++++++++++++++++++++++++++++++ examples/chat_room/src/messages.rs | 32 +++++++++++++ examples/chat_room/src/room.rs | 43 +++++++++++++++++ examples/chat_room/src/user.rs | 29 +++++++++++ 7 files changed, 204 insertions(+) create mode 100644 examples/chat_room/Cargo.toml create mode 100644 examples/chat_room/src/main.rs create mode 100644 examples/chat_room/src/messages.rs create mode 100644 examples/chat_room/src/room.rs create mode 100644 examples/chat_room/src/user.rs diff --git a/Cargo.lock b/Cargo.lock index 4d962a6..978e604 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -120,6 +120,15 @@ version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "613afe47fcd5fac7ccf1db93babcb082c5994d996f20b8b159f2ad1658eb5724" +[[package]] +name = "chat_room" +version = "0.1.0" +dependencies = [ + "spawned-concurrency", + "spawned-rt", + "tracing", +] + [[package]] name = "core-foundation" version = "0.9.4" diff --git a/Cargo.toml b/Cargo.toml index f234fe4..7bbb458 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -15,6 +15,7 @@ members = [ "examples/busy_genserver_warning", "examples/signal_test", "examples/signal_test_threads", + "examples/chat_room", ] [workspace.dependencies] diff --git a/examples/chat_room/Cargo.toml b/examples/chat_room/Cargo.toml new file mode 100644 index 0000000..4fb3882 --- /dev/null +++ b/examples/chat_room/Cargo.toml @@ -0,0 +1,13 @@ +[package] +name = "chat_room" +version = "0.1.0" +edition = "2021" + +[dependencies] +spawned-rt = { workspace = true } +spawned-concurrency = { workspace = true } +tracing = { workspace = true } + +[[bin]] +name = "chat_room" +path = "src/main.rs" diff --git a/examples/chat_room/src/main.rs b/examples/chat_room/src/main.rs new file mode 100644 index 0000000..3986e52 --- /dev/null +++ b/examples/chat_room/src/main.rs @@ -0,0 +1,77 @@ +//! Chat room example demonstrating how `Recipient` solves circular dependencies. +//! +//! The problem: +//! - `ChatRoom` needs to send `Deliver` to each `User` +//! - `User` needs to send `Say` to the `ChatRoom` +//! - With concrete types, `room.rs` would import `User` and +//! `user.rs` would import `ChatRoom` — circular module dependency. +//! +//! The solution: +//! - `ChatRoom` holds `Recipient` — doesn't know about `User` +//! - `User` holds `Recipient` — doesn't know about `ChatRoom` +//! - Both modules only depend on the shared `messages` module. +//! +//! Message flow: +//! main -> SayToRoom -> User -> Say -> ChatRoom -> Deliver -> User + +mod messages; +mod room; +mod user; + +use messages::{Deliver, Join, SayToRoom}; +use room::ChatRoom; +use spawned_concurrency::tasks::ActorStart as _; +use spawned_rt::tasks as rt; +use std::time::Duration; +use user::User; + +fn main() { + rt::run(async { + let room = ChatRoom::new().start(); + + let alice = User { + name: "Alice".into(), + room: room.recipient(), + } + .start(); + + let bob = User { + name: "Bob".into(), + room: room.recipient(), + } + .start(); + + // Register users — room stores Recipient, not ActorRef + room.send_request(Join { + name: "Alice".into(), + inbox: alice.recipient::(), + }) + .await + .unwrap(); + + room.send_request(Join { + name: "Bob".into(), + inbox: bob.recipient::(), + }) + .await + .unwrap(); + + // Alice speaks: main -> alice (SayToRoom) -> room (Say) -> bob (Deliver) + alice + .send_request(SayToRoom { + text: "Hello everyone!".into(), + }) + .await + .unwrap(); + + // Bob replies: main -> bob (SayToRoom) -> room (Say) -> alice (Deliver) + bob.send_request(SayToRoom { + text: "Hey Alice!".into(), + }) + .await + .unwrap(); + + // Let deliveries propagate + rt::sleep(Duration::from_millis(50)).await; + }) +} diff --git a/examples/chat_room/src/messages.rs b/examples/chat_room/src/messages.rs new file mode 100644 index 0000000..2bbe642 --- /dev/null +++ b/examples/chat_room/src/messages.rs @@ -0,0 +1,32 @@ +//! Shared message types — the only thing both actors need to agree on. +//! +//! Neither `ChatRoom` nor `User` appears here. The circular dependency +//! is broken because each actor holds a `Recipient` (type-erased) +//! instead of a concrete `ActorRef`. + +use spawned_concurrency::message::Message; +use spawned_concurrency::messages; +use spawned_concurrency::tasks::Recipient; + +// Join carries a Recipient, so we define it manually (Recipient doesn't impl Debug) +pub struct Join { + pub name: String, + pub inbox: Recipient, +} + +impl Message for Join { + type Result = (); +} + +impl std::fmt::Debug for Join { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.debug_struct("Join").field("name", &self.name).finish() + } +} + +// The rest use the macro +messages! { + Say { from: String, text: String } -> (); + SayToRoom { text: String } -> (); + Deliver { from: String, text: String } -> (); +} diff --git a/examples/chat_room/src/room.rs b/examples/chat_room/src/room.rs new file mode 100644 index 0000000..385cda0 --- /dev/null +++ b/examples/chat_room/src/room.rs @@ -0,0 +1,43 @@ +//! ChatRoom actor — knows about `Join`, `Say`, and `Recipient`. +//! Does NOT know about the `User` type. + +use spawned_concurrency::tasks::{Actor, Context, Handler, Recipient}; + +use crate::messages::{Deliver, Join, Say}; + +pub struct ChatRoom { + members: Vec<(String, Recipient)>, +} + +impl ChatRoom { + pub fn new() -> Self { + Self { + members: Vec::new(), + } + } +} + +impl Actor for ChatRoom {} + +impl Handler for ChatRoom { + async fn handle(&mut self, msg: Join, _ctx: &Context) { + tracing::info!("[room] {} joined", msg.name); + self.members.push((msg.name, msg.inbox)); + } +} + +impl Handler for ChatRoom { + async fn handle(&mut self, msg: Say, _ctx: &Context) { + tracing::info!("[room] {} says: {}", msg.from, msg.text); + + // Broadcast to all members except the sender + for (name, inbox) in &self.members { + if *name != msg.from { + let _ = inbox.send(Deliver { + from: msg.from.clone(), + text: msg.text.clone(), + }); + } + } + } +} diff --git a/examples/chat_room/src/user.rs b/examples/chat_room/src/user.rs new file mode 100644 index 0000000..3cd6772 --- /dev/null +++ b/examples/chat_room/src/user.rs @@ -0,0 +1,29 @@ +//! User actor — knows about `Say`, `SayToRoom`, `Deliver`, and `Recipient`. +//! Does NOT know about the `ChatRoom` type. + +use spawned_concurrency::tasks::{Actor, Context, Handler, Recipient}; + +use crate::messages::{Deliver, Say, SayToRoom}; + +pub struct User { + pub name: String, + pub room: Recipient, +} + +impl Actor for User {} + +impl Handler for User { + async fn handle(&mut self, msg: SayToRoom, _ctx: &Context) { + // Forward to the room via Recipient — no ChatRoom type needed + let _ = self.room.send(Say { + from: self.name.clone(), + text: msg.text, + }); + } +} + +impl Handler for User { + async fn handle(&mut self, msg: Deliver, _ctx: &Context) { + tracing::info!("[{}] got message from {}: {}", self.name, msg.from, msg.text); + } +}