Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

LockedLoad #194

Merged
merged 4 commits into from
May 8, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
21 changes: 14 additions & 7 deletions src/manager.rs
Original file line number Diff line number Diff line change
@@ -1,3 +1,7 @@
mod locked_load;

pub use locked_load::LockedLoad;

use uuid::Uuid;

use crate::store::{EventStore, StoreEvent};
Expand Down Expand Up @@ -71,19 +75,22 @@ where
/// Acquires a lock on this aggregate instance, and only then loads it from the event store,
/// by applying previously persisted events onto the aggregate state by order of their sequence number.
///
/// The lock is contained in the returned `AggregateState`, and released when this is dropped.
/// It can also be extracted with the `take_lock` method for more advanced uses.
/// The returned [`LockedLoad`] contains the outcome of the load and is responsible for correctly managing the lock.
pub async fn lock_and_load(
&self,
aggregate_id: impl Into<Uuid> + Send,
) -> Result<Option<AggregateState<<E::Aggregate as Aggregate>::State>>, E::Error> {
) -> Result<LockedLoad<<E::Aggregate as Aggregate>::State>, E::Error> {
let id = aggregate_id.into();
let guard = self.event_store.lock(id).await?;
let aggregate_state = self.load(id).await?;

Ok(self.load(id).await?.map(|mut state| {
state.set_lock(guard);
state
}))
Ok(match aggregate_state {
Some(mut aggregate_state) => {
aggregate_state.set_lock(guard);
LockedLoad::some(aggregate_state)
}
None => LockedLoad::none(id, guard),
})
}

/// `delete` should either complete the aggregate instance, along with all its associated events
Expand Down
67 changes: 67 additions & 0 deletions src/manager/locked_load.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
use uuid::Uuid;

use crate::store::EventStoreLockGuard;
use crate::AggregateState;

/// The outcome of [`crate::manager::AggregateManager::lock_and_load`], akin to `Option<AggregateState<_>>`.
///
/// it contains the loaded [`AggregateState`] if found,
/// and ensures that the lock is preserved in all cases.
///
/// Releases the lock on drop, unless it gets transferred through its API.
pub struct LockedLoad<T>(LockedLoadInner<T>);

impl<T> LockedLoad<T> {
/// Constructs a new instance from an AggregateState.
/// It should contain its own lock.
pub fn some(aggregate_state: AggregateState<T>) -> Self {
Self(LockedLoadInner::Some(aggregate_state))
}

/// Constructs a new instance to hold the lock for an empty state.
pub fn none(id: Uuid, lock: EventStoreLockGuard) -> Self {
Self(LockedLoadInner::None { id, lock })
}

/// Checks if the AggregateState was found.
pub fn is_some(&self) -> bool {
matches!(self, Self(LockedLoadInner::Some(_)))
}

/// Extracts the contained AggregateState, or panics otherwise.
pub fn unwrap(self) -> AggregateState<T> {
let Self(inner) = self;
match inner {
LockedLoadInner::None { .. } => None,
LockedLoadInner::Some(aggregate_state) => Some(aggregate_state),
}
.unwrap()
}
}

impl<T> LockedLoad<T>
where
T: Default,
{
/// Extracts the contained AggregateState, otherwise returns a new one
/// with the default internal state together with the lock.
pub fn unwrap_or_default(self) -> AggregateState<T> {
let Self(inner) = self;
match inner {
LockedLoadInner::None { id, lock } => {
let mut aggregate_state = AggregateState::with_id(id);
aggregate_state.set_lock(lock);
aggregate_state
}
LockedLoadInner::Some(aggregate_state) => aggregate_state,
}
}
}

/// Encapsulates the logic to avoid exposing the internal behaviour.
/// It's essentially `Option<AggregateState<T>>`, but it also needs to
/// retain the lock information.
enum LockedLoadInner<T> {
None { id: Uuid, lock: EventStoreLockGuard },
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why do this need to have an id?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Because the use case is:

  • you do lock_and_load(ID)
  • you get Empty { ID, ... }
  • you do .unwrap_or_default which gives you AggregateState<T>::default().with_id(ID).with_lock(lock)

TLDR: LockedLoad is the result of trying lock_and_load on a specific ID.

Copy link
Contributor Author

@angelo-rendina-prima angelo-rendina-prima May 3, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I guess there can be a scenario where you do

result = lock_and_load(123)
if result is None
  result = AggregateState::new() // <-- completely new ID, say 456

so we likely want LockedLoad::is_none, LockedLoad::is_some, LockedLoad::unwrap_or, LockedLoad::unwrap_or_else utilites to support this use case!

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ok, makes sense

Some(AggregateState<T>),
}
Loading