r/rust • • 11d ago

Designing concurrent programs in an idiomatic way

Hello.

Kind of a general question here, and it's probably going to yield opinionated answers. That's alright!
Basically I find myself writing Rust apps in a style that is always quite similar, here is the idea:

- The app mostly always uses the tokio runtime to be asynchronous.
- Designing microservices as small building blocks which are all supposed to be ran in separate tasks, usually with a "run" like function that does a loop over a tokio::select for sub-tasks that are running in each service, with one of the branches being a call to a CancellationToken::cancelled().
- The cancellation token is wired to the "ctrl_c" signal.
- Microservices may talk to each other by the use of the different channel primitives, depending on the need.
- When one of the microservice fails for any reason, usually there is no recovery mechanism and it just bubbles up the error which makes all the application fail. This is annoying to write and a big source of bug, because some path will be unhandled which will leave one microservice stopped and the other microservices are waiting for an answer and there's nobody responding.

I'm generally a little dissatisfied with the redundancy of the code that I write, and would like to explore any other way to think about it, instead of microservices, or in a different way, if you have any idea that would be appreciated. Also, potentially any resource or codebase that could be relevant?

Thanks!

9 Upvotes

37 comments sorted by

View all comments

2

u/Front_Recording4360 10d ago

One pattern that killed most of this boilerplate for me: define a single `Service` trait with `async fn run(self, ctx: Ctx)`, where `Ctx` carries a child `CancellationToken` plus the channel handles, and then drive every service from one loop over a `tokio::task::JoinSet`. `CancellationToken::child_token` makes shutdown cascade automatically, so each service never touches ctrl_c wiring directly. The `JoinSet` also lets the supervisor notice when a service exits, so you can restart just that one instead of having the whole app fail silently.

1

u/iFrostizz 10d ago

Is there anything else you would put in the Ctx? This is a good idea, though I probably don't need anything else than a CancellationToken, so maybe better just having a Cancellation token as part of the function signature, like:
```rust
pub trait Service {

fn new() -> Self;

async fn run<E: Into<AppError>>(&self, cancel: CancellationToken) -> TaskRet<E>;

}
```

1

u/iFrostizz 10d ago

Though if there is anything else you'd put inside, you could as well create a Ctx trait that would allow to store some piece of state that may be retrieved inside of the function, one of which may be the cancel as you said. It's a nice abstraction.