Skip to content

Commit 123ae31

Browse files
committed
Readme updated
1 parent b34c21d commit 123ae31

File tree

1 file changed

+84
-18
lines changed

1 file changed

+84
-18
lines changed

README.md

Lines changed: 84 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -430,14 +430,63 @@ Pushing these decisions from the core domain model is very valuable. Being able
430430

431431
## Fearless Concurrency
432432

433+
Concurrency and async programming do not require a multi-threaded environment. You can run async tasks on a single-threaded executor as well.
434+
435+
`fmodel-rust` lets you choose between **multi-threaded async** and **single-threaded async** via a feature flag.
436+
437+
438+
| Single-threaded world | Multi-threaded world |
439+
| --------------------- | ---------------------------------- |
440+
| `Rc<T>` | `Arc<T>` |
441+
| `RefCell<T>` | `Mutex<T>` / `RwLock<T>` |
442+
| `Rc<RefCell<T>>` | `Arc<Mutex<T>>` / `Arc<RwLock<T>>` |
443+
444+
433445
### `Send` bound futures/Async (multi-threaded executors)
434446

435-
Splitting the computation in your program into multiple threads to run multiple tasks at the same time can improve performance.
436-
However, programming with threads has a reputation for being difficult. Rust’s type system and ownership model guarantee thread safety.
447+
```toml
448+
[dependencies]
449+
fmodel-rust = { version = "0.8.2" }
450+
```
451+
452+
If you don’t enable the feature, the **default** mode requires `Send` so your futures can safely hop between threads:
453+
454+
```rust
455+
#[cfg(not(feature = "not-send-futures"))]
456+
pub trait EventRepository<C, E, Version, Error> {
457+
/// Fetches current events, based on the command.
458+
/// Desugared `async fn fetch_events(&self, command: &C) -> Result<Vec<(E, Version)>, Error>;` to a normal `fn` that returns `impl Future`, and adds bound `Send`.
459+
/// You can freely move between the `async fn` and `-> impl Future` spelling in your traits and impls. This is true even when one form has a Send bound.
460+
fn fetch_events(
461+
&self,
462+
command: &C,
463+
) -> impl Future<Output = Result<Vec<(E, Version)>, Error>> + Send;
464+
/// Saves events.
465+
/// Desugared `async fn save(&self, events: &[E], latest_version: &Option<Version>) -> Result<Vec<(E, Version)>, Error>;` to a normal `fn` that returns `impl Future`, and adds bound `Send`
466+
/// You can freely move between the `async fn` and `-> impl Future` spelling in your traits and impls. This is true even when one form has a Send bound.
467+
fn save(&self, events: &[E]) -> impl Future<Output = Result<Vec<(E, Version)>, Error>> + Send;
468+
469+
/// Version provider. It is used to provide the version/sequence of the stream to wich this event belongs to. Optimistic locking is useing this version to check if the event is already saved.
470+
/// Desugared `async fn version_provider(&self, event: &E) -> Result<Option<Version>, Error>;` to a normal `fn` that returns `impl Future`, and adds bound `Send`
471+
/// You can freely move between the `async fn` and `-> impl Future` spelling in your traits and impls. This is true even when one form has a Send bound.
472+
fn version_provider(
473+
&self,
474+
event: &E,
475+
) -> impl Future<Output = Result<Option<Version>, Error>> + Send;
476+
}
477+
```
478+
479+
This mode is designed for multi-threaded runtimes like `tokio`’s default executor, where futures may be scheduled on any worker thread.
480+
Here, you typically wrap shared state in `Arc<Mutex<T>>` or `Arc<RwLock<T>>`.
437481

438-
Example of the concurrent execution of the aggregate in multi-threaded environment (**default** - `Send`-bound futures):
482+
483+
Example of the concurrent execution of the aggregate in multi-threaded environment (`Send` bound futures):
439484

440485
```rust
486+
struct InMemoryOrderEventRepository {
487+
events: RwLock<Vec<(OrderEvent, i32)>>,
488+
}
489+
441490
async fn es_test() {
442491
let repository = InMemoryOrderEventRepository::new();
443492
let aggregate = Arc::new(EventSourcedAggregate::new(
@@ -474,30 +523,47 @@ async fn es_test() {
474523

475524
### `Send` free futures/Async (single-threaded executors)
476525

477-
Concurrency and async programming do not require a multi-threaded environment. You can run async tasks on a single-threaded executor, which allows you to write async code without the Send bound.
478-
479-
This approach has several benefits:
480-
481-
- Simpler code: No need for Arc, Mutex(RwLock), or other thread synchronization primitives for shared state.
526+
```toml
527+
[dependencies]
528+
fmodel-rust = { version = "0.8.2", features = ["not-send-futures"] }
529+
```
482530

483-
- Ergonomic references: You can freely use references within your async code without worrying about moving data across threads. 🤯
531+
This mode removes the `Send` bound from async traits.
532+
It works well with single-threaded runtimes (`tokio::task::LocalSet`) and allows using lighter primitives like `Rc<RefCell<T>>`.
484533

485-
- Efficient design: This model aligns with the “Thread-per-Core” pattern, letting you safely run multiple async tasks concurrently on a single thread.
534+
```rust
535+
#[cfg(feature = "not-send-futures")]
536+
pub trait EventRepository<C, E, Version, Error> {
537+
/// Fetches current events, based on the command.
538+
/// Desugared `async fn fetch_events(&self, command: &C) -> Result<Vec<(E, Version)>, Error>;` to a normal `fn` that returns `impl Future`.
539+
/// You can freely move between the `async fn` and `-> impl Future` spelling in your traits and impls.
540+
fn fetch_events(&self, command: &C) -> impl Future<Output = Result<Vec<(E, Version)>, Error>>;
541+
/// Saves events.
542+
/// Desugared `async fn save(&self, events: &[E], latest_version: &Option<Version>) -> Result<Vec<(E, Version)>, Error>;` to a normal `fn` that returns `impl Future`
543+
/// You can freely move between the `async fn` and `-> impl Future` spelling in your traits and impls.
544+
fn save(&self, events: &[E]) -> impl Future<Output = Result<Vec<(E, Version)>, Error>>;
545+
546+
/// Version provider. It is used to provide the version/sequence of the stream to wich this event belongs to. Optimistic locking is useing this version to check if the event is already saved.
547+
/// Desugared `async fn version_provider(&self, event: &E) -> Result<Option<Version>, Error>;` to a normal `fn` that returns `impl Future`
548+
/// You can freely move between the `async fn` and `-> impl Future` spelling in your traits and impls.
549+
fn version_provider(&self, event: &E) -> impl Future<Output = Result<Option<Version>, Error>>;
550+
}
551+
```
486552

487-
In short: you get all the power of async/await without the complexity of multi-threaded synchronization all the time.
553+
This approach has several benefits:
488554

489-
Just switching to a [LocalExecutor](https://docs.rs/async-executor/latest/async_executor/struct.LocalExecutor.html) or something like Tokio [LocalSet](https://docs.rs/tokio/latest/tokio/task/struct.LocalSet.html) should be enough.
555+
- Simpler code: No need for `Arc`, `Mutex/RwLock`, or other expensive thread synchronization primitives.
490556

491-
If you want to enable single-threaded, Send-free async support, you can enable the optional feature `not-send-futures` when adding fmodel-rust to your project:
557+
- Efficient design: This model aligns with the “Thread-per-Core” pattern, letting you safely run multiple async tasks concurrently on a single thread.
492558

493-
```toml
494-
[dependencies]
495-
fmodel-rust = { version = "0.8.2", features = ["not-send-futures"] }
496-
```
497559

498-
Example of the concurrent execution of the aggregate in single-threaded environment (**behind feature** - `Send` free `Futures`):
560+
Example of the concurrent execution of the aggregate in single-threaded environment (`Send` free `Futures`):
499561

500562
```rust
563+
struct InMemoryOrderEventRepository {
564+
events: RefCell<Vec<(OrderEvent, i32)>>,
565+
}
566+
501567
async fn es_test_not_send() {
502568
let repository = InMemoryOrderEventRepository::new();
503569

0 commit comments

Comments
 (0)