My question is mostly about design, architecture and best practices.
My code actually compiles and does what I want but what was meant as an easy way for my users to plug in to existing functionality turns out to be not so easy to use anymore.
I've tried to reproduce a smaller example below but my code is fully open-source on Github.
My background is Java (only a few months on Rust) so what I've tried to replicate is the Abstract class pattern from Java. I'd usually create an abstract "base" class with common functionality and subclasses can extend this base class and just implement the abstract methods with their business logic.
This does not work in Rust so I've tried the Strategy pattern instead.
- I have a
Controller
(struct) that processes events and it takes aStrategy
(trait) - My
Controller
calls a method (init_event
) on theStrategy
for each event that comes in, this method returns a struct implementing thePerEvent
trait - The
Controller
calls the methodoperations
on thePerEvent
object which returns aVec
of Futures. - The
Controller
now iterates over these Futures and awaits each, the result of one of these Futures determines whether the next Futures should be run at all or not
My goals for this design are:
- A bunch of independent functions which all have access to the same shared state per event (the
PerEvent
object), parts of which might be mutable, so these functions can build on each others work - These functions are run in order and they can influence whether the next ones are run or not
- No need to pass around the state manually and return it every time
My problems are:
- I need to treat all state etc. as if it were run in parallel (Arc / Mutex) while in reality they are always run serially (I understand that the compiler can't know this)
- "Unpacking" an
Arc<Mutex<>>
isn't easy (unless I'm doing it wrong), it's certainly overhead - Even when using
Arc<Mutex<>>
I run into issues: Uncomment the//self.nothing().await;
line and it'll fail because theMutexGuard
is notSend
and would be held over anawait
, I can work around that but again it's not super trivial to use - Function signatures are not trivial to read
I tried to make it easier for my users but I have a feeling that my architecture doesn't achieve that goal.
I admit that this is not trivial and I'm probably not doing the best job of explaining my issue but I'm still wondering if anyone might have an idea on how this whole architecture could be improved?
Thank you!
use core::future::Future;
use core::pin::Pin;
use std::sync::{Arc, Mutex};
pub enum ReturnAction {
Continue,
Done,
}
trait PerEvent {
fn operations(&self) -> Vec<Pin<Box<dyn Future<Output = ReturnAction> + Send + '_>>>;
}
struct TestPerEvent {
event: String,
some_state: Option<String>,
more_state: Arc<Mutex<Option<String>>>,
}
impl TestPerEvent {
async fn test1(&self) -> ReturnAction {
let t1 = Arc::clone(&self.more_state);
let mut t2 = t1.lock().unwrap();
*t2 = Some("foobar".to_string());
println!("{:?}", t2);
ReturnAction::Continue
}
async fn test2(&self) -> ReturnAction {
let t1 = Arc::clone(&self.more_state);
let t2 = t1.lock().unwrap();
println!("{:?}", t2);
//self.nothing().await;
ReturnAction::Continue
}
async fn nothing(&self) {}
}
impl PerEvent for TestPerEvent {
fn operations(&self) -> Vec<Pin<Box<dyn Future<Output = ReturnAction> + Send + '_>>> {
let vec: Vec<Pin<Box<dyn Future<Output = ReturnAction> + Send + '_>>> =
vec![Box::pin(self.test1()), Box::pin(self.test2())];
vec
}
}
trait Strategy<T: PerEvent> {
fn init_event(&self, event: String) -> T;
}
struct TestStrategy {}
impl Strategy<TestPerEvent> for TestStrategy {
fn init_event(&self, event: String) -> TestPerEvent {
TestPerEvent {
event,
some_state: None,
more_state: Arc::new(Mutex::new(None)),
}
}
}
struct Controller {}
impl Controller {
pub async fn process_event<T: PerEvent>(&self, strategy: impl Strategy<T>, event: String) {
let event_processor = strategy.init_event(event);
let operations = event_processor.operations();
for operation in operations {
let result = operation.await;
if let ReturnAction::Done = result {
return;
}
}
}
}
#[tokio::main]
async fn main() {
let strategy = TestStrategy {};
let controller = Controller {};
println!("Before");
tokio::spawn(async move {
controller
.process_event(strategy, "foobar".to_string())
.await
});
println!("After");
}
Output:
Before
After
Some("foobar")
Some("foobar")
Errors if that one line is uncommented
Compiling playground v0.0.1 (/playground)
error: future cannot be sent between threads safely
--> src/main.rs:45:42
|
45 | vec![Box::pin(self.test1()), Box::pin(self.test2())];
| ^^^^^^^^^^^^^^^^^^^^^^ future returned by `test2` is not `Send`
|
= help: within `impl Future`, the trait `Send` is not implemented for `std::sync::MutexGuard<'_, Option<String>>`
note: future is not `Send` as this value is used across an await
--> src/main.rs:35:9
|
31 | let t2 = t1.lock().unwrap();
| -- has type `std::sync::MutexGuard<'_, Option<String>>` which is not `Send`
...
35 | self.nothing().await;
| ^^^^^^^^^^^^^^^^^^^^ await occurs here, with `t2` maybe used later
...
38 | }
| - `t2` is later dropped here
= note: required for the cast to the object type `dyn Future<Output = ReturnAction> + Send`
error: aborting due to previous error
error: could not compile `playground`
To learn more, run the command again with --verbose.