From 8cf16ae0b095b9ad4a0dd8aec61b2df61c47d38d Mon Sep 17 00:00:00 2001 From: Sean Lawlor Date: Tue, 17 Dec 2024 13:03:17 -0500 Subject: [PATCH] Add proper span handling to factories through extension trait --- ractor/Cargo.toml | 2 +- ractor/src/factory/job.rs | 16 +- ractor/src/factory/mod.rs | 57 ++- ractor/src/factory/tests/basic.rs | 46 +-- ractor/src/factory/tests/dynamic_pool.rs | 41 +- ractor/src/factory/worker.rs | 456 ++++++++++++++++++++++- ractor_cluster/Cargo.toml | 6 +- ractor_cluster_derive/Cargo.toml | 2 +- 8 files changed, 528 insertions(+), 98 deletions(-) diff --git a/ractor/Cargo.toml b/ractor/Cargo.toml index 694f3b2..8e3d9e1 100644 --- a/ractor/Cargo.toml +++ b/ractor/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "ractor" -version = "0.13.5" +version = "0.14.0" authors = ["Sean Lawlor", "Evan Au", "Dillon George"] description = "A actor framework for Rust" documentation = "https://docs.rs/ractor" diff --git a/ractor/src/factory/job.rs b/ractor/src/factory/job.rs index 0e3b76b..a241d1d 100644 --- a/ractor/src/factory/job.rs +++ b/ractor/src/factory/job.rs @@ -11,6 +11,7 @@ use std::sync::Arc; use std::{hash::Hash, time::SystemTime}; use bon::Builder; +use tracing::Span; use crate::{concurrency::Duration, Message}; use crate::{ActorRef, RpcReplyPort}; @@ -40,7 +41,7 @@ pub trait JobKey: Debug + Hash + Send + Sync + Clone + Eq + PartialEq + 'static impl JobKey for T {} /// Represents options for the specified job -#[derive(Debug, Eq, PartialEq, Clone)] +#[derive(Debug, PartialEq, Clone)] pub struct JobOptions { /// Time job was submitted from the client pub submit_time: SystemTime, @@ -50,6 +51,9 @@ pub struct JobOptions { pub worker_time: SystemTime, /// Time-to-live for the job pub ttl: Option, + /// The parent span we want to propagate to the worker. + /// Spans don't propagate over the wire in networks + pub span: Option, } impl Default for JobOptions { @@ -59,6 +63,10 @@ impl Default for JobOptions { factory_time: SystemTime::now(), worker_time: SystemTime::now(), ttl: None, + #[cfg(feature = "message_span_propogation")] + span: Some(Span::current()), + #[cfg(not(feature = "message_span_propogation"))] + span: None, } } } @@ -86,7 +94,10 @@ impl BytesConvertable for JobOptions { fn from_bytes(mut data: Vec) -> Self { if data.len() != 16 { - Self::default() + Self { + span: None, + ..Default::default() + } } else { let ttl_bytes = data.split_off(8); @@ -100,6 +111,7 @@ impl BytesConvertable for JobOptions { } else { None }, + span: None, ..Default::default() } } diff --git a/ractor/src/factory/mod.rs b/ractor/src/factory/mod.rs index 66bd3f5..b40fd2b 100644 --- a/ractor/src/factory/mod.rs +++ b/ractor/src/factory/mod.rs @@ -66,53 +66,38 @@ //! /// the business logic for each message that will be done in parallel. //! struct ExampleWorker; //! #[cfg_attr(feature = "async-trait", ractor::async_trait)] -//! impl Actor for ExampleWorker { -//! type Msg = WorkerMessage<(), ExampleMessage>; -//! type State = WorkerStartContext<(), ExampleMessage, ()>; -//! type Arguments = WorkerStartContext<(), ExampleMessage, ()>; +//! impl Worker for ExampleWorker { +//! type Key = (); +//! type Message = ExampleMessage; +//! type State = (); +//! type Arguments = (); //! async fn pre_start( //! &self, -//! _myself: ActorRef, +//! wid: WorkerId, +//! factory: &ActorRef>, //! startup_context: Self::Arguments, //! ) -> Result { //! Ok(startup_context) //! } //! async fn handle( //! &self, -//! _myself: ActorRef, -//! message: Self::Msg, -//! state: &mut Self::State, +//! wid: WorkerId, +//! factory: &ActorRef>, +//! Job {msg, key, ..}: Job<(), ExampleMessage>, +//! _state: &mut Self::State, //! ) -> Result<(), ActorProcessingErr> { -//! match message { -//! WorkerMessage::FactoryPing(time) => { -//! // This is a message which all factory workers **must** -//! // adhere to. It is a background processing message from the -//! // factory which is used for (a) metrics and (b) detecting -//! // stuck workers, i.e. workers which aren't making progress -//! // processing their messages -//! state -//! .factory -//! .cast(FactoryMessage::WorkerPong(state.wid, time.elapsed()))?; +//! // Actual business logic that we want to parallelize +//! tracing::trace!("Worker {} received {:?}", wid, msg); +//! match msg { +//! ExampleMessage::PrintValue(value) => { +//! tracing::info!("Worker {} printing value {value}", wid); //! } -//! WorkerMessage::Dispatch(job) => { -//! // Actual business logic that we want to parallelize -//! tracing::trace!("Worker {} received {:?}", state.wid, job.msg); -//! match job.msg { -//! ExampleMessage::PrintValue(value) => { -//! tracing::info!("Worker {} printing value {value}", state.wid); -//! } -//! ExampleMessage::EchoValue(value, reply) => { -//! tracing::info!("Worker {} echoing value {value}", state.wid); -//! let _ = reply.send(value); -//! } -//! } -//! // job finished, on success or err we report back to the factory -//! state -//! .factory -//! .cast(FactoryMessage::Finished(state.wid, job.key))?; +//! ExampleMessage::EchoValue(value, reply) => { +//! tracing::info!("Worker {} echoing value {value}", wid); +//! let _ = reply.send(value); //! } //! } -//! Ok(()) +//! Ok(key) //! } //! } //! /// Used by the factory to build new [ExampleWorker]s. @@ -200,7 +185,7 @@ pub use factoryimpl::{Factory, FactoryArguments, FactoryArgumentsBuilder}; pub use job::{Job, JobKey, JobOptions, MessageRetryStrategy, RetriableMessage}; pub use lifecycle::FactoryLifecycleHooks; pub use worker::{ - DeadMansSwitchConfiguration, WorkerBuilder, WorkerCapacityController, WorkerMessage, + DeadMansSwitchConfiguration, Worker, WorkerBuilder, WorkerCapacityController, WorkerMessage, WorkerProperties, WorkerStartContext, }; diff --git a/ractor/src/factory/tests/basic.rs b/ractor/src/factory/tests/basic.rs index bdbd1e1..07c5e53 100644 --- a/ractor/src/factory/tests/basic.rs +++ b/ractor/src/factory/tests/basic.rs @@ -55,14 +55,16 @@ struct TestWorker { } #[cfg_attr(feature = "async-trait", crate::async_trait)] -impl Actor for TestWorker { - type Msg = WorkerMessage; - type State = Self::Arguments; - type Arguments = WorkerStartContext; +impl Worker for TestWorker { + type Key = TestKey; + type Message = TestMessage; + type State = (); + type Arguments = (); async fn pre_start( &self, - _myself: ActorRef, + _wid: WorkerId, + _factory: &ActorRef>, startup_context: Self::Arguments, ) -> Result { Ok(startup_context) @@ -70,32 +72,20 @@ impl Actor for TestWorker { async fn handle( &self, - _myself: ActorRef, - message: Self::Msg, - state: &mut Self::State, - ) -> Result<(), ActorProcessingErr> { - match message { - WorkerMessage::FactoryPing(time) => { - state - .factory - .cast(FactoryMessage::WorkerPong(state.wid, time.elapsed()))?; - } - WorkerMessage::Dispatch(job) => { - tracing::debug!("Worker received {:?}", job.msg); + _wid: WorkerId, + _factory: &ActorRef>, + Job { msg, key, .. }: Job, + _state: &mut Self::State, + ) -> Result { + tracing::debug!("Worker received {:?}", msg); - self.counter.fetch_add(1, Ordering::Relaxed); - - if let Some(timeout_ms) = self.slow { - crate::concurrency::sleep(Duration::from_millis(timeout_ms)).await; - } + self.counter.fetch_add(1, Ordering::Relaxed); - // job finished, on success or err we report back to the factory - state - .factory - .cast(FactoryMessage::Finished(state.wid, job.key))?; - } + if let Some(timeout_ms) = self.slow { + crate::concurrency::sleep(Duration::from_millis(timeout_ms)).await; } - Ok(()) + + Ok(key) } } diff --git a/ractor/src/factory/tests/dynamic_pool.rs b/ractor/src/factory/tests/dynamic_pool.rs index d8cd68c..688ad5d 100644 --- a/ractor/src/factory/tests/dynamic_pool.rs +++ b/ractor/src/factory/tests/dynamic_pool.rs @@ -48,14 +48,16 @@ struct TestWorker { impl crate::Message for TestMessage {} #[cfg_attr(feature = "async-trait", crate::async_trait)] -impl Actor for TestWorker { - type Msg = WorkerMessage; +impl Worker for TestWorker { + type Key = TestKey; + type Message = TestMessage; type State = Self::Arguments; - type Arguments = WorkerStartContext; + type Arguments = (); async fn pre_start( &self, - _myself: ActorRef, + _wid: WorkerId, + _factory: &ActorRef>, startup_context: Self::Arguments, ) -> Result { Ok(startup_context) @@ -63,28 +65,15 @@ impl Actor for TestWorker { async fn handle( &self, - _myself: ActorRef, - message: Self::Msg, - state: &mut Self::State, - ) -> Result<(), ActorProcessingErr> { - match message { - WorkerMessage::FactoryPing(time) => { - state - .factory - .cast(FactoryMessage::WorkerPong(state.wid, time.elapsed()))?; - } - WorkerMessage::Dispatch(job) => { - tracing::debug!("Worker received {:?}", job.msg); - - self.id_map.insert(state.wid); - - // job finished, on success or err we report back to the factory - state - .factory - .cast(FactoryMessage::Finished(state.wid, job.key))?; - } - } - Ok(()) + wid: WorkerId, + _factory: &ActorRef>, + Job { msg, key, .. }: Job, + _state: &mut Self::State, + ) -> Result { + tracing::debug!("Worker received {:?}", msg); + + self.id_map.insert(wid); + Ok(key) } } diff --git a/ractor/src/factory/worker.rs b/ractor/src/factory/worker.rs index 92dc3a9..59e03f0 100644 --- a/ractor/src/factory/worker.rs +++ b/ractor/src/factory/worker.rs @@ -10,10 +10,11 @@ use std::fmt::Debug; use std::sync::Arc; use bon::Builder; +use tracing::Instrument; use crate::concurrency::{Duration, Instant, JoinHandle}; use crate::{Actor, ActorId, ActorProcessingErr}; -use crate::{ActorRef, Message, MessagingErr}; +use crate::{ActorCell, ActorRef, Message, MessagingErr, SupervisionEvent}; use super::discard::{DiscardMode, WorkerDiscardSettings}; use super::stats::FactoryStatsLayer; @@ -36,6 +37,459 @@ pub struct DeadMansSwitchConfiguration { pub kill_worker: bool, } +/// A factory worker trait, which is a basic wrapper around +/// actor logic, with predefined type information specific to workers +/// +/// IMPORTANT: Workers are actors at their core principal, but with +/// somewhat customized logic. This logic assists in tracking worker health, +/// processing messages in a load-balanced manner, and managing necessary +/// start automatically without copying the code repeatedly. +/// +/// This trait implements as much of the custom wrapping logic as possible +/// without breaking the factory <-> worker API requirement. If you so wish +/// you can fully specify the actor properties instead of using this +/// assistance trait. +#[cfg_attr(feature = "async-trait", crate::async_trait)] +pub trait Worker: Send + Sync + 'static { + /// The worker's job-key type + type Key: JobKey; + /// The worker's message type + type Message: Message; + /// The optional startup arguments for the worker (use `()` to ignore) + type Arguments: Message; + /// The worker's internal state + type State: crate::State; + + /// Invoked when a worker is being started by the system. + /// + /// Any initialization inherent to the actor's role should be + /// performed here hence why it returns the initial state. + /// + /// Panics in `pre_start` do not invoke the + /// supervision strategy and the actor won't be started. The `spawn` + /// will return an error to the caller + /// + /// * `wid` - The id of this worker in the factory + /// * `factory` - The handle to the factory that owns and manages this worker + /// * `args` - Arguments that are passed in the spawning of the worker which are + /// necessary to construct the initial state + /// + /// Returns an initial [Worker::State] to bootstrap the actor + #[cfg(not(feature = "async-trait"))] + fn pre_start( + &self, + wid: WorkerId, + factory: &ActorRef>, + args: Self::Arguments, + ) -> impl Future> + Send; + + /// Invoked when a worker is being started by the system. + /// + /// Any initialization inherent to the actor's role should be + /// performed here hence why it returns the initial state. + /// + /// Panics in `pre_start` do not invoke the + /// supervision strategy and the actor won't be started. The `spawn` + /// will return an error to the caller + /// + /// * `wid` - The id of this worker in the factory + /// * `factory` - The handle to the factory that owns and manages this worker + /// * `args` - Arguments that are passed in the spawning of the worker which are + /// necessary to construct the initial state + /// + /// Returns an initial [Worker::State] to bootstrap the actor + #[cfg(feature = "async-trait")] + async fn pre_start( + &self, + wid: WorkerId, + factory: &ActorRef>, + args: Self::Arguments, + ) -> Result; + + /// Invoked after an actor has started. + /// + /// Any post initialization can be performed here, such as writing + /// to a log file, emitting metrics. + /// + /// * `wid` - The id of this worker in the factory + /// * `factory` - The handle to the factory that owns and manages this worker + /// * `state` - The worker's internal state, which is mutable and owned by the worker + /// + /// Panics in `post_start` follow the supervision strategy. + #[allow(unused_variables)] + #[cfg(not(feature = "async-trait"))] + fn post_start( + &self, + wid: WorkerId, + factory: &ActorRef>, + state: &mut Self::State, + ) -> impl Future> + Send { + async { Ok(()) } + } + /// Invoked after an actor has started. + /// + /// Any post initialization can be performed here, such as writing + /// to a log file, emitting metrics. + /// + /// * `wid` - The id of this worker in the factory + /// * `factory` - The handle to the factory that owns and manages this worker + /// * `state` - The worker's internal state, which is mutable and owned by the worker + /// + /// Panics in `post_start` follow the supervision strategy. + #[allow(unused_variables)] + #[cfg(feature = "async-trait")] + async fn post_start( + &self, + wid: WorkerId, + factory: &ActorRef>, + state: &mut Self::State, + ) -> Result<(), ActorProcessingErr> { + Ok(()) + } + + /// Invoked after an actor has been stopped to perform final cleanup. In the + /// event the actor is terminated with killed or has self-panicked, + /// `post_stop` won't be called. + /// + /// * `wid` - The id of this worker in the factory + /// * `factory` - The handle to the factory that owns and manages this worker + /// * `state` - The worker's internal state, which is mutable and owned by the worker + /// + /// Panics in `post_stop` follow the supervision strategy. + #[allow(unused_variables)] + #[cfg(not(feature = "async-trait"))] + fn post_stop( + &self, + wid: WorkerId, + factory: &ActorRef>, + state: &mut Self::State, + ) -> impl Future> + Send { + async { Ok(()) } + } + /// Invoked after an actor has been stopped to perform final cleanup. In the + /// event the actor is terminated with killed or has self-panicked, + /// `post_stop` won't be called. + /// + /// * `wid` - The id of this worker in the factory + /// * `factory` - The handle to the factory that owns and manages this worker + /// * `state` - The worker's internal state, which is mutable and owned by the worker + /// + /// Panics in `post_stop` follow the supervision strategy. + #[allow(unused_variables)] + #[cfg(feature = "async-trait")] + async fn post_stop( + &self, + wid: WorkerId, + factory: &ActorRef>, + state: &mut Self::State, + ) -> Result<(), ActorProcessingErr> { + Ok(()) + } + + /// Handle the incoming message from the event processing loop. Unhandled panickes will be + /// captured and sent to the supervisor(s) + /// + /// * `wid` - The id of this worker in the factory + /// * `factory` - The handle to the factory that owns and manages this worker + /// * `job` - The [Job] which this worker should process + /// * `state` - The worker's internal state, which is mutable and owned by the worker + /// + /// Returns the [Job::key] upon success or the error on failure + #[allow(unused_variables)] + #[cfg(not(feature = "async-trait"))] + fn handle( + &self, + wid: WorkerId, + factory: &ActorRef>, + job: Job, + state: &mut Self::State, + ) -> impl Future> + Send { + async { Ok(job.key) } + } + + /// Handle the incoming message from the event processing loop. Unhandled panickes will be + /// captured and sent to the supervisor(s) + /// + /// * `wid` - The id of this worker in the factory + /// * `factory` - The handle to the factory that owns and manages this worker + /// * `job` - The [Job] which this worker should process + /// * `state` - The worker's internal state, which is mutable and owned by the worker + /// + /// Returns the [Job::key] upon success or the error on failure + #[allow(unused_variables)] + #[cfg(feature = "async-trait")] + async fn handle( + &self, + wid: WorkerId, + factory: &ActorRef>, + job: Job, + state: &mut Self::State, + ) -> Result { + Ok(job.key) + } + + /// Handle the incoming supervision event. Unhandled panics will be captured and + /// sent the the supervisor(s). The default supervision behavior is to exit the + /// supervisor on any child exit. To override this behavior, implement this function. + /// + /// * `myself` - A reference to this actor's ActorCell + /// * `message` - The message to process + /// * `state` - A mutable reference to the internal actor's state + #[allow(unused_variables)] + #[cfg(not(feature = "async-trait"))] + fn handle_supervisor_evt( + &self, + myself: ActorCell, + message: SupervisionEvent, + state: &mut Self::State, + ) -> impl Future> + Send { + async move { + match message { + SupervisionEvent::ActorTerminated(who, _, _) + | SupervisionEvent::ActorFailed(who, _) => { + myself.stop(None); + } + _ => {} + } + Ok(()) + } + } + /// Handle the incoming supervision event. Unhandled panics will be captured and + /// sent the the supervisor(s). The default supervision behavior is to exit the + /// supervisor on any child exit. To override this behavior, implement this function. + /// + /// * `myself` - A reference to this actor's ActorCell + /// * `message` - The message to process + /// * `state` - A mutable reference to the internal actor's state + #[allow(unused_variables)] + #[cfg(feature = "async-trait")] + async fn handle_supervisor_evt( + &self, + myself: ActorCell, + message: SupervisionEvent, + state: &mut Self::State, + ) -> Result<(), ActorProcessingErr> { + match message { + SupervisionEvent::ActorTerminated(who, _, _) + | SupervisionEvent::ActorFailed(who, _) => { + myself.stop(None); + } + _ => {} + } + Ok(()) + } +} + +/// The inner state of the wrapped [Worker] but held privately in trust +/// for the [Worker] implementation +#[doc(hidden)] +pub struct WorkerState { + factory: ActorRef>, + wid: WorkerId, + state: TWorker::State, +} + +impl std::fmt::Debug for WorkerState { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "WorkerState") + } +} + +#[cfg_attr(feature = "async-trait", crate::async_trait)] +impl Actor for T +where + T: Worker, +{ + type Msg = WorkerMessage<::Key, ::Message>; + type Arguments = WorkerStartContext< + ::Key, + ::Message, + ::Arguments, + >; + type State = WorkerState; + + #[cfg(feature = "async-trait")] + async fn pre_start( + &self, + _: ActorRef, + WorkerStartContext { + wid, + factory, + custom_start, + }: Self::Arguments, + ) -> Result { + let inner_state = ::pre_start(self, wid, &factory, custom_start).await?; + Ok(Self::State { + wid, + factory, + state: inner_state, + }) + } + + #[cfg(not(feature = "async-trait"))] + fn pre_start( + &self, + _: ActorRef, + WorkerStartContext { + wid, + factory, + custom_start, + }: Self::Arguments, + ) -> impl Future> + Send { + let inner_state = ::pre_start(&self, wid, &factory, custom_start).await?; + async { + Ok(Self::State { + wid, + factory, + state: inner_state, + }) + } + } + + #[cfg(not(feature = "async-trait"))] + fn post_start( + &self, + _: ActorRef, + state: &mut Self::State, + ) -> impl Future> + Send { + async { + ::post_start(&self, state.wid, &state.factory, &mut state.state).await + } + } + + #[cfg(feature = "async-trait")] + async fn post_start( + &self, + _: ActorRef, + state: &mut Self::State, + ) -> Result<(), ActorProcessingErr> { + ::post_start(self, state.wid, &state.factory, &mut state.state).await + } + + #[cfg(not(feature = "async-trait"))] + fn post_stop( + &self, + _: ActorRef, + state: &mut Self::State, + ) -> impl Future> + Send { + async { + ::post_stop(&self, state.wid, &state.factory, &mut state.state).await + } + } + + #[cfg(feature = "async-trait")] + async fn post_stop( + &self, + _: ActorRef, + state: &mut Self::State, + ) -> Result<(), ActorProcessingErr> { + ::post_stop(self, state.wid, &state.factory, &mut state.state).await + } + + #[cfg(not(feature = "async-trait"))] + fn handle( + &self, + _: ActorRef, + message: Self::Msg, + state: &mut Self::State, + ) -> impl Future> + Send { + async { + match message { + WorkerMessage::FactoryPing(time) => { + tracing::trace!("Worker {} - ping", state.wid); + + state + .factory + .cast(FactoryMessage::WorkerPong(state.wid, time.elapsed()))?; + } + WorkerMessage::Dispatch(mut job) => { + let key = if let Some(span) = job.options.span.take() { + ::handle( + &self, + state.wid, + &state.factory, + job, + &mut state.state, + ) + .instrument(span) + .await + } else { + ::handle( + &self, + state.wid, + &state.factory, + job, + &mut state.state, + ) + .await + }?; + state + .factory + .cast(FactoryMessage::Finished(state.wid, key))?; + Ok(()) + } + } + } + } + + #[cfg(feature = "async-trait")] + async fn handle( + &self, + _: ActorRef, + message: Self::Msg, + state: &mut Self::State, + ) -> Result<(), ActorProcessingErr> { + match message { + WorkerMessage::FactoryPing(time) => { + tracing::trace!("Worker {} - ping", state.wid); + + state + .factory + .cast(FactoryMessage::WorkerPong(state.wid, time.elapsed()))?; + Ok(()) + } + WorkerMessage::Dispatch(mut job) => { + let key = if let Some(span) = job.options.span.take() { + ::handle(self, state.wid, &state.factory, job, &mut state.state) + .instrument(span) + .await + } else { + ::handle(self, state.wid, &state.factory, job, &mut state.state) + .await + }?; + state + .factory + .cast(FactoryMessage::Finished(state.wid, key))?; + Ok(()) + } + } + } + + #[cfg(not(feature = "async-trait"))] + fn handle_supervisor_evt( + &self, + myself: ActorRef, + message: SupervisionEvent, + state: &mut Self::State, + ) -> impl Future> + Send { + async move { + ::handle_supervisor_evt(&self, myself.into(), message, &mut state.state) + .await + } + } + + #[cfg(feature = "async-trait")] + async fn handle_supervisor_evt( + &self, + myself: ActorRef, + message: SupervisionEvent, + state: &mut Self::State, + ) -> Result<(), ActorProcessingErr> { + ::handle_supervisor_evt(self, myself.into(), message, &mut state.state) + .await + } +} + /// The [super::Factory] is responsible for spawning workers /// and re-spawning workers under failure scenarios. This means that /// it needs to understand how to build workers. The [WorkerBuilder] diff --git a/ractor_cluster/Cargo.toml b/ractor_cluster/Cargo.toml index 716cc1d..79a7bef 100644 --- a/ractor_cluster/Cargo.toml +++ b/ractor_cluster/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "ractor_cluster" -version = "0.13.5" +version = "0.14.0" authors = ["Sean Lawlor", "Evan Au", "Dillon George"] description = "Distributed cluster environment of Ractor actors" documentation = "https://docs.rs/ractor" @@ -24,8 +24,8 @@ prost-build = { version = "0.13" } bytes = { version = "1" } prost = { version = "0.13" } prost-types = { version = "0.13" } -ractor = { version = "0.13.0", features = ["cluster"], path = "../ractor" } -ractor_cluster_derive = { version = "0.13.0", path = "../ractor_cluster_derive" } +ractor = { version = "0.14.0", features = ["cluster"], path = "../ractor" } +ractor_cluster_derive = { version = "0.14.0", path = "../ractor_cluster_derive" } rand = "0.8" sha2 = "0.10" tokio = { version = "1.30", features = ["rt", "time", "sync", "macros", "net", "io-util", "tracing"]} diff --git a/ractor_cluster_derive/Cargo.toml b/ractor_cluster_derive/Cargo.toml index 5d18794..c0ea06e 100644 --- a/ractor_cluster_derive/Cargo.toml +++ b/ractor_cluster_derive/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "ractor_cluster_derive" -version = "0.13.5" +version = "0.14.0" authors = ["Sean Lawlor "] description = "Derives for ractor_cluster" license = "MIT"