├── theta ├── tests │ ├── test_ping_pong.rs │ └── test_persistence.rs ├── src │ ├── dev │ │ ├── mod.rs │ │ └── macro_dev.rs │ ├── .DS_Store │ ├── persistence │ │ ├── storages │ │ │ ├── mod.rs │ │ │ └── project_dir.rs │ │ ├── mod.rs │ │ └── persistent_actor.rs │ ├── remote │ │ ├── mod.rs │ │ ├── base.rs │ │ ├── network.rs │ │ └── serde.rs │ ├── lib.rs │ ├── base.rs │ ├── message.rs │ ├── context.rs │ ├── actor.rs │ └── actor_instance.rs ├── .DS_Store ├── examples │ ├── readme_counter.rs │ ├── tell_error.rs │ ├── host.rs │ ├── monitored.rs │ ├── ping_pong.rs │ ├── monitor.rs │ ├── forward_ping_pong.rs │ ├── client.rs │ ├── ping_pong_bench.rs │ └── dedup.rs └── Cargo.toml ├── .gitignore ├── .DS_Store ├── Cargo.toml ├── .cargo └── config.toml ├── theta-macros ├── Cargo.toml └── src │ ├── logging.rs │ └── lib.rs ├── LICENSE ├── workflow ├── pre_publish_tests.sh ├── test_features.py └── _DOCUMENTATION_STYLE_GUIDE.md ├── README.md └── child_process.log /theta/tests/test_ping_pong.rs: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | **/.DS_Store 2 | **/.vscode 3 | **/target -------------------------------------------------------------------------------- /.DS_Store: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cwahn/theta/HEAD/.DS_Store -------------------------------------------------------------------------------- /theta/src/dev/mod.rs: -------------------------------------------------------------------------------- 1 | #[cfg(feature = "remote")] 2 | mod macro_dev; 3 | -------------------------------------------------------------------------------- /theta/.DS_Store: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cwahn/theta/HEAD/theta/.DS_Store -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [workspace] 2 | resolver = "3" 3 | members = ["theta", "theta-macros"] 4 | -------------------------------------------------------------------------------- /theta/src/.DS_Store: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cwahn/theta/HEAD/theta/src/.DS_Store -------------------------------------------------------------------------------- /theta/src/persistence/storages/mod.rs: -------------------------------------------------------------------------------- 1 | #[cfg(feature = "project_dir")] 2 | pub mod project_dir; 3 | -------------------------------------------------------------------------------- /.cargo/config.toml: -------------------------------------------------------------------------------- 1 | [target.wasm32-unknown-unknown] 2 | rustflags = [ 3 | "--cfg", 4 | "getrandom_backend=\"wasm_js\"", 5 | ] # Necessary for WASM support 6 | runner = "wasmtime run --dir=." 7 | -------------------------------------------------------------------------------- /theta/src/remote/mod.rs: -------------------------------------------------------------------------------- 1 | //! Remote actor communication over peer-to-peer networks. 2 | //! 3 | //! This module enables distributed actor systems using the [iroh] P2P networking library. 4 | //! Actors can communicate across network boundaries as if they were local. 5 | //! 6 | //! [iroh]: https://iroh.computer/ 7 | 8 | pub mod base; 9 | pub mod network; 10 | pub mod peer; 11 | pub mod serde; 12 | -------------------------------------------------------------------------------- /theta-macros/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "theta-macros" 3 | version = "0.1.0-alpha.43" 4 | edition = "2024" 5 | authors = ["Chanwoo Ahn "] 6 | license = "MIT" 7 | description = "Procedural macros for the Theta actor framework" 8 | repository = "https://github.com/cwahn/theta" 9 | documentation = "https://docs.rs/theta-macros" 10 | readme = "../README.md" 11 | keywords = ["async", "actor", "macros", "concurrency"] 12 | categories = ["asynchronous", "concurrency"] 13 | 14 | # proc macro package 15 | [lib] 16 | proc-macro = true 17 | 18 | [features] 19 | default = [] 20 | persistence = [] 21 | remote = [] 22 | monitor = [] 23 | 24 | 25 | [dependencies] 26 | heck = "0.5.0" 27 | proc-macro2 = "1.0.95" 28 | quote = "1.0.40" 29 | syn = { version = "2.0.104", features = ["full", "fold", "extra-traits"] } 30 | log = "0.4" 31 | -------------------------------------------------------------------------------- /theta/examples/readme_counter.rs: -------------------------------------------------------------------------------- 1 | use serde::{Deserialize, Serialize}; 2 | use theta::prelude::*; 3 | 4 | #[derive(Debug, Clone, ActorArgs)] 5 | struct Counter { 6 | value: i64, 7 | } 8 | 9 | // #[derive(Debug, Clone, Serialize, Deserialize)] 10 | // pub struct Inc(i64); 11 | 12 | #[derive(Debug, Clone, Serialize, Deserialize)] 13 | pub struct GetValue; 14 | 15 | #[actor("96d9901f-24fc-4d82-8eb8-023153d41074")] 16 | impl Actor for Counter { 17 | // Behaviors will generate single enum Msg for the actor 18 | const _: () = async |amount: i64| { 19 | self.value += amount; 20 | }; 21 | 22 | const _: () = async |_: GetValue| -> i64 { self.value }; 23 | } 24 | 25 | #[tokio::main] 26 | async fn main() -> anyhow::Result<()> { 27 | let ctx = RootContext::init_local(); 28 | let counter = ctx.spawn(Counter { value: 0 }); 29 | 30 | let _ = counter.tell(5); // Fire-and-forget 31 | 32 | let current = counter.ask(GetValue).await?; // Wait for response 33 | println!("Current value: {current}"); // Current value: 5 34 | 35 | Ok(()) 36 | } 37 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2025 lighthouse 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /workflow/pre_publish_tests.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | set -e 3 | 4 | echo "==> Testing all feature combinations" 5 | python3 workflow/test_features.py 6 | 7 | echo "==> Checking examples compile" 8 | cargo check --examples --all-features 9 | 10 | echo "==> Checking documentation build" 11 | cargo doc --all-features --no-deps 12 | 13 | echo "==> Checking formatting and linting" 14 | cargo fmt -- --check 15 | cargo clippy --all-features -- -D warnings 16 | 17 | echo "==> Dry-run publishing for theta" 18 | cd theta 19 | cargo publish --dry-run --allow-dirty 20 | cd .. 21 | 22 | echo "==> Dry-run publishing for theta-macros" 23 | cd theta-macros 24 | cargo publish --dry-run --allow-dirty 25 | cd .. 26 | 27 | echo "==> All checks complete!" 28 | 29 | echo "==> Checking documentation build" 30 | cargo doc --all-features --no-deps 31 | 32 | echo "==> Checking formatting and linting" 33 | cargo fmt -- --check 34 | cargo clippy --all-features -- -D warnings 35 | 36 | echo "==> Dry-run publishing for theta" 37 | cd theta 38 | cargo publish --dry-run --allow-dirty 39 | cd .. 40 | 41 | echo "==> Dry-run publishing for theta-macros" 42 | cd theta-macros 43 | cargo publish --dry-run --allow-dirty 44 | cd .. 45 | 46 | echo "==> All checks complete!" -------------------------------------------------------------------------------- /theta/src/persistence/storages/project_dir.rs: -------------------------------------------------------------------------------- 1 | use crate::persistence::persistent_actor::PersistentStorage; 2 | use std::{ 3 | path::{Path, PathBuf}, 4 | sync::OnceLock, 5 | }; 6 | 7 | /// Cached project data directory path for local filesystem storage. 8 | static DATA_DIR: OnceLock = OnceLock::new(); 9 | 10 | /// Local filesystem storage backend for actor persistence. 11 | pub struct LocalFs; 12 | 13 | // Implementation 14 | 15 | impl LocalFs { 16 | pub fn init(path: &Path) { 17 | DATA_DIR 18 | .set(path.to_path_buf()) 19 | .expect("Failed to set project directory"); 20 | } 21 | 22 | pub fn inst() -> &'static PathBuf { 23 | DATA_DIR 24 | .get() 25 | .expect("ProjectDir should be initialized before accessing it") 26 | } 27 | 28 | fn path(&self, id: crate::actor::ActorId) -> std::path::PathBuf { 29 | LocalFs::inst().join(format!("{}", id)) 30 | } 31 | } 32 | 33 | impl PersistentStorage for LocalFs { 34 | async fn try_read(&self, id: crate::actor::ActorId) -> Result, anyhow::Error> { 35 | Ok(tokio::fs::read(self.path(id)).await?) 36 | } 37 | 38 | async fn try_write( 39 | &self, 40 | id: crate::actor::ActorId, 41 | bytes: Vec, 42 | ) -> Result<(), anyhow::Error> { 43 | let path = self.path(id); 44 | 45 | if let Some(parent) = path.parent() { 46 | tokio::fs::create_dir_all(parent).await?; 47 | } 48 | 49 | Ok(tokio::fs::write(&path, bytes).await?) 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /theta-macros/src/logging.rs: -------------------------------------------------------------------------------- 1 | //! Logging macros for conditional tracing support. 2 | //! 3 | //! These proc macros generate macro_rules! definitions that conditionally 4 | //! use the tracing crate when the "tracing" feature is enabled. 5 | 6 | use proc_macro::TokenStream; 7 | use quote::quote; 8 | 9 | /// Generate logging macros that conditionally use tracing. 10 | /// This is a function-like macro that generates all logging macros at once. 11 | #[proc_macro] 12 | pub fn generate_logging_macros(_input: TokenStream) -> TokenStream { 13 | let tokens = quote! { 14 | macro_rules! trace { 15 | ($($arg:tt)*) => { 16 | ::theta::__private::tracing::trace!($($arg)*) 17 | }; 18 | } 19 | 20 | macro_rules! debug { 21 | ($($arg:tt)*) => { 22 | ::theta::__private::tracing::debug!($($arg)*) 23 | }; 24 | } 25 | 26 | macro_rules! info { 27 | ($($arg:tt)*) => { 28 | ::theta::__private::tracing::info!($($arg)*) 29 | }; 30 | } 31 | 32 | macro_rules! warn { 33 | ($($arg:tt)*) => { 34 | ::theta::__private::tracing::warn!($($arg)*) 35 | }; 36 | } 37 | 38 | macro_rules! error { 39 | ($($arg:tt)*) => { 40 | ::theta::__private::tracing::error!($($arg)*) 41 | }; 42 | } 43 | 44 | pub(crate) use trace; 45 | pub(crate) use debug; 46 | pub(crate) use info; 47 | pub(crate) use warn; 48 | pub(crate) use error; 49 | }; 50 | 51 | tokens.into() 52 | } 53 | -------------------------------------------------------------------------------- /theta/examples/tell_error.rs: -------------------------------------------------------------------------------- 1 | use serde::{Deserialize, Serialize}; 2 | use theta::prelude::*; 3 | use thiserror::Error; 4 | use tracing::info; 5 | 6 | #[derive(Debug, Clone, ActorArgs)] 7 | struct SomeActor; 8 | 9 | #[derive(Debug, Clone, Serialize, Deserialize)] 10 | pub struct NoError; 11 | 12 | #[derive(Debug, Clone, Serialize, Deserialize)] 13 | pub struct ErrorResult; 14 | 15 | #[derive(Debug, Clone, Serialize, Deserialize)] 16 | pub struct DisplayResult; 17 | 18 | #[derive(Debug, Clone, Serialize, Deserialize)] 19 | pub struct NoneDisplayResult; 20 | 21 | #[derive(Debug, Error, Serialize, Deserialize)] 22 | #[error("a simple error")] 23 | pub struct SimpleError; 24 | 25 | #[actor("96d9901f-24fc-4d82-8eb8-023153d41074")] 26 | impl Actor for SomeActor { 27 | // Behaviors will generate single enum Msg for the actor 28 | const _: () = { 29 | async |_: NoError| -> u64 { 42 }; 30 | 31 | async |_: ErrorResult| -> Result { Err(SimpleError) }; 32 | 33 | async |_: DisplayResult| -> Result { 34 | Err("a string error occurred".to_string()) 35 | }; 36 | 37 | async |_: NoneDisplayResult| -> Result { Err(()) } 38 | }; 39 | } 40 | 41 | #[tokio::main] 42 | async fn main() -> anyhow::Result<()> { 43 | tracing_subscriber::fmt().with_env_filter("trace").init(); 44 | tracing_log::LogTracer::init().ok(); 45 | 46 | let ctx = RootContext::init_local(); 47 | let actor = ctx.spawn(SomeActor); 48 | 49 | // tells 50 | info!("result::Err returned from `tell` will be printed out as tracing::error"); 51 | info!("two lines of error expected"); 52 | let _ = actor.tell(NoError); 53 | let _ = actor.tell(ErrorResult); 54 | let _ = actor.tell(DisplayResult); 55 | let _ = actor.tell(NoneDisplayResult); 56 | 57 | tokio::time::sleep(std::time::Duration::from_millis(100)).await; 58 | 59 | info!("`ask` does not log error, it should be handled by caller"); 60 | let _ = actor.ask(NoError).await?; 61 | let _ = actor.ask(ErrorResult).await; 62 | let _ = actor.ask(DisplayResult).await; 63 | let _ = actor.ask(NoneDisplayResult).await; 64 | 65 | Ok(()) 66 | } 67 | -------------------------------------------------------------------------------- /theta/src/dev/macro_dev.rs: -------------------------------------------------------------------------------- 1 | use std::collections::HashMap; 2 | 3 | use serde::{Deserialize, Serialize}; 4 | 5 | use theta_macros::{ActorArgs, actor}; 6 | 7 | use crate::{actor::Actor, context::RootContext, message::Continuation, prelude::ActorRef}; 8 | 9 | #[derive(Debug, Clone, ActorArgs)] 10 | pub struct Manager { 11 | workers: HashMap>, 12 | } 13 | 14 | #[derive(Debug, Clone, Serialize, Deserialize)] 15 | pub struct CreateWorker { 16 | pub name: String, 17 | } 18 | 19 | #[derive(Debug, Clone, Serialize, Deserialize)] 20 | pub struct GetWorker { 21 | pub name: String, 22 | } 23 | 24 | #[derive(Debug, Clone, ActorArgs)] 25 | pub struct Worker {} 26 | 27 | #[actor("27ca7f4a-f2f7-4644-8ff9-4bdd8f40b5cd")] 28 | impl Actor for Worker {} 29 | 30 | #[actor("d89de30e-79c5-49b6-9c16-0903ac576277")] 31 | impl Actor for Manager { 32 | const _: () = { 33 | async |msg: CreateWorker| -> () { 34 | println!("Creating worker with name: {}", msg.name); 35 | 36 | let worker = ctx.spawn(Worker {}); 37 | 38 | self.workers.insert(msg.name.clone(), worker.clone()); 39 | }; 40 | 41 | async |GetWorker { name }| -> Option> { 42 | println!("Getting worker with name: {name}"); 43 | 44 | if let Some(worker) = self.workers.get(&name).cloned() { 45 | Some(worker) 46 | } else { 47 | println!("Worker with name {name} not found"); 48 | None 49 | } 50 | }; 51 | }; 52 | } 53 | 54 | #[tokio::test] 55 | async fn test_manager_behavior() { 56 | let ctx = RootContext::default(); 57 | 58 | let msgs: Vec<::Msg> = vec![ 59 | CreateWorker { 60 | name: "Worker1".to_string(), 61 | } 62 | .into(), 63 | GetWorker { 64 | name: "Worker1".to_string(), 65 | } 66 | .into(), 67 | ]; 68 | 69 | let serialized_msgs = msgs 70 | .iter() 71 | .map(|m| postcard::to_allocvec_cobs(m).unwrap()) 72 | .collect::>(); 73 | 74 | let deserialized_msgs: Vec<::Msg> = serialized_msgs 75 | .into_iter() 76 | .map(|mut m| postcard::from_bytes_cobs(m.as_mut_slice()).unwrap()) 77 | .collect(); 78 | 79 | let manager = ctx.spawn(Manager { 80 | workers: HashMap::new(), 81 | }); 82 | 83 | for msg in deserialized_msgs { 84 | let _ = manager.send(msg, Continuation::Nil); 85 | } 86 | } 87 | -------------------------------------------------------------------------------- /theta/examples/host.rs: -------------------------------------------------------------------------------- 1 | use std::hash::{Hash, Hasher}; 2 | 3 | use iroh::Endpoint; 4 | use serde::{Deserialize, Serialize}; 5 | use theta::prelude::*; 6 | use theta_macros::ActorArgs; 7 | use tracing::info; 8 | use tracing_subscriber::fmt::time::ChronoLocal; 9 | 10 | #[derive(Debug, Clone, Serialize, Deserialize)] 11 | pub struct Inc; 12 | #[derive(Debug, Clone, Serialize, Deserialize)] 13 | pub struct Dec; 14 | 15 | #[derive(Debug, Clone, ActorArgs)] 16 | pub struct Counter { 17 | pub value: i64, 18 | } 19 | 20 | #[actor("96d9901f-24fc-4d82-8eb8-023153d41074")] 21 | impl Actor for Counter { 22 | type View = i64; 23 | 24 | const _: () = { 25 | async |_: Inc| -> i64 { 26 | self.value += 1; 27 | self.value 28 | }; 29 | 30 | async |_: Dec| -> i64 { 31 | self.value -= 1; 32 | self.value 33 | }; 34 | }; 35 | 36 | fn hash_code(&self) -> u64 { 37 | let mut hasher = rustc_hash::FxHasher::default(); 38 | self.value.hash(&mut hasher); 39 | hasher.finish() 40 | } 41 | } 42 | 43 | impl From<&Counter> for i64 { 44 | fn from(counter: &Counter) -> Self { 45 | counter.value 46 | } 47 | } 48 | 49 | #[derive(Debug, Clone, Serialize, Deserialize)] 50 | pub struct GetWorker; 51 | 52 | #[derive(Debug, Clone, Hash, Serialize, Deserialize, ActorArgs)] 53 | pub struct Manager { 54 | pub worker: ActorRef, 55 | } 56 | 57 | #[actor("f65b84e6-adfe-4d3a-8140-ee55de512070")] 58 | impl Actor for Manager { 59 | type View = Self; 60 | 61 | const _: () = { 62 | // expose the worker via ask 63 | async |_: GetWorker| -> ActorRef { self.worker.clone() }; 64 | }; 65 | } 66 | 67 | #[tokio::main] 68 | async fn main() -> anyhow::Result<()> { 69 | tracing_subscriber::fmt() 70 | .with_env_filter("info,theta=trace") 71 | .with_timer(ChronoLocal::new("%H:%M:%S".into())) 72 | .compact() 73 | .init(); 74 | 75 | tracing_log::LogTracer::init().ok(); 76 | 77 | let endpoint = Endpoint::builder() 78 | .alpns(vec![b"theta".to_vec()]) 79 | .discovery_n0() 80 | .bind() 81 | .await?; 82 | 83 | let ctx = RootContext::init(endpoint); 84 | 85 | // spawn worker counter (start at 0) and manager that wraps it 86 | let worker = ctx.spawn(Counter { value: 0 }); 87 | let manager = ctx.spawn(Manager { worker: worker }); 88 | 89 | // bind a discoverable name for the manager 90 | info!("binding manager actor to 'manager' name..."); 91 | let _ = ctx.bind("manager", manager); 92 | 93 | println!("host ready. public key: {}", ctx.public_key()); 94 | 95 | // park forever 96 | futures::future::pending::<()>().await; 97 | Ok(()) 98 | } 99 | -------------------------------------------------------------------------------- /theta/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "theta" 3 | version = "0.1.0-alpha.45" 4 | edition = "2024" 5 | authors = ["Chanwoo Ahn "] 6 | license = "MIT" 7 | description = "An Rust Actor Framework" 8 | repository = "https://github.com/cwahn/theta" 9 | documentation = "https://docs.rs/theta" 10 | readme = "../README.md" 11 | keywords = ["async", "actor", "concurrency"] 12 | categories = ["asynchronous", "concurrency"] 13 | exclude = ["benches/", "examples/", ".github/", "*.tmp"] 14 | 15 | [features] 16 | default = ["macros", "remote", "monitor", "persistence", "project_dir"] 17 | macros = [] 18 | monitor = ["theta-macros/monitor"] 19 | remote = ["theta-macros/remote", "iroh", "postcard", "url"] 20 | persistence = ["theta-macros/persistence", "url", "postcard"] 21 | verbose = [] # Enable verbose logging in hot paths 22 | 23 | # persistent storage 24 | project_dir = ["tokio/fs"] 25 | 26 | 27 | [dependencies] 28 | tracing = "0.1.41" 29 | futures = "0.3.31" 30 | serde = { version = "1.0.219", features = ["derive"] } 31 | tokio = { version = "1.46.1", features = [ 32 | "sync", 33 | "rt", 34 | "time", 35 | "macros", 36 | "rt-multi-thread", 37 | ] } 38 | anyhow = "1.0.98" # todo Use custom error 39 | thiserror = "2.0.14" 40 | rand = "0.8" 41 | rustc-hash = "2.1.1" 42 | uuid = { version = "1.17.0", features = ["v4", "serde"] } 43 | theta-macros = { version = "0.1.0-alpha.43", path = "../theta-macros" } 44 | spez = "0.1.2" 45 | theta-flume = "0.11.7" 46 | 47 | postcard = { version = "1.1.2", features = ["use-std"], optional = true } 48 | url = { version = "2.5.4", features = ["serde"], optional = true } 49 | iroh = { version = "0.92.0", default-features = false, optional = true } 50 | 51 | # WASM support 52 | [target.'cfg(not(target_arch = "wasm32"))'.dependencies] 53 | dashmap = "6.1.0" # todo Use regular HashMap with lock instead on WASM 54 | 55 | [target.'cfg(target_arch = "wasm32")'.dependencies] 56 | getrandom = { version = "0.3", features = ["wasm_js"] } 57 | uuid = { version = "1.17.0", features = ["v4", "serde", "rng-getrandom"] } 58 | 59 | [dev-dependencies] 60 | tracing = "0.1.41" 61 | tracing-subscriber = { version = "0.3.19", features = ["env-filter", "chrono"] } 62 | tracing-log = "0.2.0" 63 | tempfile = "3.20.0" 64 | crossterm = "0.29.0" 65 | 66 | [[example]] 67 | name = "ping_pong" 68 | required-features = ["macros", "remote"] 69 | 70 | [[example]] 71 | name = "readme_counter" 72 | required-features = ["macros"] 73 | 74 | [[example]] 75 | name = "host" 76 | required-features = ["macros", "remote"] 77 | 78 | [[example]] 79 | name = "client" 80 | required-features = ["macros", "remote", "monitor"] 81 | 82 | [[example]] 83 | name = "monitor" 84 | required-features = ["macros", "remote", "monitor"] 85 | 86 | [[example]] 87 | name = "monitored" 88 | required-features = ["macros", "remote", "monitor"] 89 | 90 | [[example]] 91 | name = "ping_pong_bench" 92 | required-features = ["macros", "remote"] 93 | 94 | [[example]] 95 | name = "forward_ping_pong" 96 | required-features = ["macros", "remote"] 97 | 98 | [[example]] 99 | name = "tell_error" 100 | required-features = ["macros"] 101 | 102 | [[example]] 103 | name = "dedup" 104 | required-features = ["macros", "remote"] 105 | -------------------------------------------------------------------------------- /theta/examples/monitored.rs: -------------------------------------------------------------------------------- 1 | use iroh::Endpoint; 2 | use rustc_hash::FxHasher; 3 | use serde::{Deserialize, Serialize}; 4 | use std::{ 5 | hash::{Hash, Hasher}, 6 | vec, 7 | }; 8 | use theta::prelude::*; 9 | use theta_macros::ActorArgs; 10 | use tracing::{error, info}; 11 | 12 | use tracing_subscriber::fmt::time::ChronoLocal; 13 | 14 | #[derive(Debug, Clone, Hash, ActorArgs, Serialize, Deserialize)] 15 | pub struct Counter { 16 | value: i64, 17 | } 18 | 19 | impl Counter { 20 | pub fn new() -> Self { 21 | Self { value: 0 } 22 | } 23 | } 24 | 25 | #[derive(Debug, Clone, Serialize, Deserialize)] 26 | pub struct Inc { 27 | pub amount: i64, 28 | } 29 | 30 | #[derive(Debug, Clone, Serialize, Deserialize)] 31 | pub struct Dec { 32 | pub amount: i64, 33 | } 34 | 35 | #[derive(Debug, Clone, Serialize, Deserialize)] 36 | pub struct CounterResponse { 37 | pub new_value: i64, 38 | } 39 | 40 | #[actor("a1b2c3d4-5e6f-7890-abcd-ef1234567890")] 41 | impl Actor for Counter { 42 | type View = Counter; 43 | 44 | const _: () = { 45 | async |msg: Inc| -> CounterResponse { 46 | let new_value = self.value + msg.amount; 47 | self.value = new_value; 48 | info!("counter incremented by {} to {}", msg.amount, new_value); 49 | CounterResponse { new_value } 50 | }; 51 | }; 52 | 53 | const _: () = { 54 | async |msg: Dec| -> CounterResponse { 55 | let new_value = self.value - msg.amount; 56 | self.value = new_value; 57 | info!("counter decremented by {} to {}", msg.amount, new_value); 58 | CounterResponse { new_value } 59 | }; 60 | }; 61 | 62 | fn hash_code(&self) -> u64 { 63 | let mut hasher = FxHasher::default(); 64 | Hash::hash(self, &mut hasher); 65 | hasher.finish() 66 | } 67 | } 68 | 69 | #[tokio::main] 70 | async fn main() -> anyhow::Result<()> { 71 | tracing_subscriber::fmt() 72 | .with_env_filter("info,theta=trace") 73 | .with_timer(ChronoLocal::new("%y%m%d %H:%M:%S%.3f %Z".into())) 74 | .init(); 75 | tracing_log::LogTracer::init().ok(); 76 | 77 | info!("initializing RootContext..."); 78 | let endpoint = Endpoint::builder() 79 | .alpns(vec![b"theta".to_vec()]) 80 | .discovery_n0() 81 | .bind() 82 | .await?; 83 | 84 | let ctx = RootContext::init(endpoint); 85 | let public_key = ctx.public_key(); 86 | info!("rootContext initialized with public key: {public_key}"); 87 | 88 | info!("spawning Counter actor..."); 89 | let counter = ctx.spawn(Counter::new()); 90 | 91 | info!("binding Counter actor to 'counter' name..."); 92 | let _ = ctx.bind("counter", counter.clone()); 93 | 94 | info!("counter actor is now running and bound to 'counter'"); 95 | info!("public key: {public_key}"); 96 | info!("ready to receive Inc/Dec messages. Press Ctrl-C to stop."); 97 | 98 | // Keep the application running 99 | loop { 100 | tokio::time::sleep(tokio::time::Duration::from_secs(1)).await; 101 | 102 | if let Err(e) = counter.tell(Inc { amount: 1 }) { 103 | error!("failed to send Inc message: {e}"); 104 | return Err(e.into()); 105 | } 106 | } 107 | } 108 | -------------------------------------------------------------------------------- /theta/examples/ping_pong.rs: -------------------------------------------------------------------------------- 1 | use std::{str::FromStr, time::Instant, vec}; 2 | 3 | use iroh::{Endpoint, PublicKey, dns::DnsResolver}; 4 | use serde::{Deserialize, Serialize}; 5 | use theta::prelude::*; 6 | use theta_macros::ActorArgs; 7 | // use theta_macros::{ActorConfig, impl_id}; 8 | use tracing::{error, info}; 9 | 10 | use tracing_subscriber::fmt::time::ChronoLocal; 11 | use url::Url; 12 | 13 | #[derive(Debug, Clone, ActorArgs)] 14 | pub struct PingPong; 15 | 16 | #[derive(Debug, Clone, Serialize, Deserialize)] 17 | pub struct Ping { 18 | pub source: PublicKey, 19 | } 20 | 21 | #[derive(Debug, Clone, Serialize, Deserialize)] 22 | pub struct Pong {} 23 | 24 | #[actor("f68fe56f-8aa9-4f90-8af8-591a06e2818a")] 25 | impl Actor for PingPong { 26 | const _: () = async |msg: Ping| -> Pong { 27 | info!("received ping from {}", msg.source); 28 | Pong {} 29 | }; 30 | } 31 | 32 | #[tokio::main] 33 | async fn main() -> anyhow::Result<()> { 34 | // Initialize tracing subscriber first, then LogTracer 35 | tracing_subscriber::fmt() 36 | .with_env_filter("info,theta=trace") 37 | .with_timer(ChronoLocal::new("%Y-%m-%d %H:%M:%S%.3f %Z".into())) 38 | .init(); 39 | 40 | tracing_log::LogTracer::init().ok(); 41 | 42 | info!("initializing RootContext..."); 43 | 44 | let dns = DnsResolver::with_nameserver("8.8.8.8:53".parse().unwrap()); 45 | let endpoint = Endpoint::builder() 46 | .alpns(vec![b"theta".to_vec()]) 47 | .discovery_n0() 48 | .dns_resolver(dns) // Required for mobile hotspot support 49 | .bind() 50 | .await?; 51 | 52 | let ctx = RootContext::init(endpoint); 53 | let public_key = ctx.public_key(); 54 | 55 | info!("rootContext initialized with public key: {public_key}"); 56 | 57 | info!("spawning PingPong actor..."); 58 | let ping_pong = ctx.spawn(PingPong); 59 | 60 | info!("binding PingPong actor to 'ping_pong' name..."); 61 | let _ = ctx.bind("ping_pong", ping_pong); 62 | 63 | // Ask for user of other peer's public key 64 | info!("please enter the public key of the other peer:"); 65 | 66 | let mut input = String::new(); 67 | let other_public_key = loop { 68 | std::io::stdin().read_line(&mut input)?; 69 | let trimmed = input.trim(); 70 | if trimmed.is_empty() { 71 | error!("public key cannot be empty. Please try again."); 72 | input.clear(); 73 | continue; 74 | } 75 | 76 | match PublicKey::from_str(trimmed) { 77 | Err(e) => { 78 | error!("invalid public key format: {e}"); 79 | input.clear(); 80 | } 81 | Ok(key) => break key, 82 | }; 83 | }; 84 | 85 | let ping_pong_url = Url::parse(&format!("iroh://ping_pong@{other_public_key}"))?; 86 | 87 | let other_ping_pong = match ActorRef::::lookup(&ping_pong_url).await { 88 | Err(e) => { 89 | error!("failed to find PingPong actor at URL: {ping_pong_url}. Error: {e}"); 90 | return Ok(()); 91 | } 92 | Ok(actor) => actor, 93 | }; 94 | 95 | info!("sending ping to {ping_pong_url} every 5 seconds. Press Ctrl-C to stop.",); 96 | 97 | let mut interval = tokio::time::interval(tokio::time::Duration::from_secs(5)); 98 | 99 | loop { 100 | interval.tick().await; 101 | 102 | let ping = Ping { 103 | source: public_key.clone(), 104 | }; 105 | 106 | info!("sending ping to {}", other_ping_pong.id()); 107 | let sent_instant = Instant::now(); 108 | match other_ping_pong.ask(ping).await { 109 | Err(e) => break error!("failed to send ping: {e}"), 110 | Ok(_pong) => { 111 | let elapsed = sent_instant.elapsed(); 112 | info!("received pong from {} in {elapsed:?}", other_ping_pong.id()); 113 | } 114 | } 115 | } 116 | 117 | Ok(()) 118 | } 119 | -------------------------------------------------------------------------------- /theta/src/persistence/mod.rs: -------------------------------------------------------------------------------- 1 | //! Actor persistence capabilities for state snapshots and recovery. 2 | //! 3 | //! This module enables actors to persist their state and recover from failures 4 | //! or system restarts. It provides a flexible architecture that supports 5 | //! multiple storage backends and automatic snapshot management. 6 | //! 7 | //! # Core Concepts 8 | //! 9 | //! ## Snapshots 10 | //! 11 | //! A **snapshot** is a serializable representation of an actor's state that can be 12 | //! captured from the Actor's current state and supports serde for restoration. 13 | //! Snapshots are one of the ActorArgs, which means they can be used to initialize 14 | //! actors during recovery. Snapshots must: 15 | //! - Capture all essential state needed to restore the actor 16 | //! - Be serializable (implement `Serialize` + `Deserialize`) 17 | //! - Be usable as actor initialization arguments (implement `ActorArgs`) 18 | //! - Convert from the actor's current state (implement `From<&Actor>`) 19 | //! 20 | //! ## Storage Backends 21 | //! 22 | //! Storage backends implement the `PersistentStorage` trait and handle: 23 | //! - Reading snapshots by actor ID 24 | //! - Writing snapshots atomically 25 | //! - Storage-specific optimizations (compression, indexing, etc.) 26 | //! 27 | //! Built-in storage backends: 28 | //! - **File system**: Local disk storage with configurable directories 29 | //! 30 | //! ## Persistence Lifecycle 31 | //! 32 | //! 1. **Snapshot Creation**: Actors create snapshots from their current state 33 | //! 2. **Storage**: Snapshots are written to the configured storage backend 34 | //! 3. **Recovery**: Actors are restored from snapshots during spawn operations 35 | //! 4. **Automatic Management**: Framework handles snapshot timing and cleanup 36 | //! 37 | //! # Usage Patterns 38 | //! 39 | //! ## Basic Persistence 40 | //! 41 | //! ```ignore 42 | //! use theta::prelude::*; 43 | //! use serde::{Serialize, Deserialize}; 44 | //! 45 | //! #[derive(Debug, Clone, Serialize, Deserialize, ActorArgs)] 46 | //! struct Counter { 47 | //! value: i64, 48 | //! name: String, 49 | //! } 50 | //! 51 | //! // The `snapshot` flag automatically implements PersistentActor 52 | //! // with Snapshot = Counter, RuntimeArgs = (), ActorArgs = Counter 53 | //! #[actor("12345678-1234-5678-9abc-123456789abc", snapshot)] 54 | //! impl Actor for Counter { 55 | //! const _: () = { 56 | //! async |Increment(amount): Increment| { 57 | //! self.value += amount; 58 | //! // Manual snapshot saving when needed 59 | //! let _ = ctx.save_snapshot(&storage, self).await; 60 | //! }; 61 | //! }; 62 | //! } 63 | //! ``` 64 | //! 65 | //! ## Custom Snapshot Types 66 | //! 67 | //! ```ignore 68 | //! #[derive(Debug, Clone, Serialize, Deserialize)] 69 | //! struct CounterSnapshot { 70 | //! value: i64, 71 | //! // Exclude non-essential state like caches 72 | //! } 73 | //! 74 | //! impl From<&Counter> for CounterSnapshot { 75 | //! fn from(counter: &Counter) -> Self { 76 | //! Self { value: counter.value } 77 | //! } 78 | //! } 79 | //! 80 | //! impl ActorArgs for CounterSnapshot { 81 | //! type Actor = Counter; 82 | //! 83 | //! async fn initialize(ctx: Context, args: &Self) -> Counter { 84 | //! Counter { 85 | //! value: args.value, 86 | //! cache: HashMap::new(), // Rebuilt on recovery 87 | //! } 88 | //! } 89 | //! } 90 | //! ``` 91 | //! 92 | //! ## Recovery-aware Spawning 93 | //! 94 | //! ```ignore 95 | //! // Try to restore from snapshot, fallback to new instance 96 | //! let counter = ctx.respawn_or( 97 | //! &storage, 98 | //! actor_id, 99 | //! || (), // runtime args 100 | //! || Counter { value: 0, name: "default".to_string() } 101 | //! ).await?; 102 | //! ``` 103 | 104 | pub mod persistent_actor; 105 | pub mod storages; 106 | 107 | // Re-exports 108 | pub use persistent_actor::{ 109 | PersistenceError, PersistentActor, PersistentContextExt, PersistentSpawnExt, PersistentStorage, 110 | }; 111 | -------------------------------------------------------------------------------- /theta/examples/monitor.rs: -------------------------------------------------------------------------------- 1 | use iroh::{Endpoint, PublicKey}; 2 | use serde::{Deserialize, Serialize}; 3 | use std::{str::FromStr, vec}; 4 | use theta::{monitor::monitor, prelude::*}; 5 | use theta_flume::unbounded_anonymous; 6 | use theta_macros::ActorArgs; 7 | use tracing::{error, info}; 8 | use tracing_subscriber::fmt::time::ChronoLocal; 9 | use url::Url; 10 | 11 | #[derive(Debug, Clone, Hash, Serialize, Deserialize, ActorArgs)] 12 | pub struct Counter { 13 | value: i64, 14 | } 15 | 16 | impl Counter { 17 | pub fn new() -> Self { 18 | Self { value: 0 } 19 | } 20 | } 21 | 22 | #[derive(Debug, Clone, Serialize, Deserialize)] 23 | pub struct Inc { 24 | pub amount: i64, 25 | } 26 | 27 | #[derive(Debug, Clone, Serialize, Deserialize)] 28 | pub struct Dec { 29 | pub amount: i64, 30 | } 31 | 32 | #[derive(Debug, Clone, Serialize, Deserialize)] 33 | pub struct CounterResponse { 34 | pub new_value: i64, 35 | } 36 | 37 | #[actor("a1b2c3d4-5e6f-7890-abcd-ef1234567890")] 38 | impl Actor for Counter { 39 | type View = Counter; 40 | 41 | const _: () = { 42 | async |msg: Inc| -> CounterResponse { 43 | let new_value = self.value + msg.amount; 44 | self.value = new_value; 45 | info!("counter incremented by {} to {}", msg.amount, new_value); 46 | CounterResponse { new_value } 47 | }; 48 | }; 49 | 50 | const _: () = { 51 | async |msg: Dec| -> CounterResponse { 52 | let new_value = self.value - msg.amount; 53 | self.value = new_value; 54 | info!("counter decremented by {} to {}", msg.amount, new_value); 55 | CounterResponse { new_value } 56 | }; 57 | }; 58 | } 59 | 60 | #[tokio::main] 61 | async fn main() -> anyhow::Result<()> { 62 | tracing_subscriber::fmt() 63 | .with_env_filter("info,theta=trace") 64 | .with_timer(ChronoLocal::new("%y%m%d %H:%M:%S%.3f %Z".into())) 65 | .init(); 66 | 67 | tracing_log::LogTracer::init().ok(); 68 | 69 | info!("initializing RootContext..."); 70 | let endpoint = Endpoint::builder() 71 | .alpns(vec![b"theta".to_vec()]) 72 | .discovery_n0() 73 | .bind() 74 | .await?; 75 | 76 | let ctx = RootContext::init(endpoint); 77 | let public_key = ctx.public_key(); 78 | info!("rootContext initialized with public key: {public_key}"); 79 | 80 | println!("Please enter the public key of the other peer:"); 81 | let mut input = String::new(); 82 | let other_public_key = loop { 83 | std::io::stdin().read_line(&mut input)?; 84 | let trimmed = input.trim(); 85 | if trimmed.is_empty() { 86 | eprintln!("Public key cannot be empty. Please try again."); 87 | input.clear(); 88 | continue; 89 | } 90 | match PublicKey::from_str(trimmed) { 91 | Err(e) => { 92 | eprintln!("Invalid public key format: {e}"); 93 | input.clear(); 94 | } 95 | Ok(public_key) => break public_key, 96 | }; 97 | }; 98 | 99 | let couter_url = Url::parse(&format!("iroh://counter@{other_public_key}"))?; 100 | let (tx, rx) = unbounded_anonymous(); 101 | 102 | if let Err(e) = monitor::(couter_url, tx).await { 103 | error!("failed to monitor Counter actor: {e}"); 104 | return Err(e.into()); 105 | } 106 | 107 | // Alternatively, if one has the ActorRef already (e.g., from a previous lookup), 108 | // let counter = 109 | // ActorRef::::lookup(&format!("iroh://counter@{other_public_key}")).await?; 110 | // let (tx, rx) = unbounded_anonymous(); 111 | // if let Err(e) = counter.monitor(tx).await { 112 | // return Err(e.into()); 113 | // } 114 | 115 | tokio::spawn(async move { 116 | while let Some(update) = rx.recv().await { 117 | info!("received state update: {update:#?}",); 118 | } 119 | }); 120 | 121 | // Keep the application running 122 | loop { 123 | tokio::time::sleep(tokio::time::Duration::from_secs(1)).await; 124 | } 125 | } 126 | -------------------------------------------------------------------------------- /theta/examples/forward_ping_pong.rs: -------------------------------------------------------------------------------- 1 | use std::str::FromStr; 2 | 3 | use iroh::{Endpoint, PublicKey, dns::DnsResolver}; 4 | use serde::{Deserialize, Serialize}; 5 | use theta::prelude::*; 6 | use theta_macros::ActorArgs; 7 | use tracing::{error, info}; 8 | use tracing_subscriber::fmt::time::ChronoLocal; 9 | use url::Url; 10 | 11 | #[derive(Debug, Clone, ActorArgs)] 12 | pub struct ForwardPingPong; 13 | 14 | #[derive(Debug, Clone, Serialize, Deserialize)] 15 | pub struct Ping { 16 | pub source: PublicKey, 17 | pub target: ActorRef, 18 | } 19 | 20 | #[derive(Debug, Clone, Serialize, Deserialize)] 21 | pub struct Pong {} 22 | 23 | #[actor("a1b2c3d4-5e6f-7890-1234-567890abcdef")] 24 | impl Actor for ForwardPingPong { 25 | const _: () = { 26 | async |msg: Ping| -> Pong { 27 | info!( 28 | "received forwarded ping from {}, waiting 0.75 seconds...", 29 | msg.source 30 | ); 31 | 32 | // Wait 0.75 seconds before responding with Pong 33 | tokio::time::sleep(tokio::time::Duration::from_millis(750)).await; 34 | 35 | info!("sending pong back to {}", msg.source); 36 | Pong {} 37 | }; 38 | 39 | async |Pong {}| { 40 | info!("received pong, nothing to do..."); 41 | } 42 | }; 43 | } 44 | 45 | #[tokio::main] 46 | async fn main() -> anyhow::Result<()> { 47 | // Initialize tracing subscriber 48 | tracing_subscriber::fmt() 49 | .with_env_filter("info,theta=trace") 50 | .with_timer(ChronoLocal::new("%Y-%m-%d %H:%M:%S%.3f %Z".into())) 51 | .init(); 52 | 53 | tracing_log::LogTracer::init().ok(); 54 | 55 | info!("initializing RootContext..."); 56 | 57 | let dns = DnsResolver::with_nameserver("8.8.8.8:53".parse().unwrap()); 58 | let endpoint = Endpoint::builder() 59 | .alpns(vec![b"theta".to_vec()]) 60 | .discovery_n0() 61 | .dns_resolver(dns) // Required for mobile hotspot support 62 | .bind() 63 | .await?; 64 | 65 | let ctx = RootContext::init(endpoint); 66 | let public_key = ctx.public_key(); 67 | 68 | info!("rootContext initialized with public key: {public_key}"); 69 | 70 | info!("spawning ForwardPingPong actor..."); 71 | let forward_ping_pong = ctx.spawn(ForwardPingPong); 72 | 73 | info!("binding ForwardPingPong actor to 'ping-pong' name..."); 74 | let _ = ctx.bind("ping-pong", forward_ping_pong.clone()); 75 | 76 | // Ask for user of other peer's public key 77 | info!("please enter the public key of the other peer:"); 78 | 79 | let mut input = String::new(); 80 | let other_public_key = loop { 81 | std::io::stdin().read_line(&mut input)?; 82 | let trimmed = input.trim(); 83 | if trimmed.is_empty() { 84 | error!("public key cannot be empty. Please try again."); 85 | input.clear(); 86 | continue; 87 | } 88 | 89 | match PublicKey::from_str(trimmed) { 90 | Err(e) => { 91 | error!("invalid public key format: {e}"); 92 | input.clear(); 93 | } 94 | Ok(public_key) => break public_key, 95 | }; 96 | }; 97 | 98 | let forward_ping_pong_url = Url::parse(&format!("iroh://ping-pong@{other_public_key}"))?; 99 | 100 | let other_forward_ping_pong = match ActorRef::::lookup(&forward_ping_pong_url) 101 | .await 102 | { 103 | Err(e) => { 104 | error!( 105 | "failed to find ForwardPingPong actor at URL: {forward_ping_pong_url}. Error: {e}" 106 | ); 107 | return Ok(()); 108 | } 109 | Ok(actor) => actor, 110 | }; 111 | 112 | info!("starting forward ping cycle every 2 seconds. Press Ctrl-C to stop."); 113 | 114 | let mut interval = tokio::time::interval(tokio::time::Duration::from_secs(2)); 115 | 116 | loop { 117 | interval.tick().await; 118 | 119 | // Create a ping with self as target to forward to the other peer 120 | let ping = Ping { 121 | source: public_key.clone(), 122 | target: forward_ping_pong.clone(), 123 | }; 124 | 125 | info!( 126 | "forwarding ping to {} with self as target", 127 | other_forward_ping_pong.id() 128 | ); 129 | 130 | if let Err(err) = other_forward_ping_pong.forward(ping, forward_ping_pong.clone()) { 131 | error!(msg = ?err.0, %err, "failed to forward ping"); 132 | } 133 | } 134 | } 135 | -------------------------------------------------------------------------------- /theta/src/lib.rs: -------------------------------------------------------------------------------- 1 | //! # Theta: An Async Actor Framework for Rust 2 | //! 3 | //! Theta is an **ergonomic** yet **minimal** and **performant** async actor framework for Rust. 4 | //! 5 | //! ## Key Features 6 | //! 7 | //! - **Async-first**: Built on top of `tokio`, actors are lightweight tasks with MPMC communication 8 | //! - **Built-in remote capabilities**: Distributed actor systems powered by P2P protocol ([iroh]) 9 | //! - **Built-in monitoring**: Monitor actor state changes and lifecycle events 10 | //! - **Built-in persistence**: Seamless actor snapshots and recovery 11 | //! - **Type-safe messaging**: Compile-time guarantees for message handling 12 | //! - **Ergonomic macros**: Simplified actor definition with the `#[actor]` attribute 13 | //! 14 | //! ## Quick Start 15 | //! 16 | //! #[cfg(feature = "full")] 17 | //! ```rust 18 | //! use serde::{Deserialize, Serialize}; 19 | //! use theta::prelude::*; 20 | //! 21 | //! // Define actor state 22 | //! #[derive(Debug, Clone, ActorArgs)] 23 | //! struct Counter { value: i64 } 24 | //! 25 | //! // Define messages 26 | //! #[derive(Debug, Clone, Serialize, Deserialize)] 27 | //! struct Inc(i64); 28 | //! 29 | //! #[derive(Debug, Clone, Serialize, Deserialize)] 30 | //! struct GetValue; 31 | //! 32 | //! // Implement actor behavior 33 | //! #[actor("96d9901f-24fc-4d82-8eb8-023153d41074")] 34 | //! impl Actor for Counter { 35 | //! const _: () = { 36 | //! async |Inc(amount): Inc| { 37 | //! self.value += amount; 38 | //! }; 39 | //! 40 | //! async |_: GetValue| -> i64 { 41 | //! self.value 42 | //! }; 43 | //! }; 44 | //! } 45 | //! 46 | //! #[tokio::main] 47 | //! async fn main() -> anyhow::Result<()> { 48 | //! let ctx = RootContext::init_local(); 49 | //! let counter = ctx.spawn(Counter { value: 0 }); 50 | //! 51 | //! counter.tell(Inc(5))?; // Fire-and-forget 52 | //! let current = counter.ask(GetValue).await?; // Request-response 53 | //! println!("Current value: {current}"); // Current value: 5 54 | //! 55 | //! Ok(()) 56 | //! } 57 | //! ``` 58 | //! 59 | //! ## Features 60 | //! 61 | //! - **`macros`** (default): Enables the `#[actor]` and `ActorArgs` derive macros 62 | //! - **`remote`**: Enables distributed actor systems via P2P networking 63 | //! - **`monitor`**: Enables actor state monitoring and observation 64 | //! - **`persistence`**: Enables actor state persistence and recovery 65 | //! 66 | //! [iroh]: https://iroh.computer/ 67 | 68 | extern crate self as theta; 69 | 70 | pub mod actor; 71 | pub mod actor_ref; 72 | #[macro_use] 73 | pub mod base; 74 | pub mod context; 75 | pub mod message; 76 | #[cfg(feature = "monitor")] 77 | pub mod monitor; 78 | 79 | #[cfg(feature = "remote")] 80 | pub mod remote; 81 | 82 | #[cfg(feature = "persistence")] 83 | pub mod persistence; 84 | 85 | pub(crate) mod actor_instance; 86 | #[cfg(test)] 87 | mod dev; 88 | 89 | /// The prelude module re-exports the most commonly used types and traits. 90 | /// 91 | /// This module contains all the essential types needed for basic actor usage. 92 | /// Most applications should use `use theta::prelude::*;` to import these items. 93 | pub mod prelude { 94 | // Core actor types 95 | pub use crate::{ 96 | actor::{Actor, ActorArgs, ActorId, ExitCode}, 97 | actor_ref::{ActorRef, WeakActorRef}, 98 | base::{Ident, Nil}, 99 | context::{Context, RootContext}, 100 | message::{Message, Signal}, 101 | }; 102 | 103 | // Monitoring types (when enabled) 104 | #[cfg(feature = "monitor")] 105 | pub use crate::monitor::{Status, Update, UpdateRx, UpdateTx, monitor_local, monitor_local_id}; 106 | 107 | // Persistence types (when enabled) 108 | #[cfg(feature = "persistence")] 109 | pub use crate::persistence::{ 110 | PersistentActor, PersistentContextExt, PersistentSpawnExt, PersistentStorage, 111 | }; 112 | 113 | // Remote types (when enabled) 114 | #[cfg(feature = "remote")] 115 | pub use crate::remote::base::{RemoteError, Tag}; 116 | 117 | #[cfg(all(feature = "remote", feature = "monitor"))] 118 | pub use crate::monitor::monitor; 119 | 120 | // Macros 121 | #[cfg(feature = "macros")] 122 | pub use theta_macros::{ActorArgs, actor}; 123 | } 124 | 125 | /// Private re-exports for macro use. Do not use directly. 126 | #[doc(hidden)] 127 | pub mod __private { 128 | pub use rustc_hash; 129 | pub use serde; 130 | pub use spez; 131 | pub use tracing; 132 | pub use uuid; 133 | 134 | #[cfg(feature = "remote")] 135 | pub use postcard; 136 | } 137 | -------------------------------------------------------------------------------- /theta/src/base.rs: -------------------------------------------------------------------------------- 1 | //! Base types and utilities used throughout the framework. 2 | 3 | use serde::{Deserialize, Serialize}; 4 | use std::{ 5 | any::Any, 6 | fmt::{Debug, Display}, 7 | }; 8 | use thiserror::Error; 9 | use uuid::Uuid; 10 | 11 | use crate::actor::Actor; 12 | 13 | /// Actor identifier type for named bindings and lookups. 14 | /// 15 | /// Intentionally restrictive design has two reasons: 16 | /// 1. To support allocation-restricted targets 17 | /// 2. To recommend restrained use of global binding and lookups as they undermine 'locality' and 'security' of actor system 18 | pub type Ident = [u8; 16]; 19 | 20 | /// A unit type representing "no value" or empty state. 21 | /// 22 | /// This is commonly used as the `View` type for actors that 23 | /// don't need to update state to monitors. 24 | /// 25 | /// # Example 26 | /// 27 | /// ``` 28 | /// use theta::prelude::*; 29 | /// 30 | /// #[derive(Debug, Clone, ActorArgs)] 31 | /// struct MyActor; 32 | /// 33 | /// #[actor("12345678-1234-5678-9abc-123456789abc")] 34 | /// impl Actor for MyActor {} 35 | /// ``` 36 | #[derive(Debug, Clone, Serialize, Deserialize)] 37 | pub struct Nil; 38 | 39 | /// Errors that can occur during operations over bindings. 40 | #[derive(Debug, Clone, Error, Serialize, Deserialize)] 41 | pub enum BindingError { 42 | #[error("invalid identifier")] 43 | InvalidIdent, 44 | #[error("actor not found")] 45 | NotFound, 46 | #[error("actor type id mismatch")] 47 | TypeMismatch, 48 | #[error("actor ref downcast failed")] 49 | DowncastError, 50 | #[cfg(feature = "remote")] 51 | #[error(transparent)] 52 | SerializeError(#[from] postcard::Error), 53 | } 54 | 55 | /// Errors that can occur when monitoring actor status. 56 | /// This is here in order to keep the inter-peer type does not depend on feature flags. 57 | #[derive(Debug, Clone, Error, Serialize, Deserialize)] 58 | pub enum MonitorError { 59 | #[error(transparent)] 60 | BindingError(#[from] BindingError), 61 | #[error("failed to send signal")] 62 | SigSendError, 63 | } 64 | 65 | pub(crate) struct Hex<'a, const N: usize>(pub(crate) &'a [u8; N]); 66 | 67 | /// Parse string identifier into 16-byte identifier. 68 | /// Since all ASCII &str shorter than 16 bytes is not a valid UUID as byte[8] should be 0x80–0xBF which is non ASCII, 69 | /// Further for unicode, unless one types in \u{00A0}, continuation on byte[8] it could not be a valid UUID either. 70 | /// Both canonical hyphenated UUID string and all 32 hex digits never fits in 16 bytes. 71 | /// Therefore, it does not accidentally interpret a human-readable name as UUID. 72 | pub(crate) fn parse_ident(s: &str) -> Result { 73 | match Uuid::try_parse(s) { 74 | Err(_) => { 75 | let bytes = s.as_bytes(); 76 | 77 | if bytes.len() > 16 { 78 | return Err(BindingError::InvalidIdent); 79 | } 80 | 81 | let mut ident = [0u8; 16]; 82 | ident[..bytes.len()].copy_from_slice(bytes); 83 | 84 | Ok(ident) 85 | } 86 | Ok(uuid) => Ok(*uuid.as_bytes()), 87 | } 88 | } 89 | 90 | /// Extract panic message from panic payload for error updateing. 91 | pub(crate) fn panic_msg(payload: Box) -> String { 92 | if let Some(s) = payload.downcast_ref::<&str>() { 93 | s.to_string() 94 | } else if let Ok(s) = payload.downcast::() { 95 | *s 96 | } else { 97 | unreachable!("payload should be a string or &str") 98 | } 99 | } 100 | 101 | // Implementations 102 | 103 | impl From<&T> for Nil { 104 | fn from(_: &T) -> Self { 105 | Nil 106 | } 107 | } 108 | 109 | const HEX: &[u8; 16] = b"0123456789abcdef"; 110 | 111 | impl Display for Hex<'_, 16> { 112 | fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { 113 | let mut out = [0u8; 32]; 114 | for (i, byte) in self.0.iter().enumerate() { 115 | out[i * 2] = HEX[(byte >> 4) as usize]; 116 | out[i * 2 + 1] = HEX[(byte & 0x0f) as usize]; 117 | } 118 | 119 | write!(f, "{}", unsafe { std::str::from_utf8_unchecked(&out) }) // Safety: HEX is all valid UTF-8 120 | } 121 | } 122 | 123 | impl Display for Hex<'_, 32> { 124 | fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { 125 | let mut out = [0u8; 64]; 126 | for (i, byte) in self.0.iter().enumerate() { 127 | out[i * 2] = HEX[(byte >> 4) as usize]; 128 | out[i * 2 + 1] = HEX[(byte & 0x0f) as usize]; 129 | } 130 | 131 | write!(f, "{}", unsafe { std::str::from_utf8_unchecked(&out) }) // Safety: HEX is all valid UTF-8 132 | } 133 | } 134 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | [![Crates.io](https://img.shields.io/crates/v/theta.svg)](https://crates.io/crates/theta) 2 | [![Documentation](https://docs.rs/theta/badge.svg)](https://docs.rs/theta) 3 | [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT) 4 | 5 | # Theta 6 | 7 | **An actor framework for Rust** 8 | 9 | 10 |
11 |

Any questions or idea?

12 | 13 | Join GitHub Discussions 16 | 17 |
18 | 19 | 20 | ## Overview 21 | Theta is an **ergonomic** yet **minimal** and **performant** application framework based on actor model. 22 | 23 | - **Async Actor** 24 | - An actor instance is a very thin wrapper around a `tokio::task` and two MPMC channels. 25 | - `ActorRef` is just a single pointer(MPMC sender). 26 | - **Built-in remote** 27 | - Distributed actor system powered by P2P network, `iroh`. 28 | - Even `ActorRef` could be passed across network boundary as regular data in message. 29 | - Available with feature `remote`. 30 | - **Built-in monitoring** 31 | - "Monitor" suggested by Carl Hewitt's Actor Model is implemented as (possibly remote) monitoring feature. 32 | - Available with feature `monitor`. 33 | - **Built-in persistence** 34 | - Seamless respawn of actor from snapshot on file system, AWS S3 etc. 35 | - Available with feature `persistence`. 36 | - **WASM support (WIP)** 37 | - Compile to WebAssembly for running in browser or other WASM environments. 38 | 39 | 41 | 42 | ## Example 43 | ```sh 44 | cargo add theta 45 | ``` 46 | 47 | ```rust 48 | use serde::{Deserialize, Serialize}; 49 | use theta::prelude::*; 50 | 51 | #[derive(Debug, Clone, ActorArgs)] 52 | struct Counter { 53 | value: i64, 54 | } 55 | 56 | #[derive(Debug, Clone, Serialize, Deserialize)] 57 | pub struct GetValue; 58 | 59 | #[actor("96d9901f-24fc-4d82-8eb8-023153d41074")] 60 | impl Actor for Counter { 61 | type View = Nil; 62 | 63 | // Behaviors will generate single enum Msg for the actor 64 | const _: () = { 65 | async |amount: i64| { // Behavior can access &mut self 66 | self.value += amount; 67 | }; 68 | 69 | async |GetValue| -> i64 { // Behavior may or may not have return 70 | self.value 71 | }; 72 | }; 73 | } 74 | 75 | #[tokio::main] 76 | async fn main() -> anyhow::Result<()> { 77 | let ctx = RootContext::init_local(); 78 | let counter = ctx.spawn(Counter { value: 0 }); 79 | 80 | let _ = counter.tell(5); // Fire-and-forget 81 | 82 | let current = counter.ask(GetValue).await?; // Wait for response 83 | println!("Current value: {current}"); // Current value: 5 84 | 85 | Ok(()) 86 | } 87 | ``` 88 | 89 | 90 | ## 🚧 WIP 91 | Theta is currently under active development and API is subject to change. Not yet recommended for any serious business. 92 | ### Todo 93 | - Core 94 | - [x] Make `Result::Err` implementing `std::fmt::Display` on `tell` to be logged as `tracing::error!` to prevent silent failure or code duplication 95 | - [x] Use concurrent hashmap 96 | - [ ] Fix duplicated removing disconnected peer message 97 | - Macros 98 | - [ ] Make `actor` macro to take identifier as `ActorId` 99 | - Supervision 100 | - [ ] Factor out supervision as a optional feature 101 | - Remote 102 | - [x] Define lifetime behavior of exported actors (Currently, exported actor will never get dropped) 103 | - [x] Deduplicate simultanious connection attempt to each other 104 | - [ ] Support full NodeAddr including Url format definition and sharing routing information between peers 105 | - Persistence 106 | - [x] Cover partial persistence case; some could be stored in storage, but some data should be passed in runtime 107 | - [x] Have respawn API to take closure, not value. 108 | - Actor pool 109 | - [ ] Actor pool (task stealing with anonymous dynamic actors and MPMC) 110 | 111 | ## License 112 | 113 | Licensed under the [MIT License](LICENSE). -------------------------------------------------------------------------------- /theta/src/remote/base.rs: -------------------------------------------------------------------------------- 1 | use futures::channel::oneshot::Canceled; 2 | use iroh::PublicKey; 3 | use thiserror::Error; 4 | use tokio::time::error::Elapsed; 5 | use uuid::Uuid; 6 | 7 | use crate::{ 8 | base::{BindingError, Ident, MonitorError, parse_ident}, 9 | remote::network::NetworkError, 10 | }; 11 | 12 | /// Unique identifier for actor implementation types in remote communication. 13 | pub type ActorTypeId = Uuid; 14 | 15 | /// Message type identifier for remote serialization. 16 | pub type Tag = u32; 17 | 18 | pub(crate) type Key = u32; 19 | 20 | /// Errors that can occur during remote actor operations. 21 | #[derive(Debug, Clone, Error)] 22 | pub enum RemoteError { 23 | #[error(transparent)] 24 | Canceled(#[from] Canceled), 25 | 26 | #[error("invalid address")] 27 | InvalidAddress, 28 | 29 | #[error(transparent)] 30 | NetworkError(#[from] NetworkError), 31 | 32 | #[error(transparent)] 33 | SerializeError(postcard::Error), 34 | #[error(transparent)] 35 | DeserializeError(postcard::Error), 36 | 37 | #[error(transparent)] 38 | BindingError(#[from] BindingError), 39 | #[error(transparent)] 40 | MonitorError(#[from] MonitorError), 41 | 42 | #[error("deadline has elapsed")] 43 | Timeout, 44 | } 45 | 46 | // #[derive(Debug, Clone)] 47 | // pub(crate) struct Cancel { 48 | // inner: Arc, 49 | // } 50 | 51 | // #[derive(Debug)] 52 | // struct Inner { 53 | // notify: Notify, 54 | // canceled: AtomicBool, 55 | // } 56 | 57 | /// Parse IROH URL into identifier and public key components. 58 | /// 59 | /// Supports both UUID and string identifiers in the format: 60 | /// `iroh://{ident}@{public_key}` 61 | pub(crate) fn parse_url(addr: &url::Url) -> Result<(Ident, PublicKey), RemoteError> { 62 | let ident = parse_ident(addr.username())?; 63 | 64 | let public_key = addr 65 | .host_str() 66 | .ok_or(RemoteError::InvalidAddress)? 67 | .parse::() 68 | .map_err(|_| RemoteError::InvalidAddress)?; 69 | 70 | debug_assert!(addr.path_segments().is_none()); 71 | 72 | Ok((ident, public_key)) 73 | } 74 | 75 | // Implementation 76 | 77 | // impl Cancel { 78 | // #[inline] 79 | // pub fn new() -> Self { 80 | // Self { 81 | // inner: Arc::new(Inner { 82 | // notify: Notify::new(), 83 | // canceled: AtomicBool::new(false), 84 | // }), 85 | // } 86 | // } 87 | 88 | // #[inline] 89 | // pub fn is_canceled(&self) -> bool { 90 | // self.inner.canceled.load(Ordering::Relaxed) 91 | // } 92 | 93 | // /// Cancel and return previous cancel state which should be false. 94 | // #[cold] 95 | // pub fn cancel(&self) -> bool { 96 | // match self.inner.canceled.swap(true, Ordering::Release) { 97 | // false => { 98 | // self.inner.notify.notify_waiters(); 99 | // false 100 | // } 101 | // true => true, 102 | // } 103 | // } 104 | 105 | // pub async fn canceled(&self) { 106 | // if self.inner.canceled.load(Ordering::Relaxed) { 107 | // return; 108 | // } 109 | // let notified = self.inner.notify.notified(); 110 | // if self.inner.canceled.load(Ordering::Acquire) { 111 | // return; 112 | // } 113 | // notified.await; 114 | // } 115 | // } 116 | 117 | // impl Default for Cancel { 118 | // fn default() -> Self { 119 | // Self::new() 120 | // } 121 | // } 122 | 123 | impl From for RemoteError { 124 | fn from(_: Elapsed) -> Self { 125 | RemoteError::Timeout 126 | } 127 | } 128 | 129 | #[cfg(test)] 130 | mod tests { 131 | use url::Url; 132 | use uuid::uuid; 133 | 134 | use super::*; 135 | 136 | #[test] 137 | fn test_split_url() { 138 | let url = Url::parse("iroh://824d7cba-1489-4537-b2c9-1a488a3f895a@a0f71647936e25b8403433b31deb3a374d175b282baf9803a7715b138f9e6f65").unwrap(); 139 | let (ident, public_key) = parse_url(&url).unwrap(); 140 | assert_eq!( 141 | public_key.to_string(), 142 | "a0f71647936e25b8403433b31deb3a374d175b282baf9803a7715b138f9e6f65" 143 | ); 144 | assert_eq!( 145 | &ident, 146 | uuid!("824d7cba-1489-4537-b2c9-1a488a3f895a").as_bytes() 147 | ); 148 | 149 | let url = Url::parse( 150 | "iroh://foo@a0f71647936e25b8403433b31deb3a374d175b282baf9803a7715b138f9e6f65", 151 | ) 152 | .unwrap(); 153 | let (ident, public_key) = parse_url(&url).unwrap(); 154 | assert_eq!( 155 | public_key.to_string(), 156 | "a0f71647936e25b8403433b31deb3a374d175b282baf9803a7715b138f9e6f65" 157 | ); 158 | let expected: [u8; 16] = { 159 | let mut bytes = [0u8; 16]; 160 | bytes[..3].copy_from_slice(b"foo"); 161 | bytes 162 | }; 163 | assert_eq!(&ident, &expected); 164 | } 165 | } 166 | -------------------------------------------------------------------------------- /workflow/test_features.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | """ 3 | Generate and test all possible feature combinations for the theta crate. 4 | """ 5 | 6 | import subprocess 7 | import sys 8 | from itertools import combinations 9 | 10 | # Define all available features 11 | FEATURES = ["macros", "monitor", "remote", "persistence", "project_dir"] 12 | 13 | 14 | def run_cargo_command(cmd, desc): 15 | """Run a cargo command and return success/failure.""" 16 | print(f"Testing: {desc}", end=" ... ", flush=True) 17 | try: 18 | subprocess.run(cmd, check=True, capture_output=False, text=True) 19 | print("✓") 20 | return True 21 | except subprocess.CalledProcessError: 22 | print("✗") 23 | return False 24 | 25 | 26 | def main(): 27 | """Main test runner.""" 28 | print("==> Testing all feature combinations for cargo check") 29 | 30 | failed_combinations = [] 31 | total_combinations = 0 32 | 33 | # Generate all possible combinations (including empty set) 34 | for r in range(len(FEATURES) + 1): 35 | for combo in combinations(FEATURES, r): 36 | total_combinations += 1 37 | cmd = ["cargo", "check", "--no-default-features"] 38 | 39 | if combo: 40 | cmd.extend(["--features", ",".join(combo)]) 41 | desc = ", ".join(combo) 42 | else: 43 | desc = "no features" 44 | 45 | if not run_cargo_command(cmd, desc): 46 | failed_combinations.append(combo) 47 | 48 | # Test default features 49 | total_combinations += 1 50 | if not run_cargo_command(["cargo", "check"], "default features"): 51 | failed_combinations.append("default") 52 | 53 | # Test all features 54 | total_combinations += 1 55 | if not run_cargo_command(["cargo", "check", "--all-features"], "all features"): 56 | failed_combinations.append("all-features") 57 | 58 | print( 59 | f"\n==> Check phase results: {total_combinations - len(failed_combinations)}/{total_combinations} passed" 60 | ) 61 | 62 | if failed_combinations: 63 | print("Failed combinations:") 64 | for combo in failed_combinations: 65 | print(f" - {combo}") 66 | return 1 67 | 68 | print("\n==> Testing key combinations for cargo test") 69 | 70 | # For tests, only run a subset to avoid excessive test time 71 | test_failed = [] 72 | test_total = 0 73 | 74 | test_combinations = [ 75 | ([], "no features"), 76 | (["macros"], "macros only"), 77 | (["macros", "monitor"], "macros + monitor"), 78 | (["macros", "remote"], "macros + remote"), 79 | ( 80 | ["macros", "persistence", "project_dir"], 81 | "macros + persistence + project_dir", 82 | ), 83 | ] 84 | 85 | for features, desc in test_combinations: 86 | test_total += 1 87 | cmd = [ 88 | "cargo", 89 | "test", 90 | "--lib", 91 | "--bins", 92 | "--tests", 93 | "--examples", 94 | "--no-default-features", 95 | ] 96 | if features: 97 | cmd.extend(["--features", ",".join(features)]) 98 | 99 | if not run_cargo_command(cmd, desc): 100 | test_failed.append(features) 101 | 102 | # Test default features 103 | test_total += 1 104 | if not run_cargo_command( 105 | ["cargo", "test", "--lib", "--bins", "--tests", "--examples"], 106 | "default features", 107 | ): 108 | test_failed.append("default") 109 | 110 | # Test all features 111 | test_total += 1 112 | if not run_cargo_command( 113 | ["cargo", "test", "--lib", "--bins", "--tests", "--examples", "--all-features"], 114 | "all features", 115 | ): 116 | test_failed.append("all-features") 117 | 118 | print( 119 | f"\n==> Test phase results: {test_total - len(test_failed)}/{test_total} passed" 120 | ) 121 | 122 | if test_failed: 123 | print("Failed test combinations:") 124 | for combo in test_failed: 125 | print(f" - {combo}") 126 | return 1 127 | 128 | print("\n==> Testing examples compilation") 129 | 130 | # Test that examples compile with their required features 131 | examples_failed = [] 132 | examples_total = 0 133 | 134 | # Test individual examples to make sure they work with their required features 135 | examples_total += 1 136 | if not run_cargo_command( 137 | ["cargo", "check", "--examples", "--all-features"], 138 | "all examples with all features", 139 | ): 140 | examples_failed.append("all examples") 141 | 142 | print( 143 | f"\n==> Examples phase results: {examples_total - len(examples_failed)}/{examples_total} passed" 144 | ) 145 | 146 | if examples_failed: 147 | print("Failed examples:") 148 | for combo in examples_failed: 149 | print(f" - {combo}") 150 | return 1 151 | 152 | print("\n==> All feature combination tests passed!") 153 | return 0 154 | 155 | 156 | if __name__ == "__main__": 157 | sys.exit(main()) 158 | -------------------------------------------------------------------------------- /theta/examples/client.rs: -------------------------------------------------------------------------------- 1 | use std::{ 2 | hash::{Hash, Hasher}, 3 | str::FromStr, 4 | }; 5 | 6 | use crossterm::{ 7 | event::{Event, KeyCode, KeyModifiers, read}, 8 | terminal, 9 | }; 10 | use iroh::PublicKey; 11 | use serde::{Deserialize, Serialize}; 12 | use theta::{monitor::Update, prelude::*}; 13 | use theta_flume::unbounded_anonymous; 14 | use theta_macros::ActorArgs; 15 | use tracing::{error, info}; 16 | use tracing_subscriber::fmt::time::ChronoLocal; 17 | use url::Url; 18 | 19 | #[derive(Debug, Clone, Serialize, Deserialize)] 20 | pub struct Inc; 21 | #[derive(Debug, Clone, Serialize, Deserialize)] 22 | pub struct Dec; 23 | 24 | #[derive(Debug, Clone, ActorArgs)] 25 | pub struct Counter { 26 | pub value: i64, 27 | } 28 | 29 | #[actor("96d9901f-24fc-4d82-8eb8-023153d41074")] 30 | impl Actor for Counter { 31 | type View = i64; 32 | 33 | const _: () = { 34 | async |_: Inc| -> i64 { 35 | self.value += 1; 36 | self.value 37 | }; 38 | 39 | async |_: Dec| -> i64 { 40 | self.value -= 1; 41 | self.value 42 | }; 43 | }; 44 | 45 | fn hash_code(&self) -> u64 { 46 | let mut hasher = rustc_hash::FxHasher::default(); 47 | self.value.hash(&mut hasher); 48 | hasher.finish() 49 | } 50 | } 51 | 52 | impl From<&Counter> for i64 { 53 | fn from(counter: &Counter) -> Self { 54 | counter.value 55 | } 56 | } 57 | 58 | #[derive(Debug, Clone, Serialize, Deserialize)] 59 | pub struct GetWorker; 60 | 61 | #[derive(Debug, Clone, Hash, ActorArgs, Serialize, Deserialize)] 62 | pub struct Manager { 63 | pub worker: ActorRef, 64 | } 65 | 66 | #[actor("f65b84e6-adfe-4d3a-8140-ee55de512070")] 67 | impl Actor for Manager { 68 | type View = Self; 69 | 70 | const _: () = { 71 | // expose the worker via ask 72 | async |_: GetWorker| -> ActorRef { self.worker.clone() }; 73 | }; 74 | } 75 | 76 | #[tokio::main] 77 | async fn main() -> anyhow::Result<()> { 78 | tracing_subscriber::fmt() 79 | .with_env_filter("info,theta=trace") 80 | .with_timer(ChronoLocal::new("%H:%M:%S".into())) 81 | .init(); 82 | 83 | tracing_log::LogTracer::init().ok(); 84 | 85 | let endpoint = iroh::Endpoint::builder() 86 | .alpns(vec![b"theta".to_vec()]) 87 | .discovery_n0() 88 | .bind() 89 | .await?; 90 | 91 | let _ctx = RootContext::init(endpoint); 92 | 93 | // 1) get host pubkey 94 | info!("please enter the public key of the other peer:"); 95 | let mut input = String::new(); 96 | let host_pk = loop { 97 | std::io::stdin().read_line(&mut input)?; 98 | let trimmed = input.trim(); 99 | if trimmed.is_empty() { 100 | error!("public key cannot be empty. Please try again."); 101 | input.clear(); 102 | continue; 103 | } 104 | match PublicKey::from_str(trimmed) { 105 | Err(e) => { 106 | error!("invalid public key format: {e}"); 107 | input.clear(); 108 | } 109 | Ok(public_key) => break public_key, 110 | }; 111 | }; 112 | 113 | // 2) lookup manager by name@host 114 | let url = Url::parse(&format!("iroh://manager@{host_pk}"))?; 115 | info!("looking up Manager actor {url}"); 116 | let manager = ActorRef::::lookup(&url).await?; 117 | 118 | // --- A) via ask --- 119 | let worker_via_ask: ActorRef = manager.ask(GetWorker).await?; 120 | 121 | // --- B) via monitor (state update) --- 122 | info!("monitoring manager actor {url}"); 123 | let (tx, rx) = unbounded_anonymous(); 124 | if let Err(e) = monitor::(url, tx).await { 125 | error!("failed to monitor worker: {e}"); 126 | } 127 | 128 | let Some(init_update) = rx.recv().await else { 129 | panic!("failed to receive initial update"); 130 | }; 131 | 132 | let Update::State(Manager { 133 | worker: worker_via_update, 134 | }) = init_update 135 | else { 136 | panic!("unexpected update type: {init_update:?}"); 137 | }; 138 | 139 | // verify same actor 140 | if worker_via_ask.id() == worker_via_update.id() { 141 | info!("worker actor IDs match: {}", worker_via_ask.id()); 142 | } else { 143 | error!( 144 | "worker actor IDs do not match: ask: {} != update: {}", 145 | worker_via_ask.id(), 146 | worker_via_update.id() 147 | ); 148 | } 149 | 150 | // subscribe to worker state updates 151 | let counter_url = Url::parse(&format!("iroh://{}@{host_pk}", worker_via_ask.id()))?; 152 | let (counter_tx, counter_obs) = unbounded_anonymous(); 153 | 154 | info!("monitoring counter actor {counter_url}"); 155 | if let Err(e) = monitor::(counter_url, counter_tx).await { 156 | error!("failed to monitor Counter actor: {e}"); 157 | return Err(e.into()); 158 | } 159 | 160 | tokio::spawn(async move { 161 | while let Some(value) = counter_obs.recv().await { 162 | eprintln!("\rcounter = {value:?}"); 163 | } 164 | }); 165 | 166 | info!("press ↑ / ↓ to Inc/Dec; Ctrl-C to quit."); 167 | terminal::enable_raw_mode()?; 168 | 169 | let result = loop { 170 | match read()? { 171 | Event::Key(k) if k.code == KeyCode::Up => { 172 | let _ = worker_via_ask.ask(Inc).await; 173 | } 174 | Event::Key(k) if k.code == KeyCode::Down => { 175 | let _ = worker_via_ask.ask(Dec).await; 176 | } 177 | Event::Key(k) 178 | if k.code == KeyCode::Char('c') && k.modifiers.contains(KeyModifiers::CONTROL) => 179 | { 180 | info!("Ctrl+C pressed, exiting..."); 181 | break Ok(()); 182 | } 183 | Event::Key(k) if k.code == KeyCode::Esc => { 184 | info!("Escape pressed, exiting..."); 185 | break Ok(()); 186 | } 187 | _ => {} 188 | } 189 | }; 190 | 191 | terminal::disable_raw_mode()?; 192 | result 193 | } 194 | -------------------------------------------------------------------------------- /theta-macros/src/lib.rs: -------------------------------------------------------------------------------- 1 | //! Procedural macros for the Theta actor framework. 2 | //! 3 | //! This crate provides the `#[actor]` attribute macro and `ActorArgs` derive macro 4 | //! that simplify actor implementation by generating the necessary boilerplate code. 5 | 6 | // lib.rs - Root of macro crate 7 | use proc_macro::TokenStream; 8 | 9 | mod actor; 10 | 11 | /// Attribute macro for implementing actors with automatic message handling. 12 | /// 13 | /// This macro generates the necessary boilerplate for actor implementation, 14 | /// including message enum generation, message processing, and remote communication 15 | /// support when enabled. 16 | /// 17 | /// # Default Implementations 18 | /// 19 | /// The macro automatically provides default implementations for: 20 | /// - `type View = Nil;` - No state updateing by default 21 | /// - `const _: () = {};` - Empty message handler block 22 | /// 23 | /// You only need to specify these if you want custom behavior. 24 | /// 25 | /// # Usage 26 | /// 27 | /// ## Basic actor (no custom View or message handlers) 28 | /// ```ignore 29 | /// use theta::prelude::*; 30 | /// 31 | /// #[derive(Debug, Clone, ActorArgs)] 32 | /// struct MyActor { value: i32 } 33 | /// 34 | /// #[actor("12345678-1234-5678-9abc-123456789abc")] 35 | /// impl Actor for MyActor {} 36 | /// ``` 37 | /// 38 | /// ## Actor with message handlers 39 | /// ```ignore 40 | /// use theta::prelude::*; 41 | /// use serde::{Serialize, Deserialize}; 42 | /// 43 | /// #[derive(Debug, Clone, ActorArgs)] 44 | /// struct MyActor { value: i32 } 45 | /// 46 | /// #[derive(Debug, Clone, Serialize, Deserialize)] 47 | /// struct Increment(i32); 48 | /// 49 | /// #[actor("12345678-1234-5678-9abc-123456789abc")] 50 | /// impl Actor for MyActor { 51 | /// const _: () = { 52 | /// async |Increment(amount): Increment| { 53 | /// self.value += amount; 54 | /// }; 55 | /// }; 56 | /// } 57 | /// ``` 58 | /// 59 | /// # Arguments 60 | /// 61 | /// * `uuid` - A UUID string literal that uniquely identifies this actor type for remote 62 | /// communication. Must be a valid UUID format (e.g., "12345678-1234-5678-9abc-123456789abc"). 63 | /// This UUID should be generated once and remain constant for the lifetime of the actor type. 64 | /// 65 | /// ## Optional Parameters 66 | /// * `snapshot` - Enables persistence support. When specified, automatically implements 67 | /// `PersistentActor` trait for the actor type with default configuration: 68 | /// - `Snapshot = T` (the actor type itself) 69 | /// - `RuntimeArgs = ()` (no runtime arguments) 70 | /// - `ActorArgs = T` (same as snapshot type) 71 | /// Can be used as `snapshot` (defaults to `Self`) or `snapshot = CustomType` for custom snapshot types. 72 | /// 73 | /// # Return 74 | /// 75 | /// Generates a complete actor implementation including: 76 | /// * `Actor` trait implementation with message processing 77 | /// * Message enum type containing all handled message variants 78 | /// * `ProcessMessage` trait implementation for message routing 79 | /// * Remote communication support with serialization/deserialization 80 | /// * Optional `PersistentActor` implementation when `snapshot` parameter is used 81 | /// 82 | /// # Errors 83 | /// 84 | /// Compilation errors may occur if: 85 | /// * UUID string is not a valid UUID format 86 | /// * Actor implementation is malformed or missing required elements 87 | /// * Message handler closures have invalid signatures 88 | /// * Snapshot type (when specified) does not implement required traits 89 | /// 90 | /// # Notes 91 | /// 92 | /// * All message types used in handlers must implement `Serialize + Deserialize` 93 | /// * Actor state type must implement `Clone + Debug` 94 | /// * Message handlers are defined as async closures within `const _: () = {};` blocks 95 | // todo Make Uuid optional for non-remote 96 | #[proc_macro_attribute] 97 | pub fn actor(args: TokenStream, input: TokenStream) -> TokenStream { 98 | actor::actor_impl(args, input) 99 | } 100 | 101 | /// Derive macro for actor argument types with automatic trait implementations. 102 | /// 103 | /// This macro implements necessary traits for actor initialization arguments, 104 | /// including `Clone` and automatic `From<&Self>` conversion for convenient 105 | /// actor spawning patterns. 106 | /// 107 | /// # Usage 108 | /// 109 | /// ## Auto-args pattern (recommended) 110 | /// When using the auto-args pattern with `ctx.spawn_auto`, the macro enables 111 | /// convenient spawning without explicit conversion: 112 | /// ``` ignore 113 | /// use theta::prelude::*; 114 | /// 115 | /// #[derive(Debug, Clone, ActorArgs)] 116 | /// struct MyActor { value: i32 } 117 | /// 118 | /// #[actor("12345678-1234-5678-9abc-123456789abc")] 119 | /// impl Actor for MyActor {} 120 | /// 121 | /// // Usage: auto-args pattern 122 | /// let actor = ctx.spawn_auto(&MyActor { value: 42 }).await?; 123 | /// ``` 124 | /// 125 | /// ## Custom implementation pattern 126 | /// For actors with custom initialization logic: 127 | /// ``` ignore 128 | /// use theta::prelude::*; 129 | /// 130 | /// #[derive(Debug, Clone, ActorArgs)] 131 | /// struct DatabaseActor { 132 | /// connection_string: String, 133 | /// pool_size: usize, 134 | /// } 135 | /// 136 | /// impl DatabaseActor { 137 | /// pub fn new(connection_string: String) -> Self { 138 | /// Self { 139 | /// connection_string, 140 | /// pool_size: 10, // default pool size 141 | /// } 142 | /// } 143 | /// } 144 | /// ``` 145 | /// 146 | /// # Arguments 147 | /// 148 | /// The derive macro is applied to struct types that serve as actor initialization arguments. 149 | /// The struct must contain all fields necessary for actor initialization. 150 | /// 151 | /// # Return 152 | /// 153 | /// Automatically generates implementations for: 154 | /// * `Clone` trait - Required for all actor argument types to enable multiple spawning 155 | /// * `From<&Self> for Self` trait - Enables convenient reference-to-owned conversion 156 | /// for the auto-args spawning pattern used with `ctx.spawn_auto()` 157 | /// 158 | /// # Errors 159 | /// 160 | /// Compilation errors may occur if: 161 | /// * Applied to non-struct types (enums, unions not supported) 162 | /// * Struct contains fields that do not implement `Clone` 163 | /// * Struct has generic parameters that don't meet trait bounds 164 | /// 165 | /// # Notes 166 | /// 167 | /// * This derive macro is specifically designed for actor argument structs 168 | /// * Works seamlessly with the `#[actor]` attribute macro 169 | /// * Enables both direct instantiation and auto-args spawning patterns 170 | /// * All fields in the struct must be cloneable for the generated `Clone` implementation 171 | /// 172 | /// # Generated Implementations 173 | /// 174 | /// The macro automatically generates: 175 | /// - `Clone` trait (required for all actor args) 176 | /// - `From<&Self> for Self` - Enables reference-to-owned conversion for spawning 177 | #[proc_macro_derive(ActorArgs)] 178 | pub fn derive_actor_args(input: TokenStream) -> TokenStream { 179 | actor::derive_actor_args_impl(input) 180 | } 181 | -------------------------------------------------------------------------------- /theta/examples/ping_pong_bench.rs: -------------------------------------------------------------------------------- 1 | use iroh::{Endpoint, PublicKey, dns::DnsResolver}; 2 | use serde::{Deserialize, Serialize}; 3 | use std::{str::FromStr, time::Instant, vec}; 4 | use theta::prelude::*; 5 | use theta_macros::ActorArgs; 6 | // use tracing_subscriber::fmt::time::ChronoLocal; 7 | use url::Url; 8 | 9 | #[derive(Debug, Clone, ActorArgs)] 10 | pub struct PingPong; 11 | 12 | #[derive(Debug, Clone, Serialize, Deserialize)] 13 | pub struct Ping { 14 | pub source: PublicKey, 15 | } 16 | 17 | #[derive(Debug, Clone, Serialize, Deserialize)] 18 | pub struct Pong {} 19 | 20 | #[actor("f68fe56f-8aa9-4f90-8af8-591a06e2818a")] 21 | impl Actor for PingPong { 22 | const _: () = { 23 | async |_msg: Ping| -> Pong { 24 | // No logging in benchmark mode 25 | Pong {} 26 | }; 27 | }; 28 | } 29 | 30 | const WARMUP_ITERATIONS: usize = 100_000; 31 | const BENCHMARK_ITERATIONS: usize = 100_000; 32 | 33 | #[tokio::main] 34 | async fn main() -> anyhow::Result<()> { 35 | // Initialize tracing subscriber first, then LogTracer 36 | // tracing_subscriber::fmt() 37 | // .with_env_filter("error,theta=trace") 38 | // .with_timer(ChronoLocal::new("%Y-%m-%d %H:%M:%S%.3f %Z".into())) 39 | // .init(); 40 | 41 | // tracing_log::LogTracer::init().ok(); 42 | 43 | println!("Initializing RootContext..."); 44 | 45 | let dns = DnsResolver::with_nameserver("8.8.8.8:53".parse().unwrap()); 46 | let endpoint = Endpoint::builder() 47 | .alpns(vec![b"theta".to_vec()]) 48 | .discovery_n0() 49 | .dns_resolver(dns) // Required for mobile hotspot support 50 | .bind() 51 | .await?; 52 | 53 | let ctx = RootContext::init(endpoint); 54 | let public_key = ctx.public_key(); 55 | println!("RootContext initialized with public key: {public_key}"); 56 | 57 | println!("Spawning PingPong actor..."); 58 | let ping_pong = ctx.spawn(PingPong); 59 | 60 | println!("Binding PingPong actor to 'ping_pong' name..."); 61 | let _ = ctx.bind("ping_pong", ping_pong); 62 | 63 | // Get other peer's public key 64 | println!("Please enter the public key of the other peer:"); 65 | let mut input = String::new(); 66 | let other_public_key = loop { 67 | std::io::stdin().read_line(&mut input)?; 68 | let trimmed = input.trim(); 69 | if trimmed.is_empty() { 70 | eprintln!("Public key cannot be empty. Please try again."); 71 | input.clear(); 72 | continue; 73 | } 74 | match PublicKey::from_str(trimmed) { 75 | Err(e) => { 76 | eprintln!("Invalid public key format: {e}"); 77 | input.clear(); 78 | } 79 | Ok(public_key) => break public_key, 80 | }; 81 | }; 82 | 83 | let ping_pong_url = Url::parse(&format!("iroh://ping_pong@{other_public_key}"))?; 84 | let other_ping_pong = match ActorRef::::lookup(&ping_pong_url).await { 85 | Err(e) => { 86 | eprintln!("Failed to find PingPong actor at URL: {ping_pong_url}. Error: {e}"); 87 | return Ok(()); 88 | } 89 | Ok(actor) => actor, 90 | }; 91 | 92 | println!("Starting 100k ping-pong benchmark..."); 93 | println!( 94 | "Pre-allocating storage for {} measurements...", 95 | BENCHMARK_ITERATIONS 96 | ); 97 | 98 | // Pre-allocate storage for timing measurements 99 | let mut latencies = Vec::with_capacity(BENCHMARK_ITERATIONS); 100 | 101 | // Pre-create the ping message to avoid allocation overhead 102 | let ping = Ping { 103 | source: public_key.clone(), 104 | }; 105 | 106 | // Warm-up phase 107 | println!("Warming up with {WARMUP_ITERATIONS} requests..."); 108 | for _ in 0..WARMUP_ITERATIONS { 109 | let _ = other_ping_pong.ask(ping.clone()).await; 110 | } 111 | 112 | println!("Starting benchmark with {BENCHMARK_ITERATIONS} requests..."); 113 | let benchmark_start = Instant::now(); 114 | 115 | // Benchmark loop 116 | for i in 0..BENCHMARK_ITERATIONS { 117 | let start = Instant::now(); 118 | 119 | match other_ping_pong.ask(ping.clone()).await { 120 | Ok(_pong) => { 121 | let latency = start.elapsed(); 122 | latencies.push(latency.as_nanos() as u64); 123 | } 124 | Err(e) => { 125 | eprintln!("Failed to send ping at iteration {}: {e}", i + 1); 126 | break; 127 | } 128 | } 129 | } 130 | 131 | let total_duration = benchmark_start.elapsed(); 132 | let completed_requests = latencies.len(); 133 | 134 | println!("\n=== BENCHMARK RESULTS ==="); 135 | println!("Total requests completed: {}", completed_requests); 136 | println!("Total time: {:.2?}", total_duration); 137 | println!( 138 | "Requests per second: {:.2}", 139 | completed_requests as f64 / total_duration.as_secs_f64() 140 | ); 141 | 142 | if !latencies.is_empty() { 143 | // Calculate statistics 144 | latencies.sort_unstable(); 145 | 146 | let min_ns = latencies[0]; 147 | let max_ns = latencies[latencies.len() - 1]; 148 | let mean_ns = latencies.iter().map(|&x| x as u64).sum::() / latencies.len() as u64; 149 | 150 | // Percentiles 151 | let p50_ns = latencies[latencies.len() * 50 / 100]; 152 | let p95_ns = latencies[latencies.len() * 95 / 100]; 153 | let p99_ns = latencies[latencies.len() * 99 / 100]; 154 | let p999_ns = latencies[latencies.len() * 999 / 1000]; 155 | 156 | println!("\n=== LATENCY STATISTICS (microseconds) ==="); 157 | println!("Min: {:.2} us", min_ns as f64 / 1000.0); 158 | println!("Mean: {:.2} us", mean_ns as f64 / 1000.0); 159 | println!("Max: {:.2} us", max_ns as f64 / 1000.0); 160 | println!("P50: {:.2} us", p50_ns as f64 / 1000.0); 161 | println!("P95: {:.2} us", p95_ns as f64 / 1000.0); 162 | println!("P99: {:.2} us", p99_ns as f64 / 1000.0); 163 | println!("P99.9: {:.2} us", p999_ns as f64 / 1000.0); 164 | 165 | // Calculate standard deviation 166 | let variance = latencies 167 | .iter() 168 | .map(|&x| { 169 | let diff = x as f64 - mean_ns as f64; 170 | diff * diff 171 | }) 172 | .sum::() 173 | / latencies.len() as f64; 174 | let std_dev_ns = variance.sqrt(); 175 | 176 | println!("StdDev: {:.2}", std_dev_ns / 1000.0); 177 | 178 | // Throughput analysis 179 | println!("\n=== THROUGHPUT ANALYSIS ==="); 180 | let avg_throughput = completed_requests as f64 / total_duration.as_secs_f64(); 181 | println!("Average throughput: {:.2} req/sec", avg_throughput); 182 | println!("Average latency: {:.2} μs", mean_ns as f64 / 1000.0); 183 | println!( 184 | "Theoretical max throughput: {:.2} req/sec", 185 | 1_000_000.0 / (mean_ns as f64 / 1000.0) 186 | ); 187 | } 188 | 189 | Ok(()) 190 | } 191 | -------------------------------------------------------------------------------- /theta/src/message.rs: -------------------------------------------------------------------------------- 1 | //! Message types and traits for actor communication. 2 | //! 3 | //! This module defines the core message system used by actors to communicate. 4 | //! The main trait is [`Message`] which defines how messages are processed. 5 | 6 | use std::{any::Any, fmt::Debug, future::Future, sync::Arc}; 7 | 8 | #[cfg(not(feature = "remote"))] 9 | use std::panic::UnwindSafe; 10 | 11 | use futures::channel::oneshot; 12 | use theta_flume::{Receiver, Sender, WeakSender}; 13 | use tokio::sync::Notify; 14 | 15 | #[cfg(feature = "remote")] 16 | use crate::remote::base::Key; 17 | use crate::{actor::Actor, actor_ref::ActorHdl, context::Context}; 18 | 19 | #[cfg(feature = "monitor")] 20 | use crate::monitor::AnyUpdateTx; 21 | 22 | #[cfg(feature = "remote")] 23 | use { 24 | crate::remote::{ 25 | base::Tag, 26 | peer::{PEER, Peer}, 27 | }, 28 | serde::{Deserialize, Serialize}, 29 | }; 30 | 31 | /// A message pack containing a message and its continuation. 32 | pub type MsgPack = (::Msg, Continuation); 33 | 34 | /// One-shot sender for type-erased values in actor responses. 35 | pub type OneShotAny = oneshot::Sender>; 36 | 37 | /// One-shot sender for serialized bytes in remote responses. 38 | pub type OneShotBytes = oneshot::Sender>; 39 | 40 | /// Channel for sending messages to actors. 41 | pub type MsgTx = Sender>; 42 | 43 | /// Weak reference to message sender that won't prevent actor shutdown. 44 | pub type WeakMsgTx = WeakSender>; 45 | 46 | /// Channel for receiving messages in actor mailboxes. 47 | pub type MsgRx = Receiver>; 48 | 49 | /// Channel for sending shutdown signals to actors. 50 | pub type SigTx = Sender; 51 | 52 | /// Weak reference to signal sender that won't prevent actor shutdown. 53 | pub type WeakSigTx = WeakSender; 54 | 55 | /// Channel for receiving shutdown signals in actors. 56 | pub type SigRx = Receiver; 57 | 58 | /// Trait for messages that can be sent to actors. 59 | /// 60 | /// This trait is typically implemented automatically by the `#[actor]` macro 61 | /// for each message type defined in the actor's behavior block. 62 | /// 63 | /// # Type Parameters 64 | /// 65 | /// * `A` - The actor type that can handle this message 66 | pub trait Message: Debug + Send + Into + 'static { 67 | /// The return type when processing this message. 68 | /// 69 | /// For local-only actors, this can be any `Send + UnwindSafe` type. 70 | /// For remote-capable actors, it must also be serializable. 71 | #[cfg(not(feature = "remote"))] 72 | type Return: Send + UnwindSafe + 'static; 73 | 74 | #[cfg(feature = "remote")] 75 | type Return: Debug + Send + Sync + Serialize + for<'de> Deserialize<'de> + 'static; 76 | 77 | /// Tag used for identifying this message type in remote communication. 78 | #[cfg(feature = "remote")] 79 | const TAG: Tag; 80 | 81 | /// Process this message with the given actor state. 82 | /// 83 | /// This method is called when the message is delivered to an actor. 84 | /// It should not be called directly by user code. 85 | fn process( 86 | state: &mut A, 87 | ctx: Context, 88 | msg: Self, 89 | ) -> impl Future + Send; 90 | 91 | fn process_to_any( 92 | state: &mut A, 93 | ctx: Context, 94 | msg: Self, 95 | ) -> impl Future> + Send { 96 | async move { Box::new(Self::process(state, ctx, msg).await) as Box } 97 | } 98 | 99 | /// Returned buffer will be reused for next serialization 100 | #[cfg(feature = "remote")] 101 | fn process_to_bytes( 102 | state: &mut A, 103 | ctx: Context, 104 | peer: Peer, 105 | msg: Self, 106 | ) -> impl Future, postcard::Error>> + Send { 107 | async move { 108 | let ret = Self::process(state, ctx, msg).await; 109 | 110 | PEER.sync_scope(peer, || postcard::to_stdvec(&ret)) 111 | } 112 | } 113 | } 114 | 115 | /// A continuation is another actor, which is regular actor or reply channel. 116 | /// Per specification, address does not need to tell the identity of the actor, 117 | /// Which means this is kind of ad-hoc address of continuation actor. 118 | #[derive(Debug)] 119 | pub enum Continuation { 120 | Nil, 121 | 122 | Reply(OneShotAny), // type erased return 123 | Forward(OneShotAny), // type erased return 124 | 125 | // ? Will it not cause any problem between Thetas with different features? 126 | #[cfg(feature = "remote")] 127 | BinReply { 128 | peer: Peer, 129 | key: Key, 130 | }, 131 | #[cfg(feature = "remote")] 132 | LocalBinForward { 133 | peer: Peer, 134 | tx: OneShotBytes, // Relatively rare, take channel cost instead of larger continuation size 135 | }, 136 | #[cfg(feature = "remote")] 137 | RemoteBinForward { 138 | peer: Peer, 139 | tx: OneShotBytes, // Relatively rare, take channel cost instead of larger continuation size 140 | }, 141 | } 142 | 143 | /// Actor lifecycle control signals. 144 | #[derive(Debug, Clone, Copy)] 145 | pub enum Signal { 146 | /// Restart the actor terminating all the descendants 147 | Restart, 148 | /// Terminate the actor with all the descendants 149 | Terminate, 150 | } 151 | 152 | /// Error information for actor supervision and error handling. 153 | #[derive(Debug, Clone)] 154 | #[cfg_attr(feature = "remote", derive(Serialize, Deserialize))] 155 | pub enum Escalation { 156 | Initialize(String), 157 | ProcessMsg(String), 158 | Supervise(String), 159 | } 160 | 161 | /// Internal supervision and control signals for actor management. 162 | #[derive(Debug)] 163 | pub enum RawSignal { 164 | #[cfg(feature = "monitor")] 165 | Monitor(AnyUpdateTx), 166 | 167 | Escalation(ActorHdl, Escalation), 168 | ChildDropped, 169 | 170 | Pause(Option>), 171 | Resume(Option>), 172 | Restart(Option>), 173 | Terminate(Option>), 174 | } 175 | 176 | /// Internal signal variants without notification channels. 177 | #[derive(Debug, Clone, Copy)] 178 | pub(crate) enum InternalSignal { 179 | Pause, 180 | Resume, 181 | Restart, 182 | Terminate, 183 | } 184 | 185 | // Implementations 186 | 187 | impl Continuation { 188 | /// Create a reply continuation with the given response channel. 189 | /// 190 | /// # Arguments 191 | /// 192 | /// * `tx` - Channel for sending the reply 193 | /// 194 | /// # Return 195 | /// 196 | /// `Continuation::Reply` variant for direct responses. 197 | pub fn reply(tx: OneShotAny) -> Self { 198 | Continuation::Reply(tx) 199 | } 200 | 201 | /// Create a forward continuation with the given response channel. 202 | /// 203 | /// # Arguments 204 | /// 205 | /// * `tx` - Channel for forwarding the response 206 | /// 207 | /// # Return 208 | /// 209 | /// `Continuation::Forward` variant for message forwarding. 210 | pub fn forward(tx: OneShotAny) -> Self { 211 | Continuation::Forward(tx) 212 | } 213 | 214 | /// Check if this continuation is nil (no response expected). 215 | /// 216 | /// # Return 217 | /// 218 | /// `true` if this is a nil continuation, `false` otherwise. 219 | pub fn is_nil(&self) -> bool { 220 | matches!(self, Continuation::Nil) 221 | } 222 | } 223 | 224 | impl From for InternalSignal { 225 | fn from(signal: Signal) -> Self { 226 | match signal { 227 | Signal::Restart => InternalSignal::Restart, 228 | Signal::Terminate => InternalSignal::Terminate, 229 | } 230 | } 231 | } 232 | -------------------------------------------------------------------------------- /theta/src/remote/network.rs: -------------------------------------------------------------------------------- 1 | use std::sync::Arc; 2 | 3 | use futures::{ 4 | FutureExt, 5 | future::{BoxFuture, Shared}, 6 | lock::Mutex, 7 | }; 8 | use iroh::{ 9 | Endpoint, NodeAddr, PublicKey, 10 | endpoint::{Connection, RecvStream, SendStream}, 11 | }; 12 | 13 | use thiserror::Error; 14 | use tokio::io::{AsyncReadExt, AsyncWriteExt}; 15 | 16 | /// Errors that can occur during network operations. 17 | #[derive(Debug, Clone, Error)] 18 | pub enum NetworkError { 19 | #[error(transparent)] 20 | ConnectError(#[from] Arc), 21 | #[error(transparent)] 22 | ConnectionError(#[from] Arc), 23 | #[error("peer closed while accepting")] 24 | PeerClosedWhileAccepting, 25 | #[error(transparent)] 26 | IoError(#[from] Arc), 27 | #[error(transparent)] 28 | ReadExactError(#[from] Arc), 29 | #[error(transparent)] 30 | WriteError(#[from] Arc), 31 | } 32 | 33 | /// IROH-based networking backend for remote actor communication. 34 | #[derive(Debug, Clone)] 35 | pub(crate) struct Network { 36 | pub(crate) endpoint: Endpoint, 37 | } 38 | 39 | /// Transport layer for IROH network connections. 40 | #[derive(Debug, Clone)] 41 | pub(crate) struct Transport { 42 | conn: Connection, 43 | } 44 | 45 | // todo Make pub(crate) by separating AnyActorRef trait 46 | /// Stream for sending data over IROH connections. 47 | #[derive(Debug)] 48 | pub struct TxStream(SendStream); 49 | 50 | // todo Make pub(crate) by separating AnyActorRef trait 51 | /// Stream for receiving data over IROH connections. 52 | #[derive(Debug)] 53 | pub struct RxStream(RecvStream); 54 | 55 | // Implementation 56 | 57 | impl Network { 58 | pub(crate) fn new(endpoint: iroh::Endpoint) -> Self { 59 | Self { endpoint } 60 | } 61 | 62 | pub(crate) fn public_key(&self) -> iroh::PublicKey { 63 | self.endpoint.node_id() 64 | } 65 | 66 | pub(crate) async fn connect(&self, addr: NodeAddr) -> Result { 67 | let conn = self 68 | .endpoint 69 | .connect(addr, b"theta") 70 | .await 71 | .map_err(|e| NetworkError::ConnectError(Arc::new(e)))?; 72 | 73 | Ok(Transport { conn }) 74 | } 75 | 76 | pub(crate) async fn accept(&self) -> Result<(PublicKey, Transport), NetworkError> { 77 | let Some(incoming) = self.endpoint.accept().await else { 78 | return Err(NetworkError::PeerClosedWhileAccepting); 79 | }; 80 | 81 | let conn = match incoming.await { 82 | Err(e) => return Err(NetworkError::ConnectionError(Arc::new(e))), 83 | Ok(conn) => conn, 84 | }; 85 | 86 | let public_key = conn 87 | .remote_node_id() 88 | .expect("remote node ID should be present"); 89 | 90 | Ok((public_key, Transport { conn })) 91 | } 92 | 93 | pub(crate) fn connect_and_prepare(&self, addr: NodeAddr) -> PreparedConn { 94 | let this = self.clone(); 95 | 96 | let fut = async move { 97 | let transport = this.connect(addr).await?; 98 | 99 | let control_tx = transport.open_uni().await?; 100 | 101 | Ok(PreparedConnInner { 102 | transport, 103 | control_tx: Arc::new(Mutex::new(control_tx)), 104 | }) 105 | } 106 | .boxed() 107 | .shared(); 108 | 109 | PreparedConn { inner: fut } 110 | } 111 | 112 | pub(crate) async fn accept_and_prepare( 113 | &self, 114 | ) -> Result<(PublicKey, PreparedConn), NetworkError> { 115 | let (public_key, transport) = self.accept().await?; 116 | 117 | let control_tx = transport.open_uni().await?; 118 | 119 | let inner = async move { 120 | Ok(PreparedConnInner { 121 | transport, 122 | control_tx: Arc::new(Mutex::new(control_tx)), 123 | }) 124 | } 125 | .boxed() 126 | .shared(); 127 | 128 | Ok((public_key, PreparedConn { inner })) 129 | } 130 | } 131 | 132 | impl Transport { 133 | pub(crate) async fn open_uni(&self) -> Result { 134 | let tx_stream = self 135 | .conn 136 | .open_uni() 137 | .await 138 | .map_err(|e| NetworkError::ConnectionError(Arc::new(e)))?; 139 | 140 | Ok(TxStream(tx_stream)) 141 | } 142 | 143 | pub(crate) async fn accept_uni(&self) -> Result { 144 | let rx_stream = self 145 | .conn 146 | .accept_uni() 147 | .await 148 | .map_err(|e| NetworkError::ConnectionError(Arc::new(e)))?; 149 | 150 | Ok(RxStream(rx_stream)) 151 | } 152 | 153 | // pub(crate) async fn close(&self) { 154 | // self.conn.close(0u32.into(), b"closed"); 155 | // } 156 | } 157 | 158 | impl TxStream { 159 | #[inline] 160 | pub(crate) async fn send_frame(&mut self, data: &[u8]) -> Result<(), NetworkError> { 161 | // todo Add too long data error 162 | self.0 163 | .write_u32(data.len() as u32) 164 | .await 165 | .map_err(|e| NetworkError::IoError(Arc::new(e)))?; 166 | 167 | self.0 168 | .write_all(data) 169 | .await 170 | .map_err(|e| NetworkError::WriteError(Arc::new(e)))?; 171 | 172 | Ok(()) 173 | } 174 | 175 | pub(crate) async fn stopped(&mut self) { 176 | let _ = self.0.stopped().await; 177 | } 178 | } 179 | 180 | impl RxStream { 181 | /// Receive a frame into a reusable buffer, allocating only if capacity is insufficient. 182 | /// - ! Expects cleared buffer 183 | #[inline] 184 | pub(crate) async fn recv_frame_into(&mut self, buf: &mut Vec) -> Result<(), NetworkError> { 185 | let len = self 186 | .0 187 | .read_u32() 188 | .await 189 | .map_err(|e| NetworkError::IoError(Arc::new(e)))? as usize; 190 | 191 | buf.resize(len, 0); 192 | 193 | self.0 194 | .read_exact(buf) 195 | .await 196 | .map_err(|e| NetworkError::ReadExactError(Arc::new(e)))?; 197 | 198 | Ok(()) 199 | } 200 | } 201 | 202 | // This is what will be actually used 203 | 204 | #[derive(Debug, Clone)] 205 | pub(crate) struct PreparedConn { 206 | inner: Shared>>, 207 | } 208 | 209 | #[derive(Debug, Clone)] 210 | struct PreparedConnInner { 211 | transport: Transport, 212 | control_tx: Arc>, 213 | } 214 | 215 | impl PreparedConn { 216 | pub(crate) async fn send_frame(&self, data: &[u8]) -> Result<(), NetworkError> { 217 | let inner = self.get().await?; 218 | 219 | inner.control_tx.lock().await.send_frame(data).await 220 | } 221 | 222 | // ! Should be called only once 223 | pub(crate) async fn control_rx(&self) -> Result { 224 | let inner = self.get().await?; 225 | 226 | inner.transport.accept_uni().await 227 | } 228 | 229 | pub(crate) async fn open_uni(&self) -> Result { 230 | let inner = self.get().await?; 231 | 232 | inner.transport.open_uni().await 233 | } 234 | 235 | pub(crate) async fn accept_uni(&self) -> Result { 236 | let inner = self.get().await?; 237 | 238 | inner.transport.accept_uni().await 239 | } 240 | 241 | // pub(crate) async fn close(&self) -> Result<(), NetworkError> { 242 | // let inner = self.get().await?; 243 | 244 | // inner.transport.close(); 245 | 246 | // Ok(()) 247 | // } 248 | 249 | async fn get(&self) -> Result { 250 | self.inner.clone().await 251 | } 252 | } 253 | -------------------------------------------------------------------------------- /theta/tests/test_persistence.rs: -------------------------------------------------------------------------------- 1 | #![cfg(all(feature = "persistence", feature = "macros", feature = "project_dir"))] 2 | 3 | use serde::{Deserialize, Serialize}; 4 | use std::{ 5 | collections::HashMap, 6 | sync::{Arc, Mutex, Once}, 7 | time::Duration, 8 | }; 9 | use tempfile::TempDir; 10 | use theta::{ 11 | actor::{Actor, ActorArgs, ActorId}, 12 | context::{Context, RootContext}, 13 | persistence::{ 14 | persistent_actor::{PersistentContextExt, PersistentSpawnExt}, 15 | storages::project_dir::LocalFs, 16 | }, 17 | prelude::ActorRef, 18 | }; 19 | use theta_macros::{ActorArgs, actor}; 20 | use tokio::time::sleep; 21 | use tracing::warn; 22 | use uuid::uuid; 23 | 24 | // Global initialization for tests 25 | static INIT: Once = Once::new(); 26 | static TEMP_DIR: Mutex> = Mutex::new(None); 27 | 28 | fn ensure_localfs_init() { 29 | INIT.call_once(|| { 30 | let temp_dir = TempDir::new().unwrap(); 31 | LocalFs::init(temp_dir.path()); 32 | *TEMP_DIR.lock().unwrap() = Some(temp_dir); 33 | }); 34 | } 35 | 36 | // Test actors 37 | #[derive(Debug, Clone, Serialize, Deserialize, ActorArgs)] 38 | pub struct Counter { 39 | pub count: i32, 40 | } 41 | 42 | // Messages for CounterActor 43 | #[derive(Debug, Clone, Serialize, Deserialize)] 44 | pub struct Increment(pub i32); 45 | 46 | #[derive(Debug, Clone, Serialize, Deserialize)] 47 | pub struct GetCount; 48 | 49 | #[derive(Debug, Clone, Serialize, Deserialize)] 50 | pub struct Save; 51 | 52 | #[actor("847d1a75-bf42-4690-b947-c3f206fda4cf", snapshot = Counter)] 53 | impl Actor for Counter { 54 | const _: () = { 55 | async |Increment(value): Increment| { 56 | self.count += value; 57 | let _ = ctx.save_snapshot(&LocalFs, self).await; 58 | }; 59 | 60 | async |_: GetCount| -> i32 { self.count }; 61 | 62 | async |_: Save| { 63 | let _ = ctx.save_snapshot(&LocalFs, self).await; 64 | }; 65 | }; 66 | } 67 | 68 | #[derive(Debug, Clone)] 69 | pub struct Manager { 70 | pub config: String, 71 | pub counters: HashMap>, 72 | } 73 | 74 | #[derive(Debug, Clone, Serialize, Deserialize)] 75 | pub struct ManagerArgs { 76 | pub config: String, 77 | pub counter_ids: HashMap, 78 | } 79 | 80 | #[derive(Debug, Clone, Serialize, Deserialize)] 81 | pub struct GetCounter { 82 | pub name: String, 83 | } 84 | 85 | impl ActorArgs for ManagerArgs { 86 | type Actor = Manager; 87 | 88 | async fn initialize(ctx: Context, cfg: &Self) -> Self::Actor { 89 | let counter_buffer: Arc>>> = Default::default(); 90 | 91 | let respawn_tasks = cfg 92 | .counter_ids 93 | .iter() 94 | .map(|(name, id)| { 95 | let counter_buffer = counter_buffer.clone(); 96 | let ctx = ctx.clone(); 97 | let name = name.clone(); 98 | let id = id.clone(); 99 | 100 | async move { 101 | if let Ok(counter) = ctx 102 | .respawn_or(&LocalFs, id.clone(), || (), || Counter { count: 0 }) 103 | .await 104 | { 105 | counter_buffer.lock().unwrap().insert(name, counter); 106 | } else { 107 | warn!("failed to respawn counter for Id: {id}"); 108 | } 109 | } 110 | }) 111 | .collect::>(); 112 | 113 | // todo Need to find UnwindSafe parallel execution 114 | for task in respawn_tasks { 115 | task.await; 116 | } 117 | 118 | let counters = Arc::try_unwrap(counter_buffer) 119 | .unwrap() 120 | .into_inner() 121 | .unwrap(); 122 | 123 | Self::Actor { 124 | config: cfg.config.clone(), 125 | counters, 126 | } 127 | } 128 | } 129 | 130 | #[actor("4397a912-188c-45ea-8a3d-6c4ebef95911", snapshot = ManagerArgs)] 131 | impl Actor for Manager { 132 | const _: () = { 133 | async |GetCounter { name }: GetCounter| -> Option> { 134 | self.counters.get(&name).cloned() 135 | }; 136 | }; 137 | } 138 | 139 | impl From<&Manager> for ManagerArgs { 140 | fn from(actor: &Manager) -> Self { 141 | Self { 142 | config: actor.config.clone(), 143 | counter_ids: actor 144 | .counters 145 | .iter() 146 | .map(|(name, actor)| (name.clone(), actor.id())) 147 | .collect(), 148 | } 149 | } 150 | } 151 | 152 | #[tokio::test] 153 | async fn test_simple_persistent_actor() { 154 | ensure_localfs_init(); 155 | 156 | let actor_id = uuid::uuid!("9714394b-1dfe-4e2a-9f97-e19272150546"); 157 | 158 | // Create a root context (you'll need to adapt this to your actual context creation) 159 | let ctx = RootContext::init_local(); 160 | 161 | // Test 1: Create a new persistent actor 162 | let counter = ctx 163 | .spawn_persistent(&LocalFs, actor_id, Counter { count: 5 }) 164 | .await 165 | .unwrap(); 166 | 167 | // Verify initial state 168 | let count = counter.ask(GetCount).await.unwrap(); 169 | assert_eq!(count, 5); 170 | 171 | // Modify state 172 | counter.tell(Increment(10)).unwrap(); 173 | sleep(Duration::from_millis(10)).await; // Let the message process 174 | 175 | let count = counter.ask(GetCount).await.unwrap(); 176 | assert_eq!(count, 15); 177 | 178 | // Drop the actor reference 179 | drop(counter); 180 | 181 | // Test 2: Respawn the actor from persistence 182 | let respawned_counter: ActorRef = ctx.respawn(&LocalFs, actor_id, ()).await.unwrap(); 183 | 184 | // Verify state was restored 185 | let count = respawned_counter.ask(GetCount).await.unwrap(); 186 | assert_eq!(count, 15); 187 | } 188 | 189 | #[tokio::test] 190 | async fn test_respawn_or_fallback() { 191 | ensure_localfs_init(); 192 | 193 | let actor_id = uuid!("02efdacf-aa39-48bc-9750-43cf4c96b9ba"); 194 | 195 | let ctx = RootContext::init_local(); 196 | 197 | // Test respawn_or with non-existent persistence 198 | let counter = ctx 199 | .respawn_or(&LocalFs, actor_id, || (), || Counter { count: 100 }) 200 | .await 201 | .unwrap(); 202 | 203 | // Should create new actor with fallback args 204 | let count = counter.ask(GetCount).await.unwrap(); 205 | assert_eq!(count, 100); 206 | 207 | // Verify it was registered as persistent 208 | // let persistence_key = Counter::persistence_key(&counter.downgrade()); 209 | // assert!(persistence_key.is_some()); 210 | assert_eq!(counter.id(), actor_id); 211 | } 212 | 213 | #[tokio::test] 214 | async fn test_manager_with_persistent_children() { 215 | ensure_localfs_init(); 216 | 217 | let manager_id = uuid!("18fa37e7-a10c-4c6c-8022-132c125cde21"); 218 | let counter1_id = uuid!("fe5f6f30-0b49-4fc8-8db8-0522ab8fc0ee"); 219 | let counter2_id = uuid!("4a525d16-3f57-4d18-9fd8-934061b9351e"); 220 | 221 | let ctx = RootContext::init_local(); 222 | 223 | // Create some persistent counters first 224 | let counter1: ActorRef = ctx 225 | .spawn_persistent(&LocalFs, counter1_id, Counter { count: 10 }) 226 | .await 227 | .unwrap(); 228 | 229 | let counter2: ActorRef = ctx 230 | .spawn_persistent(&LocalFs, counter2_id, Counter { count: 20 }) 231 | .await 232 | .unwrap(); 233 | 234 | let _ = counter1.ask(Save).await; 235 | let _ = counter2.ask(Save).await; 236 | 237 | drop(counter1); 238 | drop(counter2); 239 | 240 | // Create manager with references to these counters 241 | let mut counter_ids = HashMap::new(); 242 | counter_ids.insert("c1".to_string(), counter1_id); 243 | counter_ids.insert("c2".to_string(), counter2_id); 244 | 245 | let manager = ctx 246 | .spawn_persistent( 247 | &LocalFs, 248 | manager_id, 249 | ManagerArgs { 250 | config: "test_manager".to_string(), 251 | counter_ids, 252 | }, 253 | ) 254 | .await 255 | .unwrap(); 256 | 257 | let counter_1 = manager 258 | .ask(GetCounter { 259 | name: "c1".to_string(), 260 | }) 261 | .await 262 | .unwrap() 263 | .unwrap(); 264 | 265 | let counter_2 = manager 266 | .ask(GetCounter { 267 | name: "c2".to_string(), 268 | }) 269 | .await 270 | .unwrap() 271 | .unwrap(); 272 | 273 | assert_eq!(counter_1.id(), counter1_id); 274 | assert_eq!(counter_2.id(), counter2_id); 275 | 276 | let count_1 = counter_1.ask(GetCount).await.unwrap(); 277 | let count_2 = counter_2.ask(GetCount).await.unwrap(); 278 | 279 | assert_eq!(count_1, 10); 280 | assert_eq!(count_2, 20); 281 | } 282 | 283 | #[tokio::test] 284 | async fn test_persistence_file_operations() { 285 | ensure_localfs_init(); 286 | 287 | let actor_id = uuid!("aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee"); 288 | 289 | let ctx = RootContext::init_local(); 290 | 291 | // Test snapshot creation and persistence by creating an actor 292 | let counter = ctx 293 | .spawn_persistent(&LocalFs, actor_id, Counter { count: 999 }) 294 | .await 295 | .unwrap(); 296 | 297 | // Verify initial state 298 | let count = counter.ask(GetCount).await.unwrap(); 299 | assert_eq!(count, 999); 300 | 301 | // Modify and save state 302 | counter.tell(Increment(1)).unwrap(); 303 | sleep(Duration::from_millis(10)).await; // Let the increment and save process 304 | 305 | let count = counter.ask(GetCount).await.unwrap(); 306 | assert_eq!(count, 1000); 307 | 308 | // Drop the actor 309 | drop(counter); 310 | 311 | // Read snapshot back by respawning 312 | let restored_counter: ActorRef = ctx.respawn(&LocalFs, actor_id, ()).await.unwrap(); 313 | 314 | let restored_count = restored_counter.ask(GetCount).await.unwrap(); 315 | assert_eq!(restored_count, 1000); 316 | } 317 | -------------------------------------------------------------------------------- /workflow/_DOCUMENTATION_STYLE_GUIDE.md: -------------------------------------------------------------------------------- 1 | # Theta Documentation Style Guide 2 | 3 | This document defines the uniform documentation format for all items in the Theta actor framework codebase. The goal is to provide minimal yet comprehensive documentation that includes essential information and avoids verbosity. 4 | 5 | ## General Principles 6 | 7 | - **Concise and Essential**: Focus on what the item does, not how it's implemented 8 | - **Consistent Structure**: Follow the same pattern for similar item types 9 | - **Executable Examples**: Use working code examples with hidden test setup where possible 10 | - **No Verbose Explanations**: Avoid implementation details and lengthy descriptions 11 | - **Essential Information Only**: Include only what users need to know to use the item effectively 12 | 13 | ## Item Type Formats 14 | 15 | ### 1. Crate Root Documentation (`lib.rs`) 16 | 17 | **Structure:** 18 | #[cfg(feature = "full")] 19 | ```rust 20 | //! # [Crate Name]: [Brief Description] 21 | //! 22 | //! [2-3 sentence overview of what the crate does] 23 | //! 24 | //! ## Key Features 25 | //! 26 | //! - **[Feature]**: [Brief description] 27 | //! - **[Feature]**: [Brief description] 28 | //! - ... 29 | //! 30 | //! ## Quick Start 31 | //! 32 | //! #[cfg(feature = "full")] 33 | //! 34 | //! #[cfg(feature = "full")] 35 | //! ```rust 36 | //! [Working example showing basic usage] 37 | //! ``` 38 | //! 39 | //! ## Features 40 | //! 41 | //! - **`feature-name`** (default): [Description] 42 | //! - **`feature-name`**: [Description] 43 | //! - ... 44 | //! 45 | //! [Optional: Links to external resources] 46 | ``` 47 | 48 | **Essential Items:** 49 | - Brief crate description (1 line) 50 | - Key features (bullet points) 51 | - Working quick start example 52 | - Feature flags documentation 53 | 54 | **Optional Items:** 55 | - Links to external documentation 56 | - Advanced usage patterns 57 | 58 | ### 2. Module Documentation 59 | 60 | **Structure:** 61 | #[cfg(feature = "full")] 62 | ```rust 63 | //! [Brief description of module purpose]. 64 | //! 65 | //! [1-2 sentences describing what the module provides] 66 | //! 67 | //! # Core Types 68 | //! 69 | //! - [`Type`] - [Brief description] 70 | //! - [`Type`] - [Brief description] 71 | //! - ... 72 | //! 73 | //! # [Category] (e.g., Communication Patterns, Usage Patterns) 74 | //! 75 | //! ## [Pattern Name] 76 | //! [Brief description]: 77 | //! 78 | //! #[cfg(feature = "full")] 79 | //! ```rust 80 | //! [Working example] 81 | //! ``` 82 | ``` 83 | 84 | **Essential Items:** 85 | - Brief module purpose 86 | - List of core types with descriptions 87 | - Usage patterns with working examples 88 | 89 | **Optional Items:** 90 | - Multiple pattern categories 91 | - Advanced examples 92 | 93 | ### 3. Struct/Enum Documentation 94 | 95 | **Structure:** 96 | #[cfg(feature = "full")] 97 | ```rust 98 | /// [Brief description of what the type represents]. 99 | /// 100 | /// [1-2 sentences about purpose and key characteristics] 101 | /// 102 | /// # [Optional: Usage/Examples] 103 | /// 104 | /// ```rust 105 | /// [Simple working example] 106 | /// ``` 107 | /// 108 | /// # Type Parameters (if generic) 109 | /// 110 | /// * `T` - [Description of type parameter] 111 | ``` 112 | 113 | **Essential Items:** 114 | - Brief description (1 line) 115 | - Purpose and characteristics (1-2 sentences) 116 | 117 | **Optional Items:** 118 | - Usage examples for complex types 119 | - Type parameter documentation for generics 120 | - Important notes or warnings 121 | 122 | ### 4. Trait Documentation 123 | 124 | **Structure:** 125 | #[cfg(feature = "full")] 126 | ```rust 127 | /// [Brief description of trait purpose]. 128 | /// 129 | /// [1-2 sentences about what implementing this trait provides] 130 | /// 131 | /// # Usage with [Related System/Macro] 132 | /// 133 | /// [Brief explanation of how trait is typically used] 134 | /// 135 | /// ## [Pattern Name] 136 | /// 137 | /// [Description of usage pattern]: 138 | /// 139 | /// ```rust 140 | /// [Working example] 141 | /// ``` 142 | /// 143 | /// # [Category] (e.g., Default Implementations, Key Methods) 144 | /// 145 | /// [Brief explanations] 146 | ``` 147 | 148 | **Essential Items:** 149 | - Brief trait purpose 150 | - How trait fits into the system 151 | - Basic usage example 152 | 153 | **Optional Items:** 154 | - Usage patterns 155 | - Default implementations 156 | - Integration with other systems 157 | 158 | ### 5. Method Documentation 159 | 160 | **Structure:** 161 | #[cfg(feature = "full")] 162 | ```rust 163 | /// [Brief description of what method does]. 164 | /// 165 | /// [Optional: 1 sentence about important behavior/notes] 166 | /// 167 | /// # Arguments 168 | /// 169 | /// * `param` - [Description of parameter purpose/constraints] 170 | /// * `param2` - [Description of parameter purpose/constraints] 171 | /// 172 | /// # Return 173 | /// 174 | /// [Description of return value and any important characteristics] 175 | /// 176 | /// # Errors (if method can fail) 177 | /// 178 | /// [Description of error conditions] 179 | /// 180 | /// # Note (if important) 181 | /// 182 | /// [Important usage note, warning, or constraint] 183 | ``` 184 | 185 | **Essential Items for PUBLIC methods:** 186 | - Brief description of method purpose (1 line) 187 | - Arguments section (for all parameters, even if obvious from signature) 188 | - Returns section (for all non-unit returns) 189 | - Errors section (if method returns Result) 190 | 191 | **Optional Items:** 192 | - Important behavioral notes 193 | - Usage warnings or constraints 194 | - Simple usage examples for complex methods 195 | 196 | ### 6. Function Documentation 197 | 198 | **Structure:** 199 | #[cfg(feature = "full")] 200 | ```rust 201 | /// [Brief description of function purpose]. 202 | /// 203 | /// [Optional: 1 sentence about key behavior] 204 | /// 205 | /// # Arguments 206 | /// 207 | /// * `param` - [Description of parameter purpose/constraints] 208 | /// * `param2` - [Description of parameter purpose/constraints] 209 | /// 210 | /// # Return 211 | /// 212 | /// [Description of return value and any important characteristics] 213 | /// 214 | /// # Errors (if function can fail) 215 | /// 216 | /// [Description of error conditions] 217 | /// 218 | /// # Examples (for complex functions) 219 | /// 220 | /// ```rust 221 | /// [Working example] 222 | /// ``` 223 | ``` 224 | 225 | **Essential Items for PUBLIC functions:** 226 | - Brief function purpose 227 | - Arguments section (for all parameters, even if obvious from signature) 228 | - Returns section (for all non-unit returns) 229 | - Errors section (if function returns Result) 230 | 231 | **Optional Items:** 232 | - Examples for complex functions 233 | - Error conditions 234 | - Usage notes 235 | 236 | ### 7. Macro Documentation 237 | 238 | **Structure:** 239 | #[cfg(feature = "full")] 240 | ```rust 241 | /// [Brief description of macro purpose]. 242 | /// 243 | /// [1-2 sentences about what the macro generates/provides] 244 | /// 245 | /// # Default Implementations (if applicable) 246 | /// 247 | /// The macro automatically provides: 248 | /// - [Item]: [Description] 249 | /// - [Item]: [Description] 250 | /// 251 | /// # Usage 252 | /// 253 | /// ## [Pattern Name] 254 | /// ```rust 255 | /// [Working example] 256 | /// ``` 257 | /// 258 | /// ## [Pattern Name] 259 | /// ```rust 260 | /// [Working example] 261 | /// ``` 262 | /// 263 | /// # Arguments 264 | /// 265 | /// [Description of required arguments and optional parameters] 266 | ``` 267 | 268 | **Essential Items:** 269 | - Brief macro purpose 270 | - What the macro generates/provides 271 | - Usage examples for different patterns 272 | - Required arguments 273 | 274 | **Optional Items:** 275 | - Default implementations 276 | - Multiple usage patterns 277 | - Optional parameters 278 | 279 | ### 8. Type Alias Documentation 280 | 281 | **Structure:** 282 | #[cfg(feature = "full")] 283 | ```rust 284 | /// [Brief description of what the alias represents]. 285 | /// 286 | /// [Optional: 1 sentence about when to use it] 287 | ``` 288 | 289 | **Essential Items:** 290 | - Brief description of alias purpose 291 | 292 | **Optional Items:** 293 | - When to use the alias vs. the underlying type 294 | 295 | ### 9. Constant Documentation 296 | 297 | **Structure:** 298 | #[cfg(feature = "full")] 299 | ```rust 300 | /// [Brief description of constant purpose/value]. 301 | ``` 302 | 303 | **Essential Items:** 304 | - Brief description of what the constant represents 305 | 306 | ## Code Example Guidelines 307 | 308 | ### Hidden Test Setup 309 | 310 | Use `# ` comments to hide boilerplate while showing clean examples: 311 | 312 | ```rust 313 | /// ```rust 314 | /// # use theta::prelude::*; 315 | /// # #[derive(Debug, Clone, ActorArgs)] 316 | /// # struct MyActor; 317 | /// # #[actor("uuid")] 318 | /// # impl Actor for MyActor {} 319 | /// let actor = ctx.spawn(MyActor); 320 | /// ``` 321 | ``` 322 | 323 | ### Working Examples 324 | 325 | - All examples should compile and demonstrate real usage 326 | - Use `ignore` only when examples require complex runtime setup 327 | - Prefer compile-only examples over complex async examples 328 | - Show the most common usage pattern first 329 | 330 | ### Example Complexity 331 | 332 | - **Simple**: Method calls, basic construction 333 | - **Medium**: Complete actor definitions, message handling 334 | - **Complex**: Full applications with multiple actors (use sparingly) 335 | 336 | ## Documentation Anti-Patterns 337 | 338 | ### Avoid These: 339 | 340 | - **Verbose explanations**: Implementation details and lengthy descriptions 341 | - **Redundant sections**: Repeating information already in type signatures 342 | - **Complex examples**: Examples requiring extensive setup 343 | - **Implementation notes**: How something works internally 344 | - **Excessive use cases**: Long lists of when to use something 345 | 346 | ### Instead: 347 | 348 | - **Essential information**: What users need to know to use it 349 | - **Clear examples**: Working code demonstrating usage 350 | - **Concise descriptions**: One-line summaries of purpose 351 | - **Important notes**: Only critical warnings or constraints 352 | 353 | ## Review Checklist 354 | 355 | Before committing documentation: 356 | 357 | - [ ] Brief, one-line description of purpose 358 | - [ ] Essential information only (no implementation details) 359 | - [ ] Working code examples (when applicable) 360 | - [ ] Consistent format for item type 361 | - [ ] No verbose explanations or redundant sections 362 | - [ ] Important warnings/constraints included 363 | - [ ] Examples compile and demonstrate real usage 364 | - [ ] Hidden test setup used for clean examples 365 | -------------------------------------------------------------------------------- /theta/examples/dedup.rs: -------------------------------------------------------------------------------- 1 | use std::{env, process::Command, str::FromStr, time::Instant}; 2 | 3 | use iroh::{Endpoint, PublicKey, SecretKey, dns::DnsResolver}; 4 | use rand::thread_rng; 5 | use serde::{Deserialize, Serialize}; 6 | use theta::prelude::*; 7 | use theta_macros::ActorArgs; 8 | use tracing::{error, info}; 9 | 10 | use tracing_subscriber::fmt::time::ChronoLocal; 11 | use url::Url; 12 | 13 | #[derive(Debug, Clone, ActorArgs)] 14 | pub struct PingPong; 15 | 16 | #[derive(Debug, Clone, Serialize, Deserialize)] 17 | pub struct Ping { 18 | pub source: PublicKey, 19 | } 20 | 21 | #[derive(Debug, Clone, Serialize, Deserialize)] 22 | pub struct Pong {} 23 | 24 | #[actor("f68fe56f-8aa9-4f90-8af8-591a06e2818a")] 25 | impl Actor for PingPong { 26 | const _: () = { 27 | async |msg: Ping| -> Pong { 28 | info!("received ping from {}", msg.source); 29 | Pong {} 30 | }; 31 | }; 32 | } 33 | 34 | #[tokio::main] 35 | async fn main() -> anyhow::Result<()> { 36 | // Initialize tracing subscriber first, then LogTracer 37 | tracing_subscriber::fmt() 38 | .with_env_filter("info,theta=trace") 39 | .with_timer(ChronoLocal::new("%Y-%m-%d %H:%M:%S%.3f %Z".into())) 40 | .init(); 41 | 42 | tracing_log::LogTracer::init().ok(); 43 | 44 | let args: Vec = env::args().collect(); 45 | 46 | // Parse command line arguments for keys 47 | let (our_secret_key, other_public_key, child_secret_for_spawn) = if args.len() == 3 { 48 | // Child process mode: program_name public_key secret_key 49 | // args[0] = program_name, args[1] = public_key, args[2] = secret_key 50 | info!("Child process mode - using provided keys"); 51 | info!("Received args[1] (other's public key): {}", &args[1]); 52 | info!( 53 | "Received args[2] (our secret key bytes): {} chars", 54 | &args[2].len() 55 | ); 56 | 57 | let other_public_key = PublicKey::from_str(&args[1])?; 58 | info!("Parsed other_public_key: {}", other_public_key); 59 | 60 | // Parse the debug format of bytes array: "[1, 2, 3, ...]" 61 | let bytes_str = args[2].trim_start_matches('[').trim_end_matches(']'); 62 | let our_secret_key_bytes: Vec = bytes_str 63 | .split(", ") 64 | .map(|s| s.parse::()) 65 | .collect::, _>>()?; 66 | let our_secret_key = SecretKey::try_from(&our_secret_key_bytes[..])?; 67 | info!( 68 | "Parsed our_secret_key, public version: {}", 69 | our_secret_key.public() 70 | ); 71 | 72 | (our_secret_key, other_public_key, None) 73 | } else { 74 | // Parent process mode: generate two secret keys 75 | info!("Parent process mode - generating keys..."); 76 | 77 | let mut rng = thread_rng(); 78 | let parent_secret = SecretKey::generate(&mut rng); 79 | let child_secret = SecretKey::generate(&mut rng); 80 | 81 | let parent_public = parent_secret.public(); 82 | let child_public = child_secret.public(); 83 | 84 | info!("Generated parent key: {}", parent_public); 85 | info!("Generated child key: {}", child_public); 86 | 87 | (parent_secret, child_public, Some(child_secret)) 88 | }; 89 | 90 | // Spawn child process if we're in parent mode 91 | if let Some(child_secret) = child_secret_for_spawn { 92 | // Convert child secret key bytes to string for passing to child process 93 | let child_secret_bytes = child_secret.to_bytes(); 94 | let child_secret_str = format!("{:?}", child_secret_bytes); 95 | 96 | use std::fs::OpenOptions; 97 | use std::process::Stdio; 98 | 99 | // Create log file for child process with ANSI color support 100 | let child_log_file = OpenOptions::new() 101 | .create(true) 102 | .write(true) 103 | .truncate(true) 104 | .open("child_process.log")?; 105 | 106 | let _child = Command::new("./target/debug/examples/dedup") 107 | .args(&[&our_secret_key.public().to_string(), &child_secret_str]) 108 | .stdout(Stdio::from(child_log_file.try_clone()?)) 109 | .stderr(Stdio::from(child_log_file)) 110 | .env("FORCE_COLOR", "1") // Force ANSI colors even when redirecting 111 | .env("CLICOLOR_FORCE", "1") // Additional color forcing 112 | .spawn()?; 113 | 114 | info!("Child process spawned. Both will try to connect simultaneously."); 115 | info!("Child process logs are being written to: child_process.log"); 116 | } 117 | 118 | // Common initialization code 119 | let dns = DnsResolver::with_nameserver("8.8.8.8:53".parse().unwrap()); 120 | let endpoint = Endpoint::builder() 121 | .alpns(vec![b"theta".to_vec()]) 122 | .discovery_n0() 123 | .dns_resolver(dns) 124 | .secret_key(our_secret_key) 125 | .bind() 126 | .await?; 127 | 128 | let ctx = RootContext::init(endpoint); 129 | let public_key = ctx.public_key(); 130 | 131 | info!("RootContext initialized with public key: {public_key}"); 132 | info!("Will attempt to connect to other peer with public key: {other_public_key}"); 133 | info!( 134 | "Key comparison: our_key={}, other_key={}, our_key > other_key = {}", 135 | public_key, 136 | other_public_key, 137 | public_key > other_public_key 138 | ); 139 | 140 | info!("spawning PingPong actor..."); 141 | let ping_pong = ctx.spawn(PingPong); 142 | 143 | info!("binding PingPong actor to 'ping_pong' name..."); 144 | let _ = ctx.bind("ping_pong", ping_pong); 145 | 146 | // Give some time for the actor to become fully discoverable and network to stabilize 147 | tokio::time::sleep(tokio::time::Duration::from_millis(1000)).await; 148 | 149 | // Common connection and ping logic 150 | info!("Connecting to peer: {}", other_public_key); 151 | 152 | let ping_pong_url = Url::parse(&format!("iroh://ping_pong@{other_public_key}"))?; 153 | info!("Constructed URL for lookup: {}", ping_pong_url); 154 | 155 | // Retry lookup with exponential backoff 156 | let mut retry_delay = tokio::time::Duration::from_millis(500); 157 | let max_retry_delay = tokio::time::Duration::from_secs(30); 158 | let mut retry_count = 0; 159 | 160 | let other_ping_pong = loop { 161 | info!( 162 | "Attempting lookup {} for {}", 163 | retry_count + 1, 164 | ping_pong_url 165 | ); 166 | match ActorRef::::lookup(&ping_pong_url).await { 167 | Ok(actor) => { 168 | info!( 169 | "Successfully connected to peer after {} attempts", 170 | retry_count + 1 171 | ); 172 | break actor; 173 | } 174 | Err(e) => { 175 | retry_count += 1; 176 | error!( 177 | "Lookup attempt {} failed for URL: {ping_pong_url}. Error: {e}", 178 | retry_count 179 | ); 180 | 181 | // Wait before retrying with exponential backoff 182 | info!("Retrying in {:?}...", retry_delay); 183 | tokio::time::sleep(retry_delay).await; 184 | 185 | // Increase delay for next retry, capped at max_retry_delay 186 | retry_delay = std::cmp::min(retry_delay * 2, max_retry_delay); 187 | } 188 | } 189 | }; 190 | 191 | info!("sending ping to {ping_pong_url} every 5 seconds. Press Ctrl-C to stop."); 192 | 193 | let mut interval = tokio::time::interval(tokio::time::Duration::from_secs(5)); 194 | 195 | // Setup shutdown signal handling 196 | let (shutdown_tx, mut shutdown_rx) = tokio::sync::watch::channel(false); 197 | 198 | tokio::spawn(async move { 199 | tokio::signal::ctrl_c().await.ok(); 200 | info!("Received Ctrl-C signal, shutting down..."); 201 | let _ = shutdown_tx.send(true); 202 | }); 203 | 204 | let mut current_actor_ref = other_ping_pong; 205 | 206 | loop { 207 | // Check for shutdown signal 208 | tokio::select! { 209 | _ = shutdown_rx.changed() => { 210 | if *shutdown_rx.borrow() { 211 | info!("Shutdown signal received, exiting..."); 212 | break; 213 | } 214 | } 215 | _ = interval.tick() => { 216 | // Continue with ping logic 217 | } 218 | } 219 | 220 | let ping = Ping { 221 | source: public_key.clone(), 222 | }; 223 | 224 | info!("sending ping to {}", current_actor_ref.id()); 225 | let sent_instant = Instant::now(); 226 | match current_actor_ref.ask(ping).await { 227 | Err(e) => { 228 | error!("failed to send ping: {e}"); 229 | info!("Connection lost, attempting to reconnect..."); 230 | 231 | // Try to reconnect 232 | let mut reconnect_delay = tokio::time::Duration::from_millis(500); 233 | let max_reconnect_delay = tokio::time::Duration::from_secs(10); 234 | let mut reconnect_attempts = 0; 235 | 236 | loop { 237 | match ActorRef::::lookup(&ping_pong_url).await { 238 | Ok(actor) => { 239 | info!( 240 | "Successfully reconnected after {} attempts", 241 | reconnect_attempts + 1 242 | ); 243 | current_actor_ref = actor; 244 | break; 245 | } 246 | Err(e) => { 247 | reconnect_attempts += 1; 248 | error!("Reconnection attempt {} failed: {e}", reconnect_attempts); 249 | 250 | info!("Retrying reconnection in {:?}...", reconnect_delay); 251 | tokio::time::sleep(reconnect_delay).await; 252 | 253 | // Increase delay for next retry, capped at max_reconnect_delay 254 | reconnect_delay = 255 | std::cmp::min(reconnect_delay * 2, max_reconnect_delay); 256 | } 257 | } 258 | } 259 | } 260 | Ok(_pong) => { 261 | let elapsed = sent_instant.elapsed(); 262 | info!( 263 | "received pong from {} in {elapsed:?}", 264 | current_actor_ref.id() 265 | ); 266 | } 267 | } 268 | } 269 | 270 | Ok(()) 271 | } 272 | -------------------------------------------------------------------------------- /theta/src/remote/serde.rs: -------------------------------------------------------------------------------- 1 | use futures::channel::oneshot; 2 | use iroh::PublicKey; 3 | use serde::{Deserialize, Deserializer, Serialize, Serializer}; 4 | use tracing::{error, trace, warn}; 5 | 6 | use crate::{ 7 | actor::{Actor, ActorId}, 8 | base::{BindingError, Hex, Ident}, 9 | context::RootContext, 10 | message::Continuation, 11 | prelude::ActorRef, 12 | remote::{ 13 | base::{Key, Tag}, 14 | peer::{LocalPeer, PEER}, 15 | }, 16 | }; 17 | 18 | /// Trait for types that can be deserialized from tagged byte streams. 19 | /// 20 | /// This trait enables reconstruction of actor messages from their serialized 21 | /// form in remote communication. Each message type gets a unique tag for 22 | /// identification during deserialization. 23 | /// 24 | /// # Usage with `#[actor]` Macro 25 | /// 26 | /// The `#[actor]` macro automatically implements this trait for actor message types. 27 | /// Manual implementation is typically not needed. 28 | pub trait FromTaggedBytes: Sized { 29 | /// Deserialize a value from its tag and byte representation. 30 | /// 31 | /// # Arguments 32 | /// 33 | /// * `tag` - The message type identifier 34 | /// * `bytes` - The serialized message data 35 | fn from(tag: Tag, bytes: &[u8]) -> Result; 36 | } 37 | 38 | pub(crate) type MsgPackDto = (::Msg, ContinuationDto); 39 | 40 | /// Forwarding information for message routing in remote communication. 41 | #[derive(Debug, Clone, Serialize, Deserialize)] 42 | pub(crate) struct ForwardInfo { 43 | pub(crate) actor_id: ActorId, 44 | pub(crate) tag: Tag, 45 | } 46 | 47 | /// Serializable actor reference for remote communication. 48 | #[derive(Debug, Clone, Serialize, Deserialize)] 49 | pub(crate) enum ActorRefDto { 50 | First { 51 | actor_id: ActorId, // First party to the recipient peer, recipient local actor 52 | }, 53 | Second { 54 | actor_id: ActorId, // Second party remote actor to the recipient peer, sender local actor 55 | }, 56 | Third { 57 | actor_id: ActorId, // Third party to both this peer and the recipient peer 58 | public_key: PublicKey, 59 | }, 60 | } 61 | 62 | /// Serializable continuation for remote message handling. 63 | #[derive(Debug, Clone, Serialize, Deserialize)] 64 | pub(crate) enum ContinuationDto { 65 | Nil, 66 | Reply(Key), // None means self reply 67 | Forward { 68 | ident: Ident, 69 | mb_public_key: Option, // None means second party Some means third party to the recipient 70 | tag: Tag, 71 | }, 72 | } 73 | 74 | // Implementations 75 | 76 | impl From<&ActorRef> for ActorRefDto { 77 | fn from(actor: &ActorRef) -> Self { 78 | let actor_id = actor.id(); 79 | 80 | match LocalPeer::inst().get_import_public_key(&actor_id) { 81 | None => { 82 | RootContext::bind_impl(*actor_id.as_bytes(), actor.downgrade()); 83 | // todo Need to find way to clean up the binding when no export exists anymore 84 | ActorRefDto::Second { actor_id } 85 | } 86 | Some(public_key) => match PEER.with(|p| p.public_key() == public_key) { 87 | false => ActorRefDto::Third { 88 | public_key, 89 | actor_id, 90 | }, 91 | true => ActorRefDto::First { actor_id }, // Second party remote actor to the recipient peer 92 | }, 93 | } 94 | } 95 | } 96 | 97 | impl Serialize for ActorRef { 98 | fn serialize(&self, serializer: S) -> Result 99 | where 100 | S: Serializer, 101 | { 102 | ActorRefDto::from(self).serialize(serializer) 103 | } 104 | } 105 | 106 | impl<'de, A: Actor> Deserialize<'de> for ActorRef { 107 | fn deserialize(deserializer: D) -> Result 108 | where 109 | D: Deserializer<'de>, 110 | { 111 | ActorRefDto::deserialize(deserializer)? 112 | .try_into() 113 | .map_err(|e| { 114 | serde::de::Error::custom(format!( 115 | "failed to construct ActorRef from ActorRefDto: {e}" 116 | )) 117 | }) 118 | } 119 | } 120 | 121 | impl TryFrom for ActorRef { 122 | type Error = BindingError; // Failes only when the actor is imported but of different type 123 | 124 | fn try_from(dto: ActorRefDto) -> Result { 125 | match dto { 126 | ActorRefDto::First { actor_id } => { 127 | Ok(ActorRef::::lookup_local_impl(actor_id.as_bytes())?) 128 | } // First party local actor 129 | ActorRefDto::Second { actor_id } => { 130 | match LocalPeer::inst().get_or_import_actor::(actor_id, || { 131 | // Second party remote actor 132 | PEER.get() 133 | }) { 134 | None => Err(BindingError::DowncastError), // The actor is imported but of different type 135 | Some(actor) => Ok(actor), 136 | } 137 | } 138 | ActorRefDto::Third { 139 | public_key, 140 | actor_id, 141 | } => { 142 | match LocalPeer::inst().get_or_import_actor::(actor_id, || { 143 | LocalPeer::inst().get_or_connect_peer(public_key) 144 | }) { 145 | None => Err(BindingError::DowncastError), // The actor is imported but of different type 146 | Some(actor) => Ok(actor), 147 | } 148 | } 149 | } 150 | } 151 | } 152 | 153 | // Continuation 154 | // ! Continuation it self is not serializable, since it has to be consumed 155 | impl Continuation { 156 | pub(crate) async fn into_dto(self) -> Option { 157 | match self { 158 | Continuation::Nil => Some(ContinuationDto::Nil), 159 | Continuation::Reply(tx) => { 160 | let (bin_reply_tx, bin_reply_rx) = oneshot::channel(); 161 | 162 | match tx.send(Box::new(bin_reply_rx)) { 163 | Err(_) => { 164 | warn!("failed to send binary reply tx"); 165 | Some(ContinuationDto::Nil) 166 | } 167 | Ok(_) => PEER 168 | // .with(|p| { 169 | // if p.is_canceled() { 170 | // warn!( 171 | // host = %p, 172 | // "failed to arrange recv remote reply" 173 | // ); 174 | // None 175 | // } else { 176 | // Some(p.arrange_recv_reply(bin_reply_tx)) 177 | // } 178 | // }) 179 | // .map(ContinuationDto::Reply), 180 | .with(|p| Some(ContinuationDto::Reply(p.arrange_recv_reply(bin_reply_tx)))), 181 | } 182 | } 183 | Continuation::Forward(tx) => { 184 | let (info_tx, info_rx) = oneshot::channel::(); 185 | 186 | if tx.send(Box::new(info_tx)).is_err() { 187 | warn!("failed to request forward info"); 188 | return Some(ContinuationDto::Nil); 189 | }; 190 | 191 | let Ok(info) = info_rx.await else { 192 | warn!("failed to receive forward info"); 193 | return Some(ContinuationDto::Nil); 194 | }; 195 | 196 | let mb_public_key = match LocalPeer::inst().get_import_public_key(&info.actor_id) { 197 | None => Some(LocalPeer::inst().public_key()), // This peer, second party to the recipient peer 198 | Some(public_key) => match PEER.with(|p| p.public_key() == public_key) { 199 | false => Some(public_key), // Third party to both this peer and the recipient peer 200 | true => None, // Recipient itself, local to the recipient peer 201 | }, 202 | }; 203 | 204 | Some(ContinuationDto::Forward { 205 | ident: *info.actor_id.as_bytes(), 206 | mb_public_key, 207 | tag: info.tag, 208 | }) 209 | } 210 | _ => panic!("only Nil | Reply | Forward continuations are serializable"), 211 | } 212 | } 213 | } 214 | 215 | impl From for Continuation { 216 | fn from(dto: ContinuationDto) -> Self { 217 | match dto { 218 | ContinuationDto::Nil => Continuation::Nil, 219 | ContinuationDto::Reply(key) => Continuation::BinReply { 220 | peer: PEER.get(), 221 | key, 222 | }, 223 | ContinuationDto::Forward { 224 | ident, 225 | mb_public_key, 226 | tag, 227 | } => match mb_public_key { 228 | None => { 229 | let (tx, rx) = oneshot::channel::>(); 230 | 231 | tokio::spawn({ 232 | PEER.scope(PEER.get(), async move { 233 | trace!( 234 | ident = %Hex(&ident), 235 | "Scheduling deligated local forwarding" 236 | ); 237 | 238 | let Ok(bytes) = rx.await else { 239 | return warn!( 240 | ident = %Hex(&ident), 241 | "failed to receive binary local forwarding" 242 | ); 243 | }; 244 | 245 | let any_actor = 246 | match RootContext::lookup_any_local_unchecked_impl(&ident) { 247 | Err(err) => { 248 | return error!( 249 | ident = %Hex(&ident), 250 | %err, 251 | "failed to find local forward target", 252 | ); 253 | } 254 | Ok(any_actor) => any_actor, 255 | }; 256 | 257 | if let Err(err) = any_actor.send_tagged_bytes(tag, bytes) { 258 | error!( 259 | ident = %Hex(&ident), 260 | %err, 261 | "failed to send binary local forwarding" 262 | ); 263 | } 264 | }) 265 | }); 266 | 267 | Continuation::LocalBinForward { 268 | peer: PEER.get(), 269 | tx, 270 | } 271 | } 272 | Some(public_key) => { 273 | let (tx, rx) = oneshot::channel::>(); 274 | 275 | tokio::spawn(PEER.scope(PEER.get(), async move { 276 | trace!( 277 | ident = %Hex(&ident), 278 | host = %PEER.get(), 279 | "scheduling deligated remote forwarding" 280 | ); 281 | 282 | let Ok(bytes) = rx.await else { 283 | return warn!("failed to receive binary remote forwarding"); 284 | }; 285 | 286 | let target_peer = LocalPeer::inst().get_or_connect_peer(public_key); 287 | 288 | if let Err(err) = target_peer.send_forward(ident, tag, bytes).await { 289 | warn!( 290 | ident = %Hex(&ident), 291 | host = %PEER.get(), 292 | %err, 293 | "failed to send binary remote forwarding" 294 | ); 295 | } 296 | })); 297 | 298 | Continuation::RemoteBinForward { 299 | peer: PEER.get(), 300 | tx, 301 | } 302 | } 303 | }, 304 | } 305 | } 306 | } 307 | -------------------------------------------------------------------------------- /theta/src/context.rs: -------------------------------------------------------------------------------- 1 | use std::sync::{Arc, LazyLock, Mutex}; 2 | 3 | use dashmap::DashMap; 4 | use rustc_hash::FxBuildHasher; 5 | use theta_flume::unbounded_with_id; 6 | use tokio::sync::Notify; 7 | use tracing::{error, trace}; 8 | use uuid::Uuid; 9 | 10 | use crate::{ 11 | actor::{Actor, ActorArgs, ActorId}, 12 | actor_instance::ActorConfig, 13 | actor_ref::{ActorHdl, ActorRef, AnyActorRef, WeakActorHdl, WeakActorRef}, 14 | base::{BindingError, Hex, Ident, parse_ident}, 15 | message::RawSignal, 16 | }; 17 | 18 | #[cfg(feature = "monitor")] 19 | use crate::monitor::HDLS; 20 | 21 | #[cfg(feature = "remote")] 22 | use { 23 | crate::remote::{base::ActorTypeId, peer::LocalPeer}, 24 | iroh::PublicKey, 25 | }; 26 | 27 | // todo Use concurrent hashmap 28 | /// Global registry mapping identifiers to actor references for named bindings. 29 | /// Replaced with DashMap for reduced contention on high lookup concurrency. 30 | pub(crate) static BINDINGS: LazyLock, FxBuildHasher>> = 31 | LazyLock::new(DashMap::default); 32 | 33 | /// Actor execution context providing communication and spawning capabilities. 34 | /// 35 | /// The `Context` is passed to actor methods and provides access to: 36 | /// - Self reference for sending messages to itself via `ctx.this.upgrade()` 37 | /// - Child actor management via `ctx.spawn()` 38 | /// - Actor metadata via `ctx.id()` 39 | /// - Lifecycle control via `ctx.terminate()` 40 | /// 41 | /// # Type Parameters 42 | /// 43 | /// * `A` - The actor type this context belongs to 44 | #[derive(Debug, Clone)] 45 | pub struct Context { 46 | /// Weak reference to this actor instance 47 | pub this: WeakActorRef, 48 | pub(crate) this_hdl: ActorHdl, // Self supervision reference 49 | pub(crate) child_hdls: Arc>>, // children of this actor 50 | } 51 | 52 | /// Root context for the actor system. 53 | /// 54 | /// `RootContext` serves as the entry point for the actor system and provides 55 | /// methods for: 56 | /// - Initializing the actor system (local or remote) 57 | /// - Spawning top-level actors 58 | /// - Binding actors to names for lookup 59 | /// - Looking up remote actors (with `remote` feature) 60 | /// 61 | /// # Examples 62 | /// 63 | /// ``` 64 | /// use theta::prelude::*; 65 | /// 66 | /// #[derive(Debug, Clone, ActorArgs)] 67 | /// struct MyActor { value: i32 } 68 | /// 69 | /// #[actor("12345678-1234-5678-9abc-123456789abc")] 70 | /// impl Actor for MyActor {} 71 | /// 72 | /// // Compile-only example 73 | /// fn example() { 74 | /// let ctx = RootContext::init_local(); 75 | /// let actor = ctx.spawn(MyActor { value: 0 }); 76 | /// ctx.bind("my_actor", actor); 77 | /// } 78 | /// ``` 79 | #[derive(Debug, Clone)] 80 | pub struct RootContext { 81 | pub(crate) this_hdl: ActorHdl, // Self reference 82 | pub(crate) child_hdls: Arc>>, // children of the global context 83 | } 84 | 85 | // Implementations 86 | 87 | impl Context { 88 | /// Get the unique identifier of this actor. 89 | /// 90 | /// # Return 91 | /// 92 | /// `ActorId` uniquely identifying this actor instance. 93 | pub fn id(&self) -> ActorId { 94 | self.this_hdl.id() 95 | } 96 | 97 | /// Spawn a new child actor with the given arguments. 98 | /// 99 | /// # Arguments 100 | /// 101 | /// * `args` - Initialization arguments for the new actor 102 | /// 103 | /// # Return 104 | /// 105 | /// `ActorRef` reference to the newly spawned actor. 106 | pub fn spawn(&self, args: Args) -> ActorRef { 107 | let (hdl, actor) = spawn_impl(&self.this_hdl, args); 108 | 109 | self.child_hdls.lock().unwrap().push(hdl.downgrade()); 110 | 111 | actor 112 | } 113 | 114 | /// Terminate this actor and all its children. 115 | // todo Make it support timeout 116 | pub async fn terminate(&self) { 117 | let k = Arc::new(Notify::new()); 118 | self.this_hdl 119 | .raw_send(RawSignal::Terminate(Some(k.clone()))) 120 | .unwrap(); 121 | 122 | k.notified().await; 123 | } 124 | } 125 | 126 | impl RootContext { 127 | /// Initialize the root context with remote capabilities. 128 | /// 129 | /// # Arguments 130 | /// 131 | /// * `endpoint` - The iroh network endpoint for remote communication 132 | /// 133 | /// # Return 134 | /// 135 | /// `RootContext` with remote communication enabled. 136 | #[cfg(feature = "remote")] 137 | pub fn init(endpoint: iroh::Endpoint) -> Self { 138 | LocalPeer::init(endpoint); 139 | 140 | Self::init_local() 141 | } 142 | 143 | /// Initialize a local-only root context. 144 | /// 145 | /// # Return 146 | /// 147 | /// `RootContext` for local actor system operations. 148 | pub fn init_local() -> Self { 149 | Self::default() 150 | } 151 | 152 | /// Get the public key of this node for remote communication. 153 | /// 154 | /// # Return 155 | /// 156 | /// `PublicKey` for this node's identity in remote operations. 157 | #[cfg(feature = "remote")] 158 | pub fn public_key(&self) -> PublicKey { 159 | LocalPeer::inst().public_key() 160 | } 161 | 162 | /// Spawn a new top-level actor. 163 | /// 164 | /// # Arguments 165 | /// 166 | /// * `args` - The initialization arguments for the new actor 167 | /// 168 | /// # Return 169 | /// 170 | /// `ActorRef` reference to the newly spawned actor. 171 | pub fn spawn(&self, args: Args) -> ActorRef { 172 | let (actor_hdl, actor) = spawn_impl(&self.this_hdl, args); 173 | 174 | self.child_hdls.lock().unwrap().push(actor_hdl.downgrade()); 175 | 176 | actor 177 | } 178 | 179 | /// Bind an actor to a global identifier for lookup. 180 | /// 181 | /// # Arguments 182 | /// 183 | /// * `ident` - A name to bind the actor to (max 16 bytes) 184 | /// * `actor` - The actor reference to bind 185 | /// 186 | /// # Panics 187 | /// * If the identifier length exceeds 16 bytes 188 | pub fn bind( 189 | &self, 190 | ident: impl AsRef, 191 | actor: ActorRef, 192 | ) -> Result<(), BindingError> { 193 | let ident = parse_ident(ident.as_ref())?; 194 | 195 | trace!(ident = %Hex(&ident), %actor, "binding"); 196 | Self::bind_impl(ident, actor); 197 | 198 | Ok(()) 199 | } 200 | 201 | /// Remove an actor binding from the global registry. 202 | /// 203 | /// # Arguments 204 | /// 205 | /// * `ident` - The identifier to remove from the registry 206 | /// 207 | /// # Return 208 | /// 209 | /// `Option>` - The removed binding if it existed. 210 | pub fn free(&self, ident: &[u8]) -> Option> { 211 | if ident.len() > 16 { 212 | return None; 213 | } 214 | 215 | let mut normalized: Ident = [0u8; 16]; 216 | normalized[..ident.len()].copy_from_slice(ident); 217 | 218 | Self::free_impl(&normalized) 219 | } 220 | 221 | /// Terminate the root context and all spawned actors. 222 | /// 223 | /// Initiates system-wide shutdown and waits for all actors to terminate. 224 | /// 225 | /// # Note 226 | /// 227 | /// TODO: Make it support timeout for graceful shutdown with fallback to forced termination. 228 | // todo Make it support timeoutping-pong 229 | pub async fn terminate(&self) { 230 | let k = Arc::new(Notify::new()); 231 | self.this_hdl 232 | .raw_send(RawSignal::Terminate(Some(k.clone()))) 233 | .unwrap(); 234 | 235 | k.notified().await; 236 | } 237 | 238 | #[allow(dead_code)] 239 | pub(crate) fn is_bound_impl(ident: &[u8; 16]) -> bool { 240 | let Some(actor) = BINDINGS.get(ident) else { 241 | return false; 242 | }; 243 | 244 | match actor.as_any() { 245 | None => false, 246 | Some(a) => a.is::>(), 247 | } 248 | } 249 | 250 | pub(crate) fn bind_impl(ident: Ident, actor: A) { 251 | let _ = BINDINGS.insert(ident, Arc::new(actor)); 252 | } 253 | 254 | // #[allow(dead_code)] 255 | // #[cfg(feature = "remote")] 256 | // pub(crate) fn lookup_any_local( 257 | // actor_ty_id: ActorTypeId, 258 | // ident: &[u8], 259 | // ) -> Result, LookupError> { 260 | // if ident.len() > 16 { 261 | // return Err(LookupError::InvalidIdent); 262 | // } 263 | 264 | // let mut normalized = [0u8; 16]; 265 | // normalized[..ident.len()].copy_from_slice(ident); 266 | 267 | // Self::lookup_any_local_impl(actor_ty_id, &normalized) 268 | // } 269 | 270 | #[cfg(feature = "remote")] 271 | pub(crate) fn lookup_any_local_impl( 272 | actor_ty_id: ActorTypeId, 273 | ident: &[u8; 16], 274 | ) -> Result, BindingError> { 275 | match Self::lookup_any_local_unchecked_impl(ident) { 276 | Err(err) => Err(err), 277 | Ok(actor) => match actor.ty_id() == actor_ty_id { 278 | false => Err(BindingError::TypeMismatch), 279 | true => Ok(actor), 280 | }, 281 | } 282 | } 283 | 284 | // #[allow(dead_code)] 285 | // #[cfg(feature = "remote")] 286 | // pub(crate) fn lookup_any_local_unchecked( 287 | // ident: &[u8], 288 | // ) -> Result, LookupError> { 289 | // if ident.len() > 16 { 290 | // return Err(LookupError::InvalidIdent); 291 | // } 292 | 293 | // let mut normalized = [0u8; 16]; 294 | // normalized[..ident.len()].copy_from_slice(ident); 295 | 296 | // Self::lookup_any_local_unchecked_impl(&normalized) 297 | // } 298 | 299 | #[cfg(feature = "remote")] 300 | pub(crate) fn lookup_any_local_unchecked_impl( 301 | ident: &[u8; 16], 302 | ) -> Result, BindingError> { 303 | let Some(actor) = BINDINGS.get(ident) else { 304 | return Err(BindingError::NotFound); 305 | }; 306 | 307 | Ok(actor.clone()) 308 | } 309 | 310 | pub(crate) fn free_impl(ident: &[u8; 16]) -> Option> { 311 | BINDINGS.remove(ident).map(|(_, v)| v) 312 | } 313 | } 314 | 315 | impl Default for RootContext { 316 | fn default() -> Self { 317 | let (sig_tx, sig_rx) = unbounded_with_id(Uuid::new_v4()); 318 | let this_hdl = ActorHdl(sig_tx); 319 | let child_hdls = Arc::new(Mutex::new(Vec::::new())); 320 | 321 | tokio::spawn({ 322 | let child_hdls = child_hdls.clone(); 323 | 324 | // Minimal supervision logic 325 | async move { 326 | while let Some(sig) = sig_rx.recv().await { 327 | match sig { 328 | RawSignal::Escalation(child_hdl, esc) => { 329 | error!(child = %child_hdl.id(), ?esc, "root received escalation"); 330 | child_hdl.raw_send(RawSignal::Terminate(None)).unwrap(); 331 | } 332 | RawSignal::ChildDropped => { 333 | let mut child_hdls = child_hdls.lock().unwrap(); 334 | child_hdls.retain(|hdl| match hdl.upgrade() { 335 | None => false, 336 | Some(hdl) => hdl.0.sender_count() > 0, 337 | }); 338 | } 339 | _ => unreachable!( 340 | "Escalation and ChildDropped are the only signals expected" 341 | ), 342 | } 343 | } 344 | } 345 | }); 346 | 347 | Self { 348 | this_hdl, 349 | child_hdls, 350 | } 351 | } 352 | } 353 | 354 | /// Create a new actor instance with auto-generated ID. 355 | pub(crate) fn spawn_impl( 356 | parent_hdl: &ActorHdl, 357 | args: Args, 358 | ) -> (ActorHdl, ActorRef) { 359 | spawn_with_id_impl(Uuid::new_v4(), parent_hdl, args) 360 | } 361 | 362 | /// Create a new actor instance with specified ID. 363 | pub(crate) fn spawn_with_id_impl( 364 | actor_id: ActorId, 365 | parent_hdl: &ActorHdl, 366 | args: Args, 367 | ) -> (ActorHdl, ActorRef) { 368 | let (msg_tx, msg_rx) = unbounded_with_id(actor_id); 369 | let (sig_tx, sig_rx) = unbounded_with_id(actor_id); 370 | 371 | let actor_hdl = ActorHdl(sig_tx); 372 | let actor = ActorRef(msg_tx); 373 | 374 | // Ignore chance of UUID v4 collision 375 | #[cfg(feature = "monitor")] 376 | let _ = HDLS.insert(actor_id, actor_hdl.clone()); 377 | 378 | tokio::spawn({ 379 | let actor = actor.downgrade(); 380 | let parent_hdl = parent_hdl.clone(); 381 | let actor_hdl = actor_hdl.clone(); 382 | 383 | async move { 384 | let config = ActorConfig::new(actor, parent_hdl, actor_hdl, sig_rx, msg_rx, args); 385 | config.exec().await; 386 | 387 | #[cfg(feature = "monitor")] 388 | let _ = HDLS.remove(&actor_id); 389 | } 390 | }); 391 | 392 | (actor_hdl, actor) 393 | } 394 | -------------------------------------------------------------------------------- /theta/src/persistence/persistent_actor.rs: -------------------------------------------------------------------------------- 1 | use std::{any::type_name, fmt::Debug, future::Future}; 2 | 3 | use serde::{Deserialize, Serialize}; 4 | use thiserror::Error; 5 | use tracing::{debug, warn}; 6 | 7 | use crate::{ 8 | actor::{Actor, ActorArgs, ActorId}, 9 | actor_ref::ActorRef, 10 | context::{Context, RootContext, spawn_with_id_impl}, 11 | }; 12 | 13 | /// Trait for storage backends that can persist actor state. 14 | /// 15 | /// Implementations provide read/write operations for actor snapshots 16 | /// to various storage systems (filesystem, S3, databases, etc.). 17 | pub trait PersistentStorage: Send + Sync { 18 | /// Try to read a snapshot for the given actor ID. 19 | fn try_read(&self, id: ActorId) -> impl Future, anyhow::Error>> + Send; 20 | 21 | /// Try to write a snapshot for the given actor ID. 22 | fn try_write( 23 | &self, 24 | id: ActorId, 25 | bytes: Vec, 26 | ) -> impl Future> + Send; 27 | } 28 | 29 | /// Trait for actors that support state persistence. 30 | /// 31 | /// Persistent actors can save snapshots of their state and be recovered 32 | /// from those snapshots. The snapshot type must implement all necessary 33 | /// traits for serialization and actor initialization. 34 | /// 35 | /// # Example 36 | /// 37 | /// ``` 38 | /// use theta::prelude::*; 39 | /// use serde::{Serialize, Deserialize}; 40 | /// 41 | /// #[derive(Debug, Clone, Serialize, Deserialize, ActorArgs)] 42 | /// struct Counter { 43 | /// value: i64, 44 | /// } 45 | /// 46 | /// #[derive(Debug, Clone, Serialize, Deserialize)] 47 | /// struct Increment(i64); 48 | /// 49 | /// // The `snapshot` flag automatically implements PersistentActor 50 | /// // with Snapshot = Counter, RuntimeArgs = (), ActorArgs = Counter 51 | /// #[actor("12345678-1234-5678-9abc-123456789abc", snapshot)] 52 | /// impl Actor for Counter { 53 | /// const _: () = { 54 | /// async |Increment(amount): Increment| { 55 | /// self.value += amount; 56 | /// }; 57 | /// }; 58 | /// } 59 | /// ``` 60 | pub trait PersistentActor: Actor { 61 | /// The snapshot type that captures this actor's persistable state. 62 | /// 63 | /// This type must: 64 | /// - Be serializable (`Serialize` + `Deserialize`) 65 | /// - Convert from the actor state (`From<&Self>`) 66 | /// - Be usable as actor initialization arguments (`ActorArgs`) 67 | type Snapshot: Debug 68 | + Clone 69 | + Send 70 | + Sync 71 | + Serialize 72 | + for<'a> Deserialize<'a> 73 | + for<'a> From<&'a Self>; 74 | 75 | /// Runtime arguments passed to `persistent_args` along with the snapshot. 76 | /// 77 | /// For actors using the `snapshot` macro attribute, this defaults to `()`. 78 | type RuntimeArgs: Debug + Send; 79 | 80 | /// Actor arguments type used to initialize the actor. 81 | /// 82 | /// For actors using the `snapshot` macro attribute without value, it is `Actor` itself. 83 | /// For actors using `snapshot` with a some type implementing `ActorArgs`, it is that type. 84 | type ActorArgs: ActorArgs; 85 | 86 | /// Combine a snapshot and runtime arguments to create actor arguments. 87 | /// 88 | /// For actors using the `snapshot` macro attribute, this simply returns the snapshot 89 | /// since `RuntimeArgs = ()` and `ActorArgs = Snapshot`. 90 | fn persistent_args( 91 | snapshot: Self::Snapshot, 92 | runtime_args: Self::RuntimeArgs, 93 | ) -> Self::ActorArgs; 94 | } 95 | 96 | /// Extension trait for spawning persistent actors. 97 | pub trait PersistentSpawnExt { 98 | /// Spawn an actor with persistence support. 99 | /// 100 | /// This method will: 101 | /// 1. Try to load an existing snapshot from storage 102 | /// 2. If found, use it to restore the actor state 103 | /// 3. If not found, initialize the actor with the provided arguments 104 | /// 105 | /// **Warning**: The caller must ensure no more than one actor is spawned 106 | /// with the same ID, as this can lead to undefined behavior. 107 | /// 108 | /// **Note**: This method does not automatically save snapshots. If you need 109 | /// automatic snapshots, implement the saving logic in `ActorArgs::initialize`. 110 | /// 111 | /// # Arguments 112 | /// 113 | /// * `storage` - The storage backend to use for persistence 114 | /// * `actor_id` - Unique ID for this actor instance (used as storage key) 115 | /// * `args` - Initialization arguments (used if no snapshot exists) 116 | fn spawn_persistent( 117 | &self, 118 | storage: &S, 119 | actor_id: ActorId, 120 | args: Args, 121 | ) -> impl Future, PersistenceError>> + Send 122 | where 123 | S: PersistentStorage, 124 | Args: ActorArgs, 125 | ::Actor: PersistentActor; 126 | 127 | /// - Since it coerce the given Id to actor, it is callers' responsibility to ensure no more than one actor is spawned with the same ID. 128 | /// - Failure to do so will not result error immediately, but latened undefined behavior. 129 | /// - It does not invoke [`PersistentContextExt::save_snapshot`] automatically, it should be specified in [`ActorArgs::initialize`] if needed. 130 | fn respawn( 131 | &self, 132 | storage: &S, 133 | actor_id: ActorId, 134 | runtime_args: B::RuntimeArgs, 135 | ) -> impl Future, PersistenceError>> + Send 136 | where 137 | S: PersistentStorage, 138 | B: Actor + PersistentActor; 139 | 140 | /// - Since it coerce the given Id to actor, it is callers' responsibility to ensure no more than one actor is spawned with the same ID. 141 | /// - Failure to do so will not result error immediately, but latened undefined behavior. 142 | /// - It does not invoke [`PersistentContextExt::save_snapshot`] automatically, it should be specified in [`ActorArgs::initialize`] if needed. 143 | fn respawn_or( 144 | &self, 145 | storage: &S, 146 | actor_id: ActorId, 147 | runtime_args: impl FnOnce() -> <::Actor as PersistentActor>::RuntimeArgs 148 | + Send, 149 | default_args: impl FnOnce() -> Args + Send, 150 | ) -> impl Future, PersistenceError>> + Send 151 | where 152 | S: PersistentStorage, 153 | Args: ActorArgs, 154 | ::Actor: PersistentActor; 155 | } 156 | 157 | /// Extension trait for manually saving actor snapshots to storage. 158 | pub trait PersistentContextExt 159 | where 160 | A: Actor + PersistentActor, 161 | { 162 | /// Save a snapshot of the actor's current state to storage. 163 | /// 164 | /// This method should only be called for actors with known IDs to avoid 165 | /// accumulating garbage data in storage. 166 | fn save_snapshot( 167 | &self, 168 | storage: &S, 169 | state: &A, 170 | ) -> impl Future> + Send; 171 | } 172 | 173 | /// Errors that can occur during persistence operations. 174 | #[derive(Debug, Error)] 175 | pub enum PersistenceError { 176 | #[error(transparent)] 177 | IoError(#[from] anyhow::Error), // Use anyhow because std::io::Error is !UnwindSafe 178 | #[error(transparent)] 179 | SerializeError(postcard::Error), 180 | #[error(transparent)] 181 | DeserializeError(postcard::Error), 182 | } 183 | 184 | // Implementation 185 | 186 | impl PersistentSpawnExt for Context 187 | where 188 | A: Actor, 189 | { 190 | async fn spawn_persistent( 191 | &self, 192 | _storage: &S, 193 | actor_id: ActorId, 194 | args: Args, 195 | ) -> Result, PersistenceError> 196 | where 197 | S: PersistentStorage, 198 | Args: ActorArgs, 199 | ::Actor: PersistentActor, 200 | { 201 | let (hdl, actor) = spawn_with_id_impl(actor_id, &self.this_hdl, args); 202 | 203 | self.child_hdls.lock().unwrap().push(hdl.downgrade()); 204 | 205 | Ok(actor) 206 | } 207 | 208 | async fn respawn( 209 | &self, 210 | storage: &S, 211 | actor_id: ActorId, 212 | runtime_args: B::RuntimeArgs, 213 | ) -> Result, PersistenceError> 214 | where 215 | S: PersistentStorage, 216 | B: Actor + PersistentActor, 217 | { 218 | let bytes = storage.try_read(actor_id).await?; 219 | 220 | let snapshot: B::Snapshot = 221 | postcard::from_bytes(&bytes).map_err(PersistenceError::DeserializeError)?; 222 | 223 | let args = B::persistent_args(snapshot, runtime_args); 224 | 225 | let actor = self.spawn_persistent(storage, actor_id, args).await?; 226 | 227 | Ok(actor) 228 | } 229 | 230 | async fn respawn_or( 231 | &self, 232 | storage: &S, 233 | actor_id: ActorId, 234 | runtime_args: impl FnOnce() -> <::Actor as PersistentActor>::RuntimeArgs 235 | + Send, 236 | default_args: impl FnOnce() -> Args + Send, 237 | ) -> Result, PersistenceError> 238 | where 239 | S: PersistentStorage, 240 | Args: ActorArgs, 241 | ::Actor: PersistentActor, 242 | { 243 | let bytes = match storage.try_read(actor_id).await { 244 | Err(err) => { 245 | debug!( 246 | actor = format_args!("{}({actor_id})", 247 | type_name::<::Actor>()), 248 | %err, 249 | "failed to read snapshot" 250 | ); 251 | return self 252 | .spawn_persistent(storage, actor_id, default_args()) 253 | .await; 254 | } 255 | Ok(bytes) => bytes, 256 | }; 257 | 258 | let snapshot: ::Snapshot = 259 | match postcard::from_bytes(&bytes) { 260 | Err(err) => { 261 | warn!( 262 | actor = format_args!("{}({actor_id})", 263 | type_name::<::Actor>()), 264 | %err, 265 | "failed to deserialize snapshot" 266 | ); 267 | return self 268 | .spawn_persistent(storage, actor_id, default_args()) 269 | .await; 270 | } 271 | Ok(snapshot) => snapshot, 272 | }; 273 | 274 | let args = Args::Actor::persistent_args(snapshot, runtime_args()); 275 | 276 | self.spawn_persistent(storage, actor_id, args).await 277 | } 278 | } 279 | 280 | impl PersistentSpawnExt for RootContext { 281 | async fn spawn_persistent( 282 | &self, 283 | _storage: &S, 284 | actor_id: ActorId, 285 | args: Args, 286 | ) -> Result, PersistenceError> 287 | where 288 | S: PersistentStorage, 289 | Args: ActorArgs, 290 | ::Actor: PersistentActor, 291 | { 292 | let (hdl, actor) = spawn_with_id_impl(actor_id, &self.this_hdl, args); 293 | 294 | self.child_hdls.lock().unwrap().push(hdl.downgrade()); 295 | 296 | Ok(actor) 297 | } 298 | 299 | async fn respawn( 300 | &self, 301 | storage: &S, 302 | actor_id: ActorId, 303 | runtime_args: B::RuntimeArgs, 304 | ) -> Result, PersistenceError> 305 | where 306 | S: PersistentStorage, 307 | B: Actor + PersistentActor, 308 | { 309 | let bytes = storage.try_read(actor_id).await?; 310 | 311 | let snapshot: B::Snapshot = 312 | postcard::from_bytes(&bytes).map_err(PersistenceError::DeserializeError)?; 313 | 314 | let args = B::persistent_args(snapshot, runtime_args); 315 | 316 | let actor = self.spawn_persistent(storage, actor_id, args).await?; 317 | 318 | Ok(actor) 319 | } 320 | 321 | async fn respawn_or( 322 | &self, 323 | storage: &S, 324 | actor_id: ActorId, 325 | runtime_args: impl FnOnce() -> <::Actor as PersistentActor>::RuntimeArgs, 326 | default_args: impl FnOnce() -> Args, 327 | ) -> Result, PersistenceError> 328 | where 329 | S: PersistentStorage, 330 | Args: ActorArgs, 331 | ::Actor: PersistentActor, 332 | { 333 | let bytes = match storage.try_read(actor_id).await { 334 | Err(err) => { 335 | debug!( 336 | actor = format_args!("{}({actor_id})", 337 | type_name::<::Actor>()), 338 | %err, 339 | "failed to read snapshot" 340 | ); 341 | return self 342 | .spawn_persistent(storage, actor_id, default_args()) 343 | .await; 344 | } 345 | Ok(bytes) => bytes, 346 | }; 347 | 348 | let snapshot: ::Snapshot = 349 | match postcard::from_bytes(&bytes) { 350 | Err(err) => { 351 | warn!( 352 | actor = format_args!("{}({actor_id})", 353 | type_name::<::Actor>()), 354 | %err, 355 | "failed to deserialize snapshot" 356 | ); 357 | return self 358 | .spawn_persistent(storage, actor_id, default_args()) 359 | .await; 360 | } 361 | Ok(snapshot) => snapshot, 362 | }; 363 | 364 | let args = Args::Actor::persistent_args(snapshot, runtime_args()); 365 | 366 | self.spawn_persistent(storage, actor_id, args).await 367 | } 368 | } 369 | 370 | impl PersistentContextExt for Context 371 | where 372 | A: Actor + PersistentActor, 373 | { 374 | fn save_snapshot( 375 | &self, 376 | storage: &S, 377 | state: &A, 378 | ) -> impl Future> + Send { 379 | let snapshot = A::Snapshot::from(state); 380 | let actor_id = self.id(); 381 | 382 | async move { 383 | storage 384 | .try_write(actor_id, postcard::to_stdvec(&snapshot)?) 385 | .await?; 386 | 387 | Ok(()) 388 | } 389 | } 390 | } 391 | -------------------------------------------------------------------------------- /theta/src/actor.rs: -------------------------------------------------------------------------------- 1 | use std::{fmt::Debug, future::Future, panic::UnwindSafe}; 2 | 3 | use crate::{ 4 | context::Context, 5 | message::{Continuation, Escalation, Signal}, 6 | }; 7 | 8 | #[cfg(feature = "remote")] 9 | use { 10 | crate::remote::{base::ActorTypeId, serde::FromTaggedBytes}, 11 | serde::{Deserialize, Serialize}, 12 | }; 13 | 14 | /// Unique identifier for each actor instance. 15 | pub type ActorId = uuid::Uuid; 16 | 17 | /// Trait for actor initialization arguments. 18 | /// 19 | /// This trait defines how actors are initialized from their arguments. 20 | /// In case of actor it self is args and `Clone`, could be derived using `#[derive(ActorArgs)]`. 21 | /// 22 | /// # Example 23 | /// 24 | /// #[cfg(feature = "full")] 25 | /// ``` 26 | /// use theta::prelude::*; 27 | /// use serde::{Serialize, Deserialize}; 28 | /// 29 | /// #[derive(Debug, Clone, ActorArgs)] 30 | /// struct MyActor { 31 | /// name: String, 32 | /// value: i32, 33 | /// } 34 | /// 35 | /// #[derive(Debug, Clone, Serialize, Deserialize)] 36 | /// struct Increment(i32); 37 | /// 38 | /// #[actor("12345678-1234-5678-9abc-123456789abc")] 39 | /// impl Actor for MyActor { 40 | /// const _: () = { 41 | /// async |Increment(amount): Increment| { 42 | /// self.value += amount; 43 | /// }; 44 | /// }; 45 | /// } 46 | /// ``` 47 | pub trait ActorArgs: Clone + Send + UnwindSafe + 'static { 48 | type Actor: Actor; 49 | 50 | /// Initialize an actor from the given arguments. 51 | /// 52 | /// This method is called when spawning a new actor and should return 53 | /// the initialized actor state. The method is panic-safe - any panics 54 | /// will be caught and escalated to the supervisor. 55 | /// 56 | /// # Arguments 57 | /// 58 | /// * `ctx` - The actor's context for communication and spawning children 59 | /// * `args` - The initialization arguments 60 | fn initialize( 61 | ctx: Context, 62 | args: &Self, 63 | ) -> impl Future + Send + UnwindSafe; 64 | } 65 | 66 | /// Core trait that defines actor behavior. 67 | /// 68 | /// This trait must be implemented by all actor types. It defines the message type, 69 | /// state updateing type, and behavior methods for message processing and supervision. 70 | /// 71 | /// # Usage with `#[actor]` Macro 72 | /// 73 | /// Actors are typically implemented using the `#[actor]` attribute macro which 74 | /// generates the necessary boilerplate. The macro provides significant conveniences 75 | /// and automatic implementations. 76 | /// 77 | /// ## Behavior Specification Syntax 78 | /// 79 | /// The `#[actor]` macro allows you to specify actor behavior using a special syntax 80 | /// within the `const _: () = {}` block. Inside this block: 81 | /// 82 | /// - `&mut self` - Reference to the actor instance (automatically provided) 83 | /// - `ctx` - The actor's context for communication and spawning (automatically provided) 84 | /// - Message handlers use async closure syntax with pattern destructuring 85 | /// - Optional return types for ask/forward patterns 86 | /// 87 | /// ### Message Handler Patterns 88 | /// 89 | /// ``` 90 | /// # use theta::prelude::*; 91 | /// # use serde::{Serialize, Deserialize}; 92 | /// # #[derive(Debug, Clone, ActorArgs)] 93 | /// # struct MyActor; 94 | /// # #[derive(Debug, Clone, Serialize, Deserialize)] 95 | /// # struct DataMessage { data: i32 } 96 | /// # #[derive(Debug, Clone, Serialize, Deserialize)] 97 | /// # struct FieldMessage { field: i32 } 98 | /// # #[derive(Debug, Clone, Serialize, Deserialize)] 99 | /// # struct Response; 100 | /// # #[derive(Debug, Clone, Serialize, Deserialize)] 101 | /// # struct SimpleMessage; 102 | /// # #[actor("12345678-1234-5678-9abc-123456789abc")] 103 | /// # impl Actor for MyActor { 104 | /// # const _: () = { 105 | /// async |data: DataMessage| { /* ... */ }; // Fire-and-forget (tell) 106 | /// async |FieldMessage { field }: FieldMessage| -> Response { Response }; // Request-response (ask) 107 | /// async |_: SimpleMessage| { /* ... */ }; // Ignore message data 108 | /// # }; 109 | /// # } 110 | /// ``` 111 | /// 112 | /// ## Basic Example 113 | /// 114 | /// ``` 115 | /// use theta::prelude::*; 116 | /// use serde::{Serialize, Deserialize}; 117 | /// 118 | /// #[derive(Debug, Clone, ActorArgs)] 119 | /// struct Counter { value: i64 } 120 | /// 121 | /// #[derive(Debug, Clone, Serialize, Deserialize)] 122 | /// struct Increment(i64); 123 | /// 124 | /// #[derive(Debug, Clone, Serialize, Deserialize)] 125 | /// struct GetValue; 126 | /// 127 | /// #[actor("12345678-1234-5678-9abc-123456789abc")] 128 | /// impl Actor for Counter { 129 | /// const _: () = { 130 | /// // Basic message handler 131 | /// async |Increment(amount): Increment| { 132 | /// self.value += amount; 133 | /// }; 134 | /// 135 | /// // Return type for ask pattern 136 | /// async |GetValue: GetValue| -> i64 { 137 | /// self.value 138 | /// }; 139 | /// }; 140 | /// } 141 | /// ``` 142 | /// 143 | /// ## Default Implementations Provided by Macro 144 | /// 145 | /// The `#[actor]` macro automatically provides: 146 | /// - `type View = Nil;` (unless you specify a custom type) 147 | /// - Empty message handler block if no handlers are specified 148 | /// - Message enum generation and dispatch logic 149 | /// - Remote communication support when the `remote` feature is enabled 150 | /// - **Auto-generated `hash_code`**: When you define a manual `type View` without providing 151 | /// a manual `hash_code` implementation, the macro automatically generates one using 152 | /// `FxHasher`.` 153 | /// 154 | /// ## Advanced Usage 155 | /// 156 | /// You can customize state updateing and use context for child actor management: 157 | /// 158 | /// ``` 159 | /// use theta::prelude::*; 160 | /// use serde::{Serialize, Deserialize}; 161 | /// 162 | /// #[derive(Debug, Clone, ActorArgs)] 163 | /// struct Supervisor { 164 | /// worker_count: u32, 165 | /// } 166 | /// 167 | /// #[derive(Debug, Clone, Serialize, Deserialize)] 168 | /// struct SpawnWorker; 169 | /// 170 | /// #[derive(Debug, Clone, Serialize, Deserialize)] 171 | /// struct WorkerStats { 172 | /// worker_count: u32, 173 | /// } 174 | /// 175 | /// impl From<&Supervisor> for WorkerStats { 176 | /// fn from(supervisor: &Supervisor) -> Self { 177 | /// WorkerStats { 178 | /// worker_count: supervisor.worker_count, 179 | /// } 180 | /// } 181 | /// } 182 | /// 183 | /// #[actor("87654321-4321-8765-dcba-987654321fed")] 184 | /// impl Actor for Supervisor { 185 | /// type View = WorkerStats; 186 | /// 187 | /// const _: () = { 188 | /// async |SpawnWorker: SpawnWorker| { 189 | /// self.worker_count += 1; 190 | /// }; 191 | /// }; 192 | /// } 193 | /// ``` 194 | /// 195 | /// ## What's Automatically Available in Message Handlers 196 | /// 197 | /// Within each message handler closure, you have automatic access to: 198 | /// - `&mut self` - Mutable reference to the actor instance for state modification 199 | /// - `ctx: Context` - Actor context providing: 200 | /// - **Self-messaging**: `ctx.this.upgrade()` to get a reference to send messages to itself 201 | /// - **Child spawning**: `ctx.spawn()` to create child actors 202 | /// - **Actor metadata**: `ctx.id()` to get the actor's unique identifier 203 | /// - **Lifecycle control**: `ctx.terminate()` to stop the actor 204 | /// 205 | /// ### Key Context Usage Patterns 206 | /// 207 | /// ``` 208 | /// # use theta::prelude::*; 209 | /// # use serde::{Serialize, Deserialize}; 210 | /// # #[derive(Debug, Clone, ActorArgs)] 211 | /// # struct MyActor; 212 | /// # #[derive(Debug, Clone, Serialize, Deserialize)] 213 | /// # struct SomeMessage; 214 | /// # #[actor("12345678-1234-5678-9abc-123456789abc")] 215 | /// # impl Actor for MyActor { 216 | /// # const _: () = { 217 | /// # async |SomeMessage: SomeMessage| { 218 | /// // Send message to self (most common pattern) 219 | /// if let Some(self_ref) = ctx.this.upgrade() { 220 | /// let _ = self_ref.tell(SomeMessage); 221 | /// } 222 | /// 223 | /// // Get actor ID for logging/debugging 224 | /// println!("Actor {} processing message", ctx.id()); 225 | /// # }; 226 | /// # }; 227 | /// # } 228 | /// ``` 229 | /// 230 | /// These are provided transparently by the macro, so you can use them freely 231 | /// without explicit parameter declarations. 232 | pub trait Actor: Sized + Debug + Send + UnwindSafe + 'static { 233 | /// The message type this actor can receive. 234 | /// 235 | /// For local-only actors, this can be any `Send` type. 236 | /// For remote-capable actors (with `remote` feature), messages must also 237 | /// implement `Serialize`, `Deserialize`, and `FromTaggedBytes`. 238 | #[cfg(not(feature = "remote"))] 239 | type Msg: Debug + Send; 240 | #[cfg(feature = "remote")] 241 | type Msg: Debug + Send + Serialize + for<'de> Deserialize<'de> + FromTaggedBytes; 242 | 243 | /// Type used for updateing actor state to monitors. 244 | /// 245 | /// This type represents a snapshot of the actor's state that can be 246 | /// sent to monitors. It must implement `From<&Self>` to convert from 247 | /// the actor's current state. 248 | #[cfg(not(feature = "remote"))] 249 | type View: Debug + Send + UnwindSafe + Clone + for<'a> From<&'a Self> + 'static; 250 | 251 | #[cfg(feature = "remote")] 252 | type View: Debug 253 | + Send 254 | + UnwindSafe 255 | + Clone 256 | + for<'a> From<&'a Self> 257 | + Serialize 258 | + for<'de> Deserialize<'de>; 259 | 260 | /// Process incoming messages. 261 | /// 262 | /// This method is called for each message received by the actor. 263 | /// It is panic-safe - panics will be caught and escalated to the supervisor. 264 | /// 265 | /// **Note:** This method is typically generated by the `#[actor]` macro 266 | /// and should not be implemented manually. 267 | /// 268 | /// # Arguments 269 | /// 270 | /// * `ctx` - The actor's context for communication 271 | /// * `msg` - The incoming message to process 272 | /// * `k` - Continuation for handling responses 273 | #[allow(unused_variables)] 274 | fn process_msg( 275 | &mut self, 276 | ctx: Context, 277 | msg: Self::Msg, 278 | k: Continuation, 279 | ) -> impl Future + Send; 280 | 281 | /// Handle escalations from child actors. 282 | /// 283 | /// This method is called when a child actor fails and needs supervision. 284 | /// The default implementation restarts the child actor. 285 | /// 286 | /// # Arguments 287 | /// 288 | /// * `escalation` - Information about the child actor failure 289 | /// 290 | /// # Return 291 | /// 292 | /// A tuple of signals: (signal_to_child, signal_to_parent) 293 | #[allow(unused_variables)] 294 | fn supervise( 295 | &mut self, 296 | escalation: Escalation, 297 | ) -> impl Future)> + Send { 298 | __default_supervise(self, escalation) 299 | } 300 | 301 | /// Called when the actor is restarted. 302 | /// 303 | /// This lifecycle hook is invoked before re-initialization during a restart. 304 | /// The default implementation does nothing. 305 | /// 306 | /// **Note:** This method is panic-safe, but panics will be logged rather than escalated. 307 | /// State corruption is possible if this method panics. 308 | #[allow(unused_variables)] 309 | fn on_restart(&mut self) -> impl Future + Send { 310 | __default_on_restart(self) 311 | } 312 | 313 | /// Called when the actor is terminating or being dropped. 314 | /// 315 | /// This lifecycle hook allows for cleanup operations before the actor shuts down. 316 | /// The default implementation does nothing. 317 | /// 318 | /// **Note:** This method is panic-safe, but panics will be logged rather than escalated. 319 | /// Since the message loop has stopped, any messages sent to `self` will be lost. 320 | /// 321 | /// # Arguments 322 | /// 323 | /// * `exit_code` - The reason for termination 324 | #[allow(unused_variables)] 325 | fn on_exit(&mut self, exit_code: ExitCode) -> impl Future + Send { 326 | __default_on_exit(self, exit_code) 327 | } 328 | 329 | /// Generate a hash code for this actor instance. 330 | /// 331 | /// This method is used to determine whether the actor sends state update to monitors. 332 | /// The default implementation returns a constant value. 333 | /// 334 | /// ## Automatic Generation 335 | /// 336 | /// When using the `#[actor]` macro, if you: 337 | /// 1. Define a custom `type View` 338 | /// 2. Don't provide a custom `hash_code` implementation 339 | /// 340 | /// Then the macro will automatically generate a `hash_code` implementation 341 | /// that uses `FxHasher` assuming `self` is `Hash`. 342 | /// 343 | /// ```ignore 344 | /// // This will auto-generate hash_code using FxHasher 345 | /// #[actor("uuid")] 346 | /// impl Actor for MyActor { 347 | /// type View = MyView; // Custom View type that implements Hash 348 | /// // No hash_code implementation provided 349 | /// } 350 | /// ``` 351 | /// 352 | /// The auto-generated implementation is equivalent to: 353 | /// ```ignore 354 | /// fn hash_code(&self) -> u64 { 355 | /// let mut hasher = FxHasher::default(); 356 | /// Hash::hash(&self.state_update(), &mut hasher); 357 | /// hasher.finish() 358 | /// } 359 | /// ``` 360 | #[allow(unused_variables)] 361 | fn hash_code(&self) -> u64 { 362 | 0 // no-op by default 363 | } 364 | 365 | /// Generate a state view for monitoring. 366 | /// 367 | /// This method creates a snapshot of the actor's current state for monitors. 368 | /// The default implementation uses the `From<&Self>` conversion. 369 | #[allow(unused_variables)] 370 | fn state_view(&self) -> Self::View { 371 | self.into() // no-op by default 372 | } 373 | 374 | /// Internal implementation ID for remote actors. 375 | /// 376 | /// **Note:** This should not be implemented manually - it's generated by the `#[actor]` macro. 377 | #[cfg(feature = "remote")] 378 | const IMPL_ID: ActorTypeId; 379 | 380 | // #[cfg(feature = "remote")] 381 | } 382 | 383 | /// Reason for actor termination. 384 | #[derive(Debug, Clone)] 385 | pub enum ExitCode { 386 | /// Actor was dropped normally 387 | Dropped, 388 | /// Actor was explicitly terminated 389 | Terminated, 390 | } 391 | 392 | // Delegated default implementation in order to decouple it from macro expansion 393 | 394 | /// Default supervision strategy that terminates failing actors. 395 | /// 396 | /// # Arguments 397 | /// 398 | /// * `_actor` - The actor being supervised (unused in default implementation) 399 | /// * `_escalation` - The escalation information (unused in default implementation) 400 | /// 401 | /// # Return 402 | /// 403 | /// `(Signal, Option)` - Signal for the failing actor and optional parent signal 404 | pub async fn __default_supervise( 405 | _actor: &mut A, 406 | _escalation: Escalation, 407 | ) -> (Signal, Option) { 408 | (Signal::Terminate, None) 409 | } 410 | 411 | /// Default restart handler that performs no additional actions. 412 | /// 413 | /// # Arguments 414 | /// 415 | /// * `_actor` - The actor being restarted (unused in default implementation) 416 | pub async fn __default_on_restart(_actor: &mut A) {} 417 | 418 | /// Default exit handler that performs no additional actions. 419 | /// 420 | /// # Arguments 421 | /// 422 | /// * `_actor` - The actor exiting (unused in default implementation) 423 | /// * `_exit_code` - The exit code (unused in default implementation) 424 | pub async fn __default_on_exit(_actor: &mut A, _exit_code: ExitCode) {} 425 | -------------------------------------------------------------------------------- /theta/src/actor_instance.rs: -------------------------------------------------------------------------------- 1 | use std::{ 2 | any::type_name, 3 | fmt::Display, 4 | panic::AssertUnwindSafe, 5 | sync::{Arc, Mutex}, 6 | }; 7 | 8 | use futures::{FutureExt, future::join_all}; 9 | use tokio::{select, sync::Notify}; 10 | use tracing::error; 11 | 12 | use crate::{ 13 | actor::{Actor, ActorArgs, ExitCode}, 14 | actor_ref::{ActorHdl, WeakActorHdl, WeakActorRef}, 15 | base::panic_msg, 16 | context::Context, 17 | message::{Continuation, Escalation, InternalSignal, MsgRx, RawSignal, SigRx}, 18 | }; 19 | 20 | const DROP_HDL_COUNT: usize = if cfg!(feature = "monitor") { 2 } else { 1 }; 21 | 22 | #[cfg(feature = "monitor")] 23 | use crate::monitor::{AnyUpdateTx, Monitor, Update, UpdateTx}; 24 | 25 | /// Configuration and runtime resources for an actor instance. 26 | pub(crate) struct ActorConfig> { 27 | pub(crate) this: WeakActorRef, // Self reference 28 | 29 | pub(crate) parent_hdl: ActorHdl, // Parent handle 30 | pub(crate) this_hdl: ActorHdl, // Self handle 31 | pub(crate) child_hdls: Arc>>, // Children of this actor 32 | 33 | pub(crate) sig_rx: SigRx, 34 | pub(crate) msg_rx: MsgRx, 35 | 36 | #[cfg(feature = "monitor")] 37 | pub(crate) monitor: Monitor, // Monitor for this actor 38 | 39 | pub(crate) args: Args, // Arguments for actor initialization 40 | pub(crate) mb_restart_k: Option>, // Optional continuation for restart signal 41 | } 42 | 43 | /// Active actor instance with its execution continuation. 44 | pub(crate) struct ActorInst> { 45 | k: Cont, 46 | state: ActorState, 47 | } 48 | 49 | /// Actor state container with configuration and hash. 50 | pub(crate) struct ActorState> { 51 | state: A, 52 | #[cfg(feature = "monitor")] 53 | hash: u64, 54 | config: ActorConfig, 55 | } 56 | 57 | /// Actor lifecycle states for runtime management. 58 | pub(crate) enum Lifecycle> { 59 | Running(ActorInst), 60 | Restarting(ActorConfig), 61 | Exit, 62 | } 63 | 64 | /// Execution continuation states for actor processing. 65 | pub(crate) enum Cont { 66 | Process, 67 | 68 | Pause(Option>), 69 | WaitSignal, 70 | Resume(Option>), 71 | 72 | Supervise(ActorHdl, Escalation), 73 | CleanupChildren, 74 | 75 | Panic(Escalation), 76 | Restart(Option>), 77 | 78 | Drop, 79 | Terminate(Option>), 80 | } 81 | 82 | // Implementations 83 | 84 | impl ActorConfig 85 | where 86 | A: Actor, 87 | Args: ActorArgs, 88 | { 89 | pub(crate) fn new( 90 | this: WeakActorRef, 91 | parent_hdl: ActorHdl, 92 | this_hdl: ActorHdl, 93 | sig_rx: SigRx, 94 | msg_rx: MsgRx, 95 | args: Args, 96 | ) -> Self { 97 | let child_hdls = Arc::new(Mutex::new(Vec::new())); 98 | let mb_restart_k = None; 99 | #[cfg(feature = "monitor")] 100 | let monitor = Monitor::default(); 101 | 102 | Self { 103 | this, 104 | parent_hdl, 105 | this_hdl, 106 | child_hdls, 107 | sig_rx, 108 | msg_rx, 109 | #[cfg(feature = "monitor")] 110 | monitor, 111 | args, 112 | mb_restart_k, 113 | } 114 | } 115 | 116 | pub(crate) async fn exec(self) { 117 | let mut mb_config = Some(self); 118 | 119 | while let Some(config) = mb_config { 120 | mb_config = config.exec_impl().await; 121 | } 122 | } 123 | 124 | async fn exec_impl(self) -> Option { 125 | let mb_inst = self.init_instance().await; 126 | 127 | let inst = match mb_inst { 128 | Err((config, e)) => { 129 | config 130 | .parent_hdl 131 | .escalate(config.this_hdl.clone(), e) 132 | .expect("Escalation should not fail"); 133 | 134 | return config.wait_signal().await; 135 | } 136 | Ok(inst) => inst, 137 | }; 138 | 139 | let mut lifecycle = Lifecycle::Running(inst); 140 | 141 | loop { 142 | match lifecycle { 143 | Lifecycle::Running(inst) => { 144 | lifecycle = inst.run().await; 145 | } 146 | Lifecycle::Restarting(config) => return Some(config), 147 | Lifecycle::Exit => return None, 148 | } 149 | } 150 | } 151 | 152 | async fn init_instance(self) -> Result, (Self, Escalation)> { 153 | Ok(ActorInst { 154 | k: Cont::Process, 155 | state: ActorState::init(self).await?, 156 | }) 157 | } 158 | 159 | async fn wait_signal(mut self) -> Option { 160 | let sig = self.sig_rx.recv().await.unwrap(); 161 | 162 | match sig { 163 | RawSignal::Restart(k) => { 164 | self.mb_restart_k = k; 165 | Some(self) 166 | } 167 | _ => None, 168 | } 169 | } 170 | 171 | fn ctx_cfg(&mut self) -> (Context, &Args) { 172 | ( 173 | Context { 174 | this: self.this.clone(), 175 | child_hdls: self.child_hdls.clone(), 176 | this_hdl: self.this_hdl.clone(), 177 | }, 178 | &self.args, 179 | ) 180 | } 181 | 182 | fn ctx(&mut self) -> Context { 183 | Context { 184 | this: self.this.clone(), 185 | child_hdls: self.child_hdls.clone(), 186 | this_hdl: self.this_hdl.clone(), 187 | } 188 | } 189 | } 190 | 191 | impl ActorInst 192 | where 193 | A: Actor, 194 | Args: ActorArgs, 195 | { 196 | async fn run(mut self) -> Lifecycle { 197 | loop { 198 | #[cfg(feature = "monitor")] 199 | self.state 200 | .config 201 | .monitor 202 | .update(Update::Status((&self.k).into())); 203 | 204 | self.k = match self.k { 205 | Cont::Process => self.state.process().await, 206 | 207 | Cont::Pause(k) => self.state.pause(k).await, 208 | Cont::WaitSignal => self.state.wait_signal().await, 209 | Cont::Resume(k) => self.state.resume(k).await, 210 | 211 | Cont::Supervise(c, e) => self.state.supervise(c, e).await, 212 | Cont::CleanupChildren => self.state.cleanup_children().await, 213 | 214 | Cont::Panic(e) => self.state.escalate(e).await, 215 | Cont::Restart(k) => return self.state.restart(k).await, 216 | 217 | Cont::Drop => return self.state.drop().await, 218 | Cont::Terminate(k) => return self.state.terminate(k).await, 219 | }; 220 | } 221 | } 222 | } 223 | 224 | impl ActorState 225 | where 226 | A: Actor, 227 | Args: ActorArgs, 228 | { 229 | async fn init( 230 | mut config: ActorConfig, 231 | ) -> Result, Escalation)> { 232 | let (ctx, cfg) = config.ctx_cfg(); 233 | 234 | let init_res = Args::initialize(ctx, cfg).catch_unwind().await; 235 | 236 | if let Some(k) = config.mb_restart_k.take() { 237 | k.notify_one() 238 | } 239 | 240 | let state = match init_res { 241 | Err(e) => { 242 | config.child_hdls.clear_poison(); 243 | return Err((config, Escalation::Initialize(panic_msg(e)))); 244 | } 245 | Ok(state) => state, 246 | }; 247 | 248 | #[cfg(feature = "monitor")] 249 | let hash_code = state.hash_code(); 250 | 251 | Ok(ActorState { 252 | state, 253 | #[cfg(feature = "monitor")] 254 | hash: hash_code, 255 | config, 256 | }) 257 | } 258 | 259 | async fn process(&mut self) -> Cont { 260 | loop { 261 | select! { 262 | biased; 263 | mb_sig = self.config.sig_rx.recv() => match self.process_sig(mb_sig.unwrap()) { 264 | None => continue, 265 | Some(k) => return k, 266 | }, 267 | mb_msg_k = self.config.msg_rx.recv() => match mb_msg_k { 268 | None => { 269 | if self.config.sig_rx.sender_count() <= DROP_HDL_COUNT { 270 | return Cont::Drop; 271 | } 272 | return Cont::WaitSignal; 273 | }, 274 | Some(msg_k) => if let Some(k) = self.process_msg(msg_k).await { 275 | return k; 276 | }, 277 | }, 278 | } 279 | } 280 | } 281 | 282 | #[cfg(feature = "monitor")] 283 | fn add_monitor(&mut self, any_tx: AnyUpdateTx) { 284 | let Ok(tx) = any_tx.downcast::>() else { 285 | return error!(actor = %self, "received invalid monitor"); 286 | }; 287 | 288 | if let Err(err) = tx.send(Update::State(self.state.state_view())) { 289 | return error!(actor = %self, %err, "failed to send initial state update"); 290 | } 291 | 292 | self.config.monitor.add_monitor(*tx); 293 | } 294 | 295 | async fn pause(&mut self, k: Option>) -> Cont { 296 | self.signal_children(InternalSignal::Pause, k).await; 297 | 298 | Cont::WaitSignal 299 | } 300 | 301 | async fn wait_signal(&mut self) -> Cont { 302 | loop { 303 | let sig = self.config.sig_rx.recv().await.unwrap(); 304 | // Monitor does not count in this context 305 | match self.process_sig(sig) { 306 | None => continue, 307 | Some(k) => return k, 308 | } 309 | } 310 | } 311 | 312 | async fn resume(&mut self, k: Option>) -> Cont { 313 | self.signal_children(InternalSignal::Resume, k).await; 314 | 315 | Cont::Process 316 | } 317 | 318 | async fn supervise(&mut self, child_hdl: ActorHdl, escalation: Escalation) -> Cont { 319 | let res = AssertUnwindSafe(self.state.supervise(escalation)) 320 | .catch_unwind() 321 | .await; 322 | 323 | match res { 324 | Err(e) => { 325 | self.config.child_hdls.clear_poison(); 326 | 327 | return Cont::Panic(Escalation::Supervise(panic_msg(e))); 328 | } 329 | Ok((one, rest)) => { 330 | let alive_hdls: Vec<_> = self 331 | .config 332 | .child_hdls 333 | .lock() 334 | .unwrap() 335 | .iter() 336 | .filter_map(|c| c.upgrade()) 337 | .collect(); 338 | 339 | let sig_ks = alive_hdls.iter().filter_map(|hdl| { 340 | if hdl == &child_hdl { 341 | Some(hdl.signal(one.into()).into_future()) 342 | } else { 343 | rest.map(|r| hdl.signal(r.into()).into_future()) 344 | } 345 | }); 346 | 347 | join_all(sig_ks).await; 348 | } 349 | } 350 | 351 | Cont::Process 352 | } 353 | 354 | async fn cleanup_children(&mut self) -> Cont { 355 | self.config 356 | .child_hdls 357 | .lock() 358 | .unwrap() 359 | .retain(|hdl| match hdl.upgrade() { 360 | None => false, 361 | Some(hdl) => hdl.0.sender_count() > 0, 362 | }); 363 | 364 | Cont::Process 365 | } 366 | 367 | async fn escalate(&mut self, e: Escalation) -> Cont { 368 | self.signal_children(InternalSignal::Pause, None).await; 369 | 370 | let res = self 371 | .config 372 | .parent_hdl 373 | .escalate(self.config.this_hdl.clone(), e); 374 | 375 | if res.is_err() { 376 | return Cont::Terminate(None); 377 | } 378 | 379 | Cont::WaitSignal 380 | } 381 | 382 | async fn restart(mut self, k: Option>) -> Lifecycle { 383 | self.config.mb_restart_k = k; 384 | 385 | self.signal_children(InternalSignal::Terminate, None).await; 386 | 387 | let res = AssertUnwindSafe(A::on_restart(&mut self.state)) 388 | .catch_unwind() 389 | .await; 390 | 391 | if let Err(e) = res { 392 | self.config.child_hdls.clear_poison(); 393 | error!(actor = %self, err = panic_msg(e), "paniced on restart"); 394 | } 395 | 396 | Lifecycle::Restarting(self.config) 397 | } 398 | 399 | async fn drop(&mut self) -> Lifecycle { 400 | for sig in self.config.sig_rx.drain() { 401 | match sig { 402 | RawSignal::Pause(k) 403 | | RawSignal::Resume(k) 404 | | RawSignal::Restart(k) 405 | | RawSignal::Terminate(k) => { 406 | if let Some(k) = k { 407 | k.notify_one() 408 | } 409 | } 410 | s => error!(actor = %self, sig = ?s, "received unexpected signal while dropping"), 411 | } 412 | } 413 | 414 | let res = AssertUnwindSafe(A::on_exit(&mut self.state, ExitCode::Dropped)) 415 | .catch_unwind() 416 | .await; 417 | 418 | if let Err(e) = res { 419 | self.config.child_hdls.clear_poison(); 420 | 421 | error!(actor = %self, err = panic_msg(e), "paniced on exit"); 422 | } 423 | 424 | self.config 425 | .parent_hdl 426 | .raw_send(RawSignal::ChildDropped) 427 | .expect("parent lives longer than child"); 428 | 429 | Lifecycle::Exit 430 | } 431 | 432 | async fn terminate(&mut self, k: Option>) -> Lifecycle { 433 | self.signal_children(InternalSignal::Terminate, k).await; 434 | 435 | let res = AssertUnwindSafe(A::on_exit(&mut self.state, ExitCode::Dropped)) 436 | .catch_unwind() 437 | .await; 438 | 439 | if let Err(e) = res { 440 | self.config.child_hdls.clear_poison(); 441 | 442 | error!(actor = %self, err = panic_msg(e), "paniced on exit"); 443 | } 444 | 445 | Lifecycle::Exit 446 | } 447 | 448 | fn process_sig(&mut self, sig: RawSignal) -> Option { 449 | match sig { 450 | #[cfg(feature = "monitor")] 451 | RawSignal::Monitor(t) => { 452 | self.add_monitor(t); 453 | None 454 | } 455 | 456 | RawSignal::Escalation(c, e) => Some(Cont::Supervise(c, e)), 457 | RawSignal::ChildDropped => Some(Cont::CleanupChildren), 458 | 459 | RawSignal::Pause(k) => Some(Cont::Pause(k)), 460 | RawSignal::Resume(k) => Some(Cont::Resume(k)), 461 | RawSignal::Restart(k) => Some(Cont::Restart(k)), 462 | RawSignal::Terminate(k) => Some(Cont::Terminate(k)), 463 | } 464 | 465 | // This is the place where monitor can access 466 | } 467 | 468 | async fn process_msg(&mut self, (msg, k): (A::Msg, Continuation)) -> Option { 469 | let ctx = self.config.ctx(); 470 | let res = AssertUnwindSafe(self.state.process_msg(ctx, msg, k)) 471 | .catch_unwind() 472 | .await; 473 | 474 | if let Err(e) = res { 475 | self.config.child_hdls.clear_poison(); 476 | return Some(Cont::Panic(Escalation::ProcessMsg(panic_msg(e)))); 477 | } 478 | 479 | #[cfg(feature = "monitor")] 480 | if self.config.monitor.is_monitor() { 481 | let new_hash = self.state.hash_code(); 482 | 483 | if new_hash != self.hash { 484 | let update = Update::State(self.state.state_view()); 485 | self.config.monitor.update(update); 486 | } 487 | 488 | self.hash = new_hash; 489 | } 490 | 491 | None 492 | } 493 | 494 | async fn signal_children(&mut self, sig: InternalSignal, k: Option>) { 495 | let alive_hdls: Vec<_> = self 496 | .config 497 | .child_hdls 498 | .lock() 499 | .unwrap() 500 | .iter() 501 | .filter_map(|c| c.upgrade()) 502 | .collect(); 503 | 504 | let sig_ks = alive_hdls.iter().map(|c| c.signal(sig).into_future()); 505 | 506 | join_all(sig_ks).await; 507 | 508 | if let Some(k) = k { 509 | k.notify_one() 510 | } 511 | } 512 | } 513 | 514 | impl Display for ActorState 515 | where 516 | A: Actor, 517 | Arg: ActorArgs, 518 | { 519 | fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { 520 | write!(f, "{}({})", type_name::(), self.config.this_hdl.id()) 521 | } 522 | } 523 | -------------------------------------------------------------------------------- /child_process.log: -------------------------------------------------------------------------------- 1 | 2025-09-26 15:37:47.205 +09:00  INFO dedup: Child process mode - using provided keys 2 | 2025-09-26 15:37:47.205 +09:00  INFO dedup: Received args[1] (other's public key): 4d40f8c74d907c4c1aa06bd6da93711a83cc4fad660282caa52646e6ea1019d0 3 | 2025-09-26 15:37:47.205 +09:00  INFO dedup: Received args[2] (our secret key bytes): 146 chars 4 | 2025-09-26 15:37:47.206 +09:00  INFO dedup: Parsed other_public_key: 4d40f8c74d907c4c1aa06bd6da93711a83cc4fad660282caa52646e6ea1019d0 5 | 2025-09-26 15:37:47.206 +09:00  INFO dedup: Parsed our_secret_key, public version: b82318e7d5af4e8ec242e32ece158110c29fd2b8bd8ecffd0917447d81978c3b 6 | 2025-09-26 15:37:47.260 +09:00  INFO dedup: RootContext initialized with public key: b82318e7d5af4e8ec242e32ece158110c29fd2b8bd8ecffd0917447d81978c3b 7 | 2025-09-26 15:37:47.260 +09:00  INFO dedup: Will attempt to connect to other peer with public key: 4d40f8c74d907c4c1aa06bd6da93711a83cc4fad660282caa52646e6ea1019d0 8 | 2025-09-26 15:37:47.260 +09:00  INFO dedup: Key comparison: our_key=b82318e7d5af4e8ec242e32ece158110c29fd2b8bd8ecffd0917447d81978c3b, other_key=4d40f8c74d907c4c1aa06bd6da93711a83cc4fad660282caa52646e6ea1019d0, our_key > other_key = true 9 | 2025-09-26 15:37:47.260 +09:00  INFO dedup: spawning PingPong actor... 10 | 2025-09-26 15:37:47.260 +09:00  INFO dedup: binding PingPong actor to 'ping_pong' name... 11 | 2025-09-26 15:37:47.260 +09:00 TRACE theta::context: binding ident=70696e675f706f6e6700000000000000 actor=dedup::PingPong(48cd3e79b957492d9fde1efc468d8dbb) 12 | 2025-09-26 15:37:47.260 +09:00 TRACE theta::remote::peer: starting local peer public_key=b82318e7d5af4e8ec242e32ece158110c29fd2b8bd8ecffd0917447d81978c3b 13 | 2025-09-26 15:37:48.260 +09:00  INFO dedup: Connecting to peer: 4d40f8c74d907c4c1aa06bd6da93711a83cc4fad660282caa52646e6ea1019d0 14 | 2025-09-26 15:37:48.261 +09:00  INFO dedup: Constructed URL for lookup: iroh://ping_pong@4d40f8c74d907c4c1aa06bd6da93711a83cc4fad660282caa52646e6ea1019d0 15 | 2025-09-26 15:37:48.261 +09:00  INFO dedup: Attempting lookup 1 for iroh://ping_pong@4d40f8c74d907c4c1aa06bd6da93711a83cc4fad660282caa52646e6ea1019d0 16 | 2025-09-26 15:37:48.261 +09:00 TRACE theta::remote::peer: creating peer=Peer(0x154d17430) public_key=4d40f8c74d907c4c1aa06bd6da93711a83cc4fad660282caa52646e6ea1019d0 17 | 2025-09-26 15:37:48.261 +09:00 TRACE theta::remote::peer: adding peer=Peer(0x154d17430) 18 | 2025-09-26 15:37:48.261 +09:00 TRACE theta::remote::peer: sending lookup request actor=dedup::PingPong(70696e675f706f6e6700000000000000) host=Peer(0x154d17430) key=0 19 | 2025-09-26 15:37:48.261 +09:00 TRACE theta::remote::peer: starting to accept streams from=Peer(0x154d17430) 20 | 2025-09-26 15:37:48.480 +09:00  INFO ep{me=b82318e7d5}:relay-actor: iroh::magicsock::transports::relay::actor: home is now relay https://aps1-1.relay.n0.iroh.iroh.link./, was None 21 | 2025-09-26 15:37:48.591 +09:00  INFO connect{me=b82318e7d5 alpn="theta" remote=4d40f8c74d}:discovery{me=b82318e7d5 node=4d40f8c74d}:add_node_addr:add_node_addr{node=4d40f8c74d}: iroh::magicsock::node_map: inserting new node in NodeMap node=4d40f8c74d relay_url=None source=dns 22 | 2025-09-26 15:37:48.593 +09:00  INFO connect{me=b82318e7d5 alpn="theta" remote=4d40f8c74d}:prepare_send:get_send_addrs{node=4d40f8c74d}: iroh::magicsock::node_map::node_state: new connection type typ=direct(192.168.1.104:52812) 23 | 2025-09-26 15:37:48.607 +09:00 TRACE theta::remote::peer: starting to receive frames from=Peer(0x154d17430) 24 | 2025-09-26 15:37:48.608 +09:00 DEBUG theta::remote::peer: received lookup response actor=dedup::PingPong(70696e675f706f6e6700000000000000) host=Peer(0x154d17430) key=0 resp=Ok(18 bytes) 25 | 2025-09-26 15:37:48.608 +09:00  INFO dedup: Successfully connected to peer after 1 attempts 26 | 2025-09-26 15:37:48.608 +09:00  INFO dedup: sending ping to iroh://ping_pong@4d40f8c74d907c4c1aa06bd6da93711a83cc4fad660282caa52646e6ea1019d0 every 5 seconds. Press Ctrl-C to stop. 27 | 2025-09-26 15:37:48.608 +09:00 TRACE theta::remote::peer: starting imported remote actor=dedup::PingPong(4e867953-85ed-4618-924b-86e5932860f3) host=Peer(0x154d17430) 28 | 2025-09-26 15:37:48.608 +09:00 TRACE theta::remote::peer: sending import initial frame actor=dedup::PingPong(4e867953-85ed-4618-924b-86e5932860f3) host=Peer(0x154d17430) 29 | 2025-09-26 15:37:48.608 +09:00 DEBUG theta::remote::peer: accepted incoming connection public_key=4d40f8c74d907c4c1aa06bd6da93711a83cc4fad660282caa52646e6ea1019d0 30 | 2025-09-26 15:37:48.608 +09:00 TRACE theta::remote::peer: handling unfavored incoming connection public_key=4d40f8c74d907c4c1aa06bd6da93711a83cc4fad660282caa52646e6ea1019d0 31 | 2025-09-26 15:37:48.608 +09:00 TRACE theta::remote::peer: accepting unfavored control_rx from=Peer(0x154d17430) 32 | 2025-09-26 15:37:48.609 +09:00  INFO dedup: sending ping to 4e867953-85ed-4618-924b-86e5932860f3 33 | 2025-09-26 15:37:48.609 +09:00 TRACE theta::remote::peer: starting to receive frames from=Peer(0x154d17430) 34 | 2025-09-26 15:37:48.609 +09:00 DEBUG theta::remote::peer: processed lookup request ident=70696e675f706f6e6700000000000000 host=Peer(0x154d17430) key=0 resp=ControlFrame::LookupResp { res: Ok(18 bytes), key: 0 } 35 | 2025-09-26 15:37:48.610 +09:00  INFO dedup: received pong from 4e867953-85ed-4618-924b-86e5932860f3 in 1.132583ms 36 | 2025-09-26 15:37:48.610 +09:00 DEBUG theta::remote::peer: accepted stream from=Peer(0x154d17430) 37 | 2025-09-26 15:37:48.610 +09:00 DEBUG theta::remote::peer: received initial frame len=18 from=Peer(0x154d17430) 38 | 2025-09-26 15:37:48.610 +09:00 DEBUG theta::remote::peer: deserialized initial frame from=Peer(0x154d17430) frame=InitFrame::Import { actor_id: 48cd3e79b957492d9fde1efc468d8dbb } 39 | 2025-09-26 15:37:48.610 +09:00 TRACE theta::actor_ref: listening exported actor=dedup::PingPong(48cd3e79b957492d9fde1efc468d8dbb) 40 | 2025-09-26 15:37:48.611 +09:00  INFO dedup: received ping from 4d40f8c74d907c4c1aa06bd6da93711a83cc4fad660282caa52646e6ea1019d0 41 | 2025-09-26 15:37:53.610 +09:00  INFO dedup: sending ping to 4e867953-85ed-4618-924b-86e5932860f3 42 | 2025-09-26 15:37:53.612 +09:00  INFO dedup: received ping from 4d40f8c74d907c4c1aa06bd6da93711a83cc4fad660282caa52646e6ea1019d0 43 | 2025-09-26 15:37:53.613 +09:00  INFO dedup: received pong from 4e867953-85ed-4618-924b-86e5932860f3 in 2.663208ms 44 | 2025-09-26 15:37:58.611 +09:00  INFO dedup: sending ping to 4e867953-85ed-4618-924b-86e5932860f3 45 | 2025-09-26 15:37:58.613 +09:00  INFO dedup: received ping from 4d40f8c74d907c4c1aa06bd6da93711a83cc4fad660282caa52646e6ea1019d0 46 | 2025-09-26 15:37:58.614 +09:00  INFO dedup: received pong from 4e867953-85ed-4618-924b-86e5932860f3 in 2.687583ms 47 | 2025-09-26 15:38:03.611 +09:00  INFO dedup: sending ping to 4e867953-85ed-4618-924b-86e5932860f3 48 | 2025-09-26 15:38:03.617 +09:00  INFO dedup: received ping from 4d40f8c74d907c4c1aa06bd6da93711a83cc4fad660282caa52646e6ea1019d0 49 | 2025-09-26 15:38:03.617 +09:00  INFO dedup: received pong from 4e867953-85ed-4618-924b-86e5932860f3 in 5.644791ms 50 | 2025-09-26 15:38:08.610 +09:00  INFO dedup: sending ping to 4e867953-85ed-4618-924b-86e5932860f3 51 | 2025-09-26 15:38:08.612 +09:00  INFO dedup: received ping from 4d40f8c74d907c4c1aa06bd6da93711a83cc4fad660282caa52646e6ea1019d0 52 | 2025-09-26 15:38:08.612 +09:00  INFO dedup: received pong from 4e867953-85ed-4618-924b-86e5932860f3 in 1.987ms 53 | 2025-09-26 15:38:13.611 +09:00  INFO dedup: sending ping to 4e867953-85ed-4618-924b-86e5932860f3 54 | 2025-09-26 15:38:13.617 +09:00  INFO dedup: received ping from 4d40f8c74d907c4c1aa06bd6da93711a83cc4fad660282caa52646e6ea1019d0 55 | 2025-09-26 15:38:13.618 +09:00  INFO dedup: received pong from 4e867953-85ed-4618-924b-86e5932860f3 in 6.677834ms 56 | 2025-09-26 15:38:18.611 +09:00  INFO dedup: sending ping to 4e867953-85ed-4618-924b-86e5932860f3 57 | 2025-09-26 15:38:18.615 +09:00  INFO dedup: received ping from 4d40f8c74d907c4c1aa06bd6da93711a83cc4fad660282caa52646e6ea1019d0 58 | 2025-09-26 15:38:18.616 +09:00  INFO dedup: received pong from 4e867953-85ed-4618-924b-86e5932860f3 in 4.168792ms 59 | 2025-09-26 15:38:23.611 +09:00  INFO dedup: sending ping to 4e867953-85ed-4618-924b-86e5932860f3 60 | 2025-09-26 15:38:23.613 +09:00  INFO dedup: received ping from 4d40f8c74d907c4c1aa06bd6da93711a83cc4fad660282caa52646e6ea1019d0 61 | 2025-09-26 15:38:23.614 +09:00  INFO dedup: received pong from 4e867953-85ed-4618-924b-86e5932860f3 in 2.437458ms 62 | 2025-09-26 15:38:28.611 +09:00  INFO dedup: sending ping to 4e867953-85ed-4618-924b-86e5932860f3 63 | 2025-09-26 15:38:28.613 +09:00  INFO dedup: received ping from 4d40f8c74d907c4c1aa06bd6da93711a83cc4fad660282caa52646e6ea1019d0 64 | 2025-09-26 15:38:28.614 +09:00  INFO dedup: received pong from 4e867953-85ed-4618-924b-86e5932860f3 in 2.313791ms 65 | 2025-09-26 15:38:33.611 +09:00  INFO dedup: sending ping to 4e867953-85ed-4618-924b-86e5932860f3 66 | 2025-09-26 15:38:33.612 +09:00  INFO dedup: received pong from 4e867953-85ed-4618-924b-86e5932860f3 in 927.208µs 67 | 2025-09-26 15:38:33.612 +09:00  INFO dedup: received ping from 4d40f8c74d907c4c1aa06bd6da93711a83cc4fad660282caa52646e6ea1019d0 68 | 2025-09-26 15:38:38.611 +09:00  INFO dedup: sending ping to 4e867953-85ed-4618-924b-86e5932860f3 69 | 2025-09-26 15:38:38.612 +09:00  INFO dedup: received ping from 4d40f8c74d907c4c1aa06bd6da93711a83cc4fad660282caa52646e6ea1019d0 70 | 2025-09-26 15:38:38.613 +09:00  INFO dedup: received pong from 4e867953-85ed-4618-924b-86e5932860f3 in 1.370792ms 71 | 2025-09-26 15:38:43.611 +09:00  INFO dedup: sending ping to 4e867953-85ed-4618-924b-86e5932860f3 72 | 2025-09-26 15:38:43.612 +09:00  INFO dedup: received ping from 4d40f8c74d907c4c1aa06bd6da93711a83cc4fad660282caa52646e6ea1019d0 73 | 2025-09-26 15:38:43.613 +09:00  INFO dedup: received pong from 4e867953-85ed-4618-924b-86e5932860f3 in 1.588083ms 74 | 2025-09-26 15:38:48.612 +09:00  INFO dedup: sending ping to 4e867953-85ed-4618-924b-86e5932860f3 75 | 2025-09-26 15:38:48.617 +09:00  INFO dedup: received ping from 4d40f8c74d907c4c1aa06bd6da93711a83cc4fad660282caa52646e6ea1019d0 76 | 2025-09-26 15:38:48.618 +09:00  INFO dedup: received pong from 4e867953-85ed-4618-924b-86e5932860f3 in 5.126166ms 77 | 2025-09-26 15:38:53.612 +09:00  INFO dedup: sending ping to 4e867953-85ed-4618-924b-86e5932860f3 78 | 2025-09-26 15:38:53.613 +09:00  INFO dedup: received ping from 4d40f8c74d907c4c1aa06bd6da93711a83cc4fad660282caa52646e6ea1019d0 79 | 2025-09-26 15:38:53.613 +09:00  INFO dedup: received pong from 4e867953-85ed-4618-924b-86e5932860f3 in 1.38025ms 80 | 2025-09-26 15:38:58.612 +09:00  INFO dedup: sending ping to 4e867953-85ed-4618-924b-86e5932860f3 81 | 2025-09-26 15:38:58.616 +09:00  INFO dedup: received ping from 4d40f8c74d907c4c1aa06bd6da93711a83cc4fad660282caa52646e6ea1019d0 82 | 2025-09-26 15:38:58.617 +09:00  INFO dedup: received pong from 4e867953-85ed-4618-924b-86e5932860f3 in 4.937084ms 83 | 2025-09-26 15:39:03.611 +09:00  INFO dedup: sending ping to 4e867953-85ed-4618-924b-86e5932860f3 84 | 2025-09-26 15:39:03.612 +09:00  INFO dedup: received pong from 4e867953-85ed-4618-924b-86e5932860f3 in 1.028834ms 85 | 2025-09-26 15:39:03.613 +09:00  INFO dedup: received ping from 4d40f8c74d907c4c1aa06bd6da93711a83cc4fad660282caa52646e6ea1019d0 86 | 2025-09-26 15:39:08.611 +09:00  INFO dedup: sending ping to 4e867953-85ed-4618-924b-86e5932860f3 87 | 2025-09-26 15:39:08.613 +09:00  INFO dedup: received pong from 4e867953-85ed-4618-924b-86e5932860f3 in 1.194375ms 88 | 2025-09-26 15:39:08.614 +09:00  INFO dedup: received ping from 4d40f8c74d907c4c1aa06bd6da93711a83cc4fad660282caa52646e6ea1019d0 89 | 2025-09-26 15:39:13.612 +09:00  INFO dedup: sending ping to 4e867953-85ed-4618-924b-86e5932860f3 90 | 2025-09-26 15:39:13.615 +09:00  INFO dedup: received ping from 4d40f8c74d907c4c1aa06bd6da93711a83cc4fad660282caa52646e6ea1019d0 91 | 2025-09-26 15:39:13.616 +09:00  INFO dedup: received pong from 4e867953-85ed-4618-924b-86e5932860f3 in 2.995583ms 92 | 2025-09-26 15:39:18.612 +09:00  INFO dedup: sending ping to 4e867953-85ed-4618-924b-86e5932860f3 93 | 2025-09-26 15:39:18.615 +09:00  INFO dedup: received ping from 4d40f8c74d907c4c1aa06bd6da93711a83cc4fad660282caa52646e6ea1019d0 94 | 2025-09-26 15:39:18.616 +09:00  INFO dedup: received pong from 4e867953-85ed-4618-924b-86e5932860f3 in 3.666375ms 95 | 2025-09-26 15:39:23.611 +09:00  INFO dedup: sending ping to 4e867953-85ed-4618-924b-86e5932860f3 96 | 2025-09-26 15:39:23.614 +09:00  INFO dedup: received ping from 4d40f8c74d907c4c1aa06bd6da93711a83cc4fad660282caa52646e6ea1019d0 97 | 2025-09-26 15:39:23.615 +09:00  INFO dedup: received pong from 4e867953-85ed-4618-924b-86e5932860f3 in 3.501ms 98 | 2025-09-26 15:39:28.612 +09:00  INFO dedup: sending ping to 4e867953-85ed-4618-924b-86e5932860f3 99 | 2025-09-26 15:39:28.615 +09:00  INFO dedup: received ping from 4d40f8c74d907c4c1aa06bd6da93711a83cc4fad660282caa52646e6ea1019d0 100 | 2025-09-26 15:39:28.617 +09:00  INFO dedup: received pong from 4e867953-85ed-4618-924b-86e5932860f3 in 3.816917ms 101 | 2025-09-26 15:39:33.612 +09:00  INFO dedup: sending ping to 4e867953-85ed-4618-924b-86e5932860f3 102 | 2025-09-26 15:39:33.616 +09:00  INFO dedup: received ping from 4d40f8c74d907c4c1aa06bd6da93711a83cc4fad660282caa52646e6ea1019d0 103 | 2025-09-26 15:39:33.618 +09:00  INFO dedup: received pong from 4e867953-85ed-4618-924b-86e5932860f3 in 4.577667ms 104 | 2025-09-26 15:39:38.612 +09:00  INFO dedup: sending ping to 4e867953-85ed-4618-924b-86e5932860f3 105 | 2025-09-26 15:39:38.613 +09:00  INFO dedup: received ping from 4d40f8c74d907c4c1aa06bd6da93711a83cc4fad660282caa52646e6ea1019d0 106 | 2025-09-26 15:39:38.614 +09:00  INFO dedup: received pong from 4e867953-85ed-4618-924b-86e5932860f3 in 1.256708ms 107 | 2025-09-26 15:39:39.765 +09:00  INFO dedup: Received Ctrl-C signal, shutting down... 108 | 2025-09-26 15:39:39.766 +09:00  INFO dedup: Shutdown signal received, exiting... 109 | --------------------------------------------------------------------------------