Event Sourcing and CQRS, Dispatch options 2
By Jérémie Chassaing on Tuesday, June 21, 2011, 10:38 - Domain Driven Design - Permalink
In the part one comments, Clement suggested a more efficient solution than registering handler in constructor.
The proposed solution is to have a RegisterAllEvents virtual method in which event handler registration would occur. This method is a method instance to have access to this but will be called only once per class. The registration use Expression<Action<T>> to access the expression tree and extract the method info of the handler. This enables type checking, make R# happy – no unused methods – and make reflection not too painful.
Good solution.
I didn't go that far because with Event Sourcing, you usually keep
aggregates in memory, so aggregates are instantiated once per service
lifetime.
I just crafted a small performance test :
using System;
using System.Collections.Generic;
using System.Diagnostics;
namespace AggregatePerfTest
{
class Program
{
static void Main(string[] args)
{
var watch = new Stopwatch();
const int count = 10000000;
Guid id = Guid.NewGuid();
watch.Start();
for (int i = 0; i < count; i++)
new AggregateRegisteredOncePerInstance(id);
watch.Stop();
Console.WriteLine(watch.Elapsed.TotalMilliseconds);
watch.Reset();
watch.Start();
for (int i = 0; i < count; i++)
new AggregateRegisteredOncePerClass(id);
watch.Stop();
Console.WriteLine(watch.Elapsed.TotalMilliseconds);
}
}
public class AggregateRegisteredOncePerClass
{
private readonly Guid id;
private static readonly object ClassInitLock = new object();
private static bool initialized;
public AggregateRegisteredOncePerClass(Guid id)
{
this.id = id;
lock (ClassInitLock)
{
if (!initialized)
{
initialized = true;
// registration happens only once here
}
}
}
public Guid Id
{
get { return id; }
}
}
public class AggregateRegisteredOncePerInstance
{
private readonly Guid id;
private readonly Dictionary<Type, dynamic> handlers =
new Dictionary<Type, dynamic>(5);
public AggregateRegisteredOncePerInstance(Guid id)
{
this.id = id;
Register<int>(OnSomethingHappened);
Register<double>(OnSomethingHappened);
Register<float>(OnSomethingHappened);
Register<long>(OnSomethingHappened);
}
public Guid Id
{
get { return id; }
}
public void DoSomething()
{
Apply(1);
}
private void OnSomethingHappened(int message) { }
private void OnSomethingHappened(double message){ }
private void OnSomethingHappened(float message) { }
private void OnSomethingHappened(long message) { }
protected void Apply<T>(T @event)
{
handlers[typeof (T)](@event);
}
protected void Register<T>(Action<T> handler)
{
handlers.Add(typeof(T), handler);
}
}
}
The code is straight forward, I just created two aggregate classes :
- one with registration in .ctor based on this post code
- one without any registration at all, considering that doing it once is the same as not doing it for large numbers, but I added a lock section with a boolean check to simulate what will done on each instance creation.
I created 10.000.000 instances for each, and you get:
- 3978ms for the one with .ctor registrations,
- 377 ms for the one without.
It's true that it makes a difference. But how many aggregates do you have in your system ?
With 10.000 aggregate you're still under 8ms. I think you can afford that.
This is then a trade-off between performance and simplicity :
- If you have very large numbers, go for expression tree parsing, class lock management etc.
- In any other situation I recommend using registration in .ctor that makes the code easy to implement in approximately 5min.
Comments
One small remark: the "routing to state handler" code is different for the "initialization once per type(class)" case. Your registered Action<T> in the "initialization per instance" case references "this", which would make it unusable AFAIK in the "initialization once per type(class)" case.
Here's a paste of how I'm handling this currently (keeping registration process out of the base class): http://pastie.org/2100733
@yves> Sure.
That's why Clement proposed to use Expression<Action<T>> for the Register method in that case.
This way you can walk the tree to extract the method info from e => this.OnSomethingHappened(e) lambda.
Using this MethodInfo, you can then invoke the method on the instance when you need it in your Apply method.
For the per instance case, you can rely on dynamic, or use a Delegate base type for action and call DynamicInvoke.
In our case, we have lot of legacy code and we need to improve performance on meaningful code we started to refactor, until we get a better cache feature (working)...
Note that you can achieve even better performance (if really needed yet, i.e you have "lot of" events raised) using compiled late bound expression rather than MethodInfo.Invoke or Delegate.DynamicInvoke...see http://clem-it.blogspot.com/2009/07... for information on performance of these different solutions
@clement> But do those few seconds have a meaning ?
You gain less than 4 second for 10.000.000 aggregates.
For this number of aggregates you get at least 10x more messages/snapshots to load from disk, deserialize and apply. I think there are more time to get there than on instance creation.
We are not at this stage of maturity, nor in a classic CQRS approach...we use lot of ideas, but not all of them (for now at least...for lots of reason, bad ones or good ones...), that's why we got the problem for sure.
But not so much a problem for us, since solved quite quickly.
We can discuss more in depth at next Alt.Net paris beer of what we are doing...:)
Hi Jérémie, I found your posts about CQRS very interesting and I would like to ask if you could put the code from your posts somewhere, perhaps on bitbucket along the other code you have there to make it easy to play with it.
Hi Jérémie, I am enjoying reading through your DDD & CQRS posts, thanks. I just thought that I should point out that your perf test for the AggregateRegisteredOncePerClass is biased by potentially having to go down to Kernel mode due to the lock statement which need only be called if the class has not already been initialised. Your version would have the side effect of making all calls to the .ctor single threaded. With the change below it is ~ 20 times faster than the once per instance option. Again it may be premature optimisation.
public AggregateRegisteredOncePerClass(Guid id)
{
this.id = id;
if(!initialized) {lock (ClassInitLock)
{
if (!initialized)
{
initialized = true;
// registration happens only once here
}
}
}
}