Repository: slawlor/ractor (rust)

Issue:
Unable to opt-out async-trait feature when using ractor_cluster
**Is your feature request related to a problem? Please describe.**

`ractor_cluster` uses `ractor` with default features, so when I import both and want to use native async trait support, I can't.

**Describe the solution you'd like**

Probably add async-trait feature also to `ractor_cluster`.

**Describe alternatives you've considered**

Use other mechanism to enable async-trait crate
1. Feature environment variable, or custom cfg
  - pros: applicable to both crates
  - cons: async-crate will always be pulled in
2. Require native async trait
  - pros: remove the complexity of having two slightly incompatible async trait impl
  - cons: they are not 100% parity

**Additional context**

N/A

Pull request:
Expose async-trait to  as a feature which can be enabled or disabled selectively
Resolves #309 

This is really a no-op change except assuring that all of the test coverage runs for both `async-trait` and no-`async-trait` usages.

Patch:
diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml
index e61929f..80767a8 100644
--- a/.github/workflows/ci.yaml
+++ b/.github/workflows/ci.yaml
@@ -29,9 +29,12 @@ jobs:
           - name: Test ractor with the `blanket_serde` feature
             package: ractor
             flags: -F blanket_serde
-          - name: Test ractor_cluster
+          - name: Test ractor_cluster with native async traits
             package: ractor_cluster
             # flags: 
+          - name: Test ractor_cluster with async_trait
+            package: ractor_cluster
+            flags: -F async-trait
             
     steps:
       - uses: actions/checkout@main
diff --git a/.github/workflows/integration.yaml b/.github/workflows/integration.yaml
index 8aebe4d..c29ad11 100644
--- a/.github/workflows/integration.yaml
+++ b/.github/workflows/integration.yaml
@@ -12,15 +12,14 @@ on:
 
 jobs:
   test:
-    name: Test ractor_cluster with Docker based networked images
-    runs-on: ${{matrix.os}}-latest
+    name: Test networked cluster
+    runs-on: ubuntu-latest
     strategy:
       fail-fast: false
       matrix:
-        toolchain: [stable]
-        os: [ubuntu]
-        features: [blanket_serde, cluster]
-
+        features:
+          - 'blanket_serde,cluster'
+          - 'blanket_serde,cluster,async-trait'
     steps:
       - uses: actions/checkout@main
 
diff --git a/ractor/Cargo.toml b/ractor/Cargo.toml
index 2201188..80d14a6 100644
--- a/ractor/Cargo.toml
+++ b/ractor/Cargo.toml
@@ -22,8 +22,9 @@ cluster = []
 message_span_propogation = []
 tokio_runtime = ["tokio/time", "tokio/rt", "tokio/macros", "tokio/tracing"]
 blanket_serde = ["serde", "pot", "cluster"]
+async-trait = ["dep:async-trait"]
 
-default = ["tokio_runtime", "async-trait", "message_span_propogation"]
+default = ["tokio_runtime", "message_span_propogation"]
 
 [dependencies]
 ## Required dependencies
diff --git a/ractor/src/actor/mod.rs b/ractor/src/actor/mod.rs
index 51cdea3..e5ad6a8 100644
--- a/ractor/src/actor/mod.rs
+++ b/ractor/src/actor/mod.rs
@@ -133,7 +133,7 @@ pub trait Actor: Sized + Sync + Send + 'static {
     ///
     /// * `myself` - A handle to the [ActorCell] representing this actor
     /// * `args` - Arguments that are passed in the spawning of the actor which might
-    /// be necessary to construct the initial state
+    ///   be necessary to construct the initial state
     ///
     /// Returns an initial [Actor::State] to bootstrap the actor
     #[cfg(not(feature = "async-trait"))]
@@ -357,7 +357,7 @@ pub trait Actor: Sized + Sync + Send + 'static {
     /// * `name`: A name to give the actor. Useful for global referencing or debug printing
     /// * `handler` The implementation of Self
     /// * `startup_args`: Arguments passed to the `pre_start` call of the [Actor] to facilitate startup and
-    /// initial state creation
+    ///   initial state creation
     ///
     /// Returns a [Ok((ActorRef, JoinHandle<()>))] upon successful start, denoting the actor reference
     /// along with the join handle which will complete when the actor terminates. Returns [Err(SpawnErr)] if
@@ -375,7 +375,7 @@ pub trait Actor: Sized + Sync + Send + 'static {
     /// * `name`: A name to give the actor. Useful for global referencing or debug printing
     /// * `handler` The implementation of Self
     /// * `startup_args`: Arguments passed to the `pre_start` call of the [Actor] to facilitate startup and
-    /// initial state creation
+    ///   initial state creation
     ///
     /// Returns a [Ok((ActorRef, JoinHandle<()>))] upon successful start, denoting the actor reference
     /// along with the join handle which will complete when the actor terminates. Returns [Err(SpawnErr)] if
@@ -394,7 +394,7 @@ pub trait Actor: Sized + Sync + Send + 'static {
     /// * `name`: A name to give the actor. Useful for global referencing or debug printing
     /// * `handler` The implementation of Self
     /// * `startup_args`: Arguments passed to the `pre_start` call of the [Actor] to facilitate startup and
-    /// initial state creation
+    ///   initial state creation
     /// * `supervisor`: The [ActorCell] which is to become the supervisor (parent) of this actor
     ///
     /// Returns a [Ok((ActorRef, JoinHandle<()>))] upon successful start, denoting the actor reference
diff --git a/ractor/src/factory/job.rs b/ractor/src/factory/job.rs
index b5407ac..666fd7f 100644
--- a/ractor/src/factory/job.rs
+++ b/ractor/src/factory/job.rs
@@ -429,7 +429,7 @@ impl MessageRetryStrategy {
 /// 1. Regular loadshed events will cause this message to be retried if there is still retries left
 ///    and the job isn't expired unless you explicitely call `completed()` in the discard handler.
 /// 2. Consumable types are not well supported here without some wrapping in Option types, which is
-///    because the value is handled everywhere as `&mut ref`` due to the drop implementation requiring that
+///    because the value is handled everywhere as `&mut ref` due to the drop implementation requiring that
 ///    it be so. This means that RPCs using [crate::concurrency::oneshot]s likely won't work without
 ///    some real painful ergonomics.
 /// 3. Upon successful handling of the job, you need to mark it as `completed()` at the end of your
diff --git a/ractor/src/factory/worker.rs b/ractor/src/factory/worker.rs
index 2d58cd1..5d8526b 100644
--- a/ractor/src/factory/worker.rs
+++ b/ractor/src/factory/worker.rs
@@ -74,7 +74,7 @@ pub trait Worker: Send + Sync + 'static {
     /// * `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
+    ///   necessary to construct the initial state
     ///
     /// Returns an initial [Worker::State] to bootstrap the actor
     #[cfg(not(feature = "async-trait"))]
@@ -310,7 +310,6 @@ where
     >;
     type State = WorkerState<Self>;
 
-    #[cfg(feature = "async-trait")]
     async fn pre_start(
         &self,
         _: ActorRef<Self::Msg>,
@@ -328,39 +327,6 @@ where
         })
     }
 
-    #[cfg(not(feature = "async-trait"))]
-    fn pre_start(
-        &self,
-        _: ActorRef<Self::Msg>,
-        WorkerStartContext {
-            wid,
-            factory,
-            custom_start,
-        }: Self::Arguments,
-    ) -> impl Future<Output = Result<Self::State, ActorProcessingErr>> + Send {
-        async move {
-            let inner_state =
-                <Self as Worker>::pre_start(&self, wid, &factory, custom_start).await?;
-            Ok(Self::State {
-                wid,
-                factory,
-                state: inner_state,
-            })
-        }
-    }
-
-    #[cfg(not(feature = "async-trait"))]
-    fn post_start(
-        &self,
-        _: ActorRef<Self::Msg>,
-        state: &mut Self::State,
-    ) -> impl Future<Output = Result<(), ActorProcessingErr>> + Send {
-        async move {
-            <Self as Worker>::post_start(&self, state.wid, &state.factory, &mut state.state).await
-        }
-    }
-
-    #[cfg(feature = "async-trait")]
     async fn post_start(
         &self,
         _: ActorRef<Self::Msg>,
@@ -369,18 +335,6 @@ where
         <Self as Worker>::post_start(self, state.wid, &state.factory, &mut state.state).await
     }
 
-    #[cfg(not(feature = "async-trait"))]
-    fn post_stop(
-        &self,
-        _: ActorRef<Self::Msg>,
-        state: &mut Self::State,
-    ) -> impl Future<Output = Result<(), ActorProcessingErr>> + Send {
-        async move {
-            <Self as Worker>::post_stop(&self, state.wid, &state.factory, &mut state.state).await
-        }
-    }
-
-    #[cfg(feature = "async-trait")]
     async fn post_stop(
         &self,
         _: ActorRef<Self::Msg>,
@@ -389,53 +343,6 @@ where
         <Self as Worker>::post_stop(self, state.wid, &state.factory, &mut state.state).await
     }
 
-    #[cfg(not(feature = "async-trait"))]
-    fn handle(
-        &self,
-        _: ActorRef<Self::Msg>,
-        message: Self::Msg,
-        state: &mut Self::State,
-    ) -> impl Future<Output = Result<(), ActorProcessingErr>> + Send {
-        async move {
-            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.take_span() {
-                        <Self as Worker>::handle(
-                            &self,
-                            state.wid,
-                            &state.factory,
-                            job,
-                            &mut state.state,
-                        )
-                        .instrument(span)
-                        .await
-                    } else {
-                        <Self as Worker>::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<Self::Msg>,
@@ -468,20 +375,6 @@ where
         }
     }
 
-    #[cfg(not(feature = "async-trait"))]
-    fn handle_supervisor_evt(
-        &self,
-        myself: ActorRef<Self::Msg>,
-        message: SupervisionEvent,
-        state: &mut Self::State,
-    ) -> impl Future<Output = Result<(), ActorProcessingErr>> + Send {
-        async move {
-            <Self as Worker>::handle_supervisor_evt(&self, myself.into(), message, &mut state.state)
-                .await
-        }
-    }
-
-    #[cfg(feature = "async-trait")]
     async fn handle_supervisor_evt(
         &self,
         myself: ActorRef<Self::Msg>,
diff --git a/ractor_cluster/Cargo.toml b/ractor_cluster/Cargo.toml
index f69d588..722210b 100644
--- a/ractor_cluster/Cargo.toml
+++ b/ractor_cluster/Cargo.toml
@@ -14,6 +14,11 @@ categories = ["asynchronous"]
 build = "src/build.rs"
 rust-version = "1.64"
 
+[features]
+async-trait = ["dep:async-trait", "ractor/async-trait"]
+
+default = []
+
 [build-dependencies]
 # protobuf-src = "2"
 protoc-bin-vendored = "3"
@@ -24,13 +29,15 @@ prost-build = { version = "0.13" }
 bytes = { version = "1" }
 prost = { version = "0.13" }
 prost-types = { version = "0.13" }
-ractor = { version = "0.14.0", features = ["cluster"], path = "../ractor" }
+ractor = { version = "0.14.0", default-features = false, features = ["tokio_runtime", "message_span_propogation", "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"]}
 tokio-rustls = { version = "0.26" }
 tracing = "0.1"
+## Optional dependencies
+async-trait = { version = "0.1", optional = true }
 
 [dev-dependencies]
 tokio = { version = "1.30", features = ["rt", "time", "sync", "macros", "net", "io-util", "rt-multi-thread"] }
diff --git a/ractor_cluster/src/lib.rs b/ractor_cluster/src/lib.rs
index c22eceb..3f6f879 100644
--- a/ractor_cluster/src/lib.rs
+++ b/ractor_cluster/src/lib.rs
@@ -60,6 +60,10 @@ pub mod node;
 /// Node's are representing by an integer id
 pub type NodeId = u64;
 
+// Satisfy dependencies transitively imported
+#[cfg(feature = "async-trait")]
+use async_trait as _;
+
 // ============== Re-exports ============== //
 pub use net::{IncomingEncryptionMode, NetworkStream};
 pub use node::client::connect as client_connect;
diff --git a/ractor_cluster/src/net/listener.rs b/ractor_cluster/src/net/listener.rs
index 29a2015..85530fb 100644
--- a/ractor_cluster/src/net/listener.rs
+++ b/ractor_cluster/src/net/listener.rs
@@ -46,7 +46,7 @@ pub struct ListenerState {
 #[derive(crate::RactorMessage)]
 pub struct ListenerMessage;
 
-#[ractor::async_trait]
+#[cfg_attr(feature = "async-trait", ractor::async_trait)]
 impl Actor for Listener {
     type Msg = ListenerMessage;
     type Arguments = ();
diff --git a/ractor_cluster/src/net/session.rs b/ractor_cluster/src/net/session.rs
index 9b91f7a..5856163 100644
--- a/ractor_cluster/src/net/session.rs
+++ b/ractor_cluster/src/net/session.rs
@@ -107,7 +107,7 @@ pub struct SessionState {
     reader: ActorRef<SessionReaderMessage>,
 }
 
-#[ractor::async_trait]
+#[cfg_attr(feature = "async-trait", ractor::async_trait)]
 impl Actor for Session {
     type Msg = SessionMessage;
     type Arguments = super::NetworkStream;
@@ -296,7 +296,7 @@ enum SessionWriterMessage {
     WriteObject(crate::protocol::NetworkMessage),
 }
 
-#[ractor::async_trait]
+#[cfg_attr(feature = "async-trait", ractor::async_trait)]
 impl Actor for SessionWriter {
     type Msg = SessionWriterMessage;
     type Arguments = ActorWriteHalf;
@@ -385,7 +385,7 @@ struct SessionReaderState {
     reader: Option<ActorReadHalf>,
 }
 
-#[ractor::async_trait]
+#[cfg_attr(feature = "async-trait", ractor::async_trait)]
 impl Actor for SessionReader {
     type Msg = SessionReaderMessage;
     type Arguments = ActorReadHalf;
diff --git a/ractor_cluster/src/node/mod.rs b/ractor_cluster/src/node/mod.rs
index 4be74ad..923e277 100644
--- a/ractor_cluster/src/node/mod.rs
+++ b/ractor_cluster/src/node/mod.rs
@@ -317,7 +317,7 @@ impl NodeServerState {
     }
 }
 
-#[ractor::async_trait]
+#[cfg_attr(feature = "async-trait", ractor::async_trait)]
 impl Actor for NodeServer {
     type Msg = NodeServerMessage;
     type State = NodeServerState;
diff --git a/ractor_cluster/src/node/node_session/mod.rs b/ractor_cluster/src/node/node_session/mod.rs
index 46236e3..7218442 100644
--- a/ractor_cluster/src/node/node_session/mod.rs
+++ b/ractor_cluster/src/node/node_session/mod.rs
@@ -827,7 +827,7 @@ impl NodeSessionState {
     }
 }
 
-#[ractor::async_trait]
+#[cfg_attr(feature = "async-trait", ractor::async_trait)]
 impl Actor for NodeSession {
     type Msg = super::NodeSessionMessage;
     type Arguments = crate::net::NetworkStream;
diff --git a/ractor_cluster/src/remote_actor/mod.rs b/ractor_cluster/src/remote_actor/mod.rs
index 8137db8..541c6aa 100644
--- a/ractor_cluster/src/remote_actor/mod.rs
+++ b/ractor_cluster/src/remote_actor/mod.rs
@@ -78,7 +78,7 @@ impl RemoteActorState {
 #[derive(RactorMessage)]
 pub(crate) struct RemoteActorMessage;
 
-#[ractor::async_trait]
+#[cfg_attr(feature = "async-trait", ractor::async_trait)]
 impl Actor for RemoteActor {
     type Msg = RemoteActorMessage;
     type State = RemoteActorState;
diff --git a/ractor_playground/src/distributed.rs b/ractor_playground/src/distributed.rs
index e719ccd..39d80c2 100644
--- a/ractor_playground/src/distributed.rs
+++ b/ractor_playground/src/distributed.rs
@@ -76,7 +76,6 @@ enum PingPongActorMessage {
     Rpc(String, RpcReplyPort<String>),
 }
 
-#[ractor::async_trait]
 impl Actor for PingPongActor {
     type Msg = PingPongActorMessage;
     type State = ();
diff --git a/ractor_playground/src/ping_pong.rs b/ractor_playground/src/ping_pong.rs
index 1ed7778..3d66615 100644
--- a/ractor_playground/src/ping_pong.rs
+++ b/ractor_playground/src/ping_pong.rs
@@ -32,7 +32,6 @@ impl Message {
     }
 }
 
-#[ractor::async_trait]
 impl Actor for PingPong {
     type Msg = Message;
     type Arguments = ();


Test patch:
diff --git a/ractor/src/actor/tests/mod.rs b/ractor/src/actor/tests/mod.rs
index 53afa6d..225df0e 100644
--- a/ractor/src/actor/tests/mod.rs
+++ b/ractor/src/actor/tests/mod.rs
@@ -1036,7 +1036,7 @@ async fn actor_post_stop_executed_before_stop_and_wait_returns() {
             _: &mut Self::State,
         ) -> Result<(), ActorProcessingErr> {
             sleep(Duration::from_millis(1000)).await;
-            let _ = self.signal.store(1, Ordering::SeqCst);
+            self.signal.store(1, Ordering::SeqCst);
             Ok(())
         }
     }
diff --git a/ractor_cluster/src/node/node_session/tests.rs b/ractor_cluster/src/node/node_session/tests.rs
index 990028f..0160b10 100644
--- a/ractor_cluster/src/node/node_session/tests.rs
+++ b/ractor_cluster/src/node/node_session/tests.rs
@@ -18,7 +18,7 @@ use crate::NodeSessionMessage;
 use super::*;
 
 struct DummyNodeServer;
-#[ractor::async_trait]
+#[cfg_attr(feature = "async-trait", ractor::async_trait)]
 impl Actor for DummyNodeServer {
     type Msg = crate::node::NodeServerMessage;
     type State = ();
@@ -57,7 +57,7 @@ impl Actor for DummyNodeServer {
 }
 
 struct DummyNodeSession;
-#[ractor::async_trait]
+#[cfg_attr(feature = "async-trait", ractor::async_trait)]
 impl Actor for DummyNodeSession {
     type Msg = crate::node::NodeSessionMessage;
     type State = ();
@@ -540,7 +540,7 @@ async fn node_session_handle_node_msg() {
         call_replies: Arc<AtomicU8>,
     }
 
-    #[ractor::async_trait]
+    #[cfg_attr(feature = "async-trait", ractor::async_trait)]
     impl Actor for DummyRemoteActor {
         type Msg = crate::remote_actor::RemoteActorMessage;
         type State = ();
diff --git a/ractor_cluster/src/remote_actor/tests.rs b/ractor_cluster/src/remote_actor/tests.rs
index 39b8ec6..62a75ce 100644
--- a/ractor_cluster/src/remote_actor/tests.rs
+++ b/ractor_cluster/src/remote_actor/tests.rs
@@ -17,7 +17,7 @@ impl FakeNodeSession {
     }
 }
 
-#[ractor::async_trait]
+#[cfg_attr(feature = "async-trait", ractor::async_trait)]
 impl Actor for FakeNodeSession {
     type Msg = crate::node::NodeSessionMessage;
     type State = ();
diff --git a/ractor_cluster_integration_tests/Cargo.toml b/ractor_cluster_integration_tests/Cargo.toml
index ab927c7..37f0da5 100644
--- a/ractor_cluster_integration_tests/Cargo.toml
+++ b/ractor_cluster_integration_tests/Cargo.toml
@@ -13,12 +13,15 @@ publish = false
 [features]
 blanket_serde = ["ractor/blanket_serde"]
 cluster = []
+async-trait = ["ractor_cluster/async-trait"]
+
+default = []
 
 [dependencies]
 anyhow = "1"
 async-trait = "0.1"
 clap = { version = "4", features = ["derive"] }
-ractor = { path = "../ractor" }
+ractor = { default-features = false, features = ["tokio_runtime", "message_span_propogation", "cluster"], path = "../ractor" }
 ractor_cluster = { path = "../ractor_cluster" }
 rand = "0.8"
 tokio-rustls = { version = "0.26" }
diff --git a/ractor_cluster_integration_tests/src/tests/pg_groups.rs b/ractor_cluster_integration_tests/src/tests/pg_groups.rs
index c77abba..7908d35 100644
--- a/ractor_cluster_integration_tests/src/tests/pg_groups.rs
+++ b/ractor_cluster_integration_tests/src/tests/pg_groups.rs
@@ -37,7 +37,7 @@ struct HelloActorState {
     done: bool,
 }
 
-#[ractor::async_trait]
+#[cfg_attr(feature = "async-trait", ractor::async_trait)]
 impl Actor for HelloActor {
     type Msg = HelloActorMessage;
     type State = HelloActorState;