eventuous icon indicating copy to clipboard operation
eventuous copied to clipboard

Caching aggregates to avoid repeated loads

Open alexeyzimarev opened this issue 2 years ago • 9 comments

Proposal:

  • Place the loaded aggregate instance in a cache
  • When getting a command for that instance, get it from the cache instead of loading it from the stream
  • Check the stream revision
  • If it's higher, read the stream tail

It can be done in a composable version of the AggregateStore.

EVE-35

alexeyzimarev avatar Nov 04 '22 12:11 alexeyzimarev

Stating the obvious.... This requires the state (aka aggregate instance) to be thread-safe to cover for when two callers take it from the cache for an overlapping period. I'd say that having the state require this is a good pattern in general, but it's definitely another concept that needs to be covered in the high level docs. (i.e. referring to using System.Collections.Immutable etc to implement persistent data structures). (As an aside, while the cost of folding things in terms of perf or GC impact is rarely a concern IRL, having the folding function accept a sequence of events such that a batch of updates can be applied to the immutable/persistent structure in an optimal manner is good to be able to accommodate. In other words, if you have lots of events that make small changes to the state, forcing that to use a persistent data structure for every event being applied might be suboptimal compared to providing batches of 100 events, and having the fold function copy the state to a mutable structure, do a bulk updated, and then convert it back to a persistent data structure after that's done)

When designing the API... Sometimes it's useful to be able to specify that you want to skip reading newer events and assume that, if the cache has a value, that it's worth doing a roundtrip assuming that the cache is in-date (in Equinox, I call that AllowStale). Of course you need to have a good story for what happens when the cache entry was stale (in Equinox the default behavior caters to this by re-running the command if the Sync based on the cached value shows a conflict)

bartelink avatar Nov 07 '22 10:11 bartelink

You're right, I haven't considered concurrent calls for the same object. The expected scenario, however, was a bit different and doesn't involve concurrency. In many cases, a user opens a single page and uses some task-based UI to execute several operations in a relatively short time (for a computer), sequentially.

Regarding the implementation, I planned to use the ASP.NET Core caching extensions and avoid thinking about concurrency. When the cached state is loaded, I'd expect it to be a deserialised instance (I might be wrong), so concurrent calls won't get the same instance.

alexeyzimarev avatar Nov 07 '22 15:11 alexeyzimarev

I planned to use the ASP.NET Core caching extensions and avoid thinking about concurrency.

If you look in the Equinox impl, it uses System.MemoryCache - the advantage is that it stashes ready-to-consume objects without the state needing to be serializable (any snapshotting involves a call to an explicit snapshotting function that takes that state and renders an explicitly versionable/serializable form of it). But yes, if the cache is hydrating it as an independent fresh instance, that does indeed render the concurrency issues solved ;)

bartelink avatar Nov 07 '22 15:11 bartelink

I planned to use IMemoryCache (who in their sane mind created an interface per cache kind???), and I don't know if it involves serialisation. Their Redis implementation does, of course, so I'd expect it to behave similarly. Need to check anyway, not willing to make wrong assumptions.

alexeyzimarev avatar Nov 07 '22 16:11 alexeyzimarev

I'm in need of this - any thoughts on ETA? :-) (Sorry for asking...)

ugumba avatar May 11 '23 13:05 ugumba

and I don't know if it involves serialisation.

It does not rely on serialization - it holds onto the actual object in your single process(only) the impl internally relies on GC hooks etc to keep the total size of held objects within a defined capacity (it's used in Equinox and works well - note the thing that's held is the folded state, together with the version of the stream at which it was produced)

bartelink avatar May 11 '23 13:05 bartelink

As I am currently working on subscriptions (split of physical subscription and consumer pipeline), it is now up for grabs. It's relatively straightforward, and I think the good first step would be to add it at the lowest level (read of stream in the event reader). Composition would work there nicely.

alexeyzimarev avatar May 11 '23 14:05 alexeyzimarev

#218 , #219 and #220 together add implicit support for IMemoryCache (if injected). I've only been able to run the basic tests, but the changes are also very basic... :-)

Tests in my own application indicate that more than 10 times as many (basic) commands can be processed in 1 second than without caching. With longer intervals, the difference should be exponentially better.

I've ignored composability, but I'll look into it next - use of IMemoryCache must at least be optional.

ugumba avatar May 12 '23 21:05 ugumba