Latest Replies
Sunday
Jul172011

Lokad.CQRS - Getting Simpler and Faster

Recently I've been pushing a few changes into Lokad.CQRS trunk, as required by current Lokad projects. Two major ones are:

  • easy consumption of messages from completely separate projects;
  • lambda dispatch.

Multi-verse consumption

The most important improvement is to simplify consumption of messages coming from different systems. Consider situation, when project C consumes messages from projects A and B. Each project is in essence it's own universe with it's own dependencies and development speed. Message contracts are distributed in binary form of A.Contracts.dll, B.Contracts.dll etc.

based on our experience at Lokad we strongly recommend to ensure that these contract libraries should depend only on System.* dlls. Otherwise managing multiple projects will become time-consuming task.

Problem was related to the fact, that given:

  • project A defines its own base interface IMessageA for message classes;
  • project B defines its own base interface IMessageB for message classes.

in the project C we can't define a consumer signature that will handle both base interfaces.

As it turns out, the solution is quite simple. We can define polymorphic dispatcher to use object as a base message class (could be done on the latest):

mdm.HandlerSample<IMyConsume<object>>(s => s.Consume(null));

And then actual handler could be defined as:

public class SomeHandler : IMyConsume<ProjectAMessage>, IMyConsume<ProjectBMessage>

This small refactoring actually allowed me to decouple polymorphic dispatcher from the actual dispatch process. The latter simplified things a lot and enabled dead-simple performance optimization.

More than that, it allowed to move forward with our current projects within Lokad (where we have on average less than one developer per active project and thus can't waste any time on unnecessary friction).

What is polymorphic dispatcher? It is that complicated piece of code in Lokad.CQRS that allows you to define handlers as classes that inherit from various consuming interfaces (i.e.: IConsume[SpecificMessage] or IConsume[IMessageInterface]), while correctly resolving them through the IoC Container, handling scopes and transactions as needed.

Lambda Dispatch

Lokad.CQRS is rather simple (unless Azure slows it down) and hence it can be fast. Since we no longer have extremely tight coupling with the polymorphic complexity, we can dispatch directly to a delegate, which has form of lambda:

Action<ImmutableEnvelope> e => /* do something */

This is implemented as another dispatcher type available for all Lokad.CQRS partitions.

c.Memory(m =>
    {
        m.AddMemorySender("test");
        m.AddMemoryProcess("test", x => x.DispatcherIsLambda(Factory));
    }

static Action<ImmutableEnvelope> Factory(IComponentContext componentContext)
{
    var sender = componentContext.Resolve<IMessageSender>();
    return envelope => sender.SendOne(envelope.Items[0].Content);
}

Note, that we are actually using container once here, while building the lambda. Aside from that, there is neither reflection nor container resolution. This allows to get better performance. Below are some test results. First number goes for classical polymorphic dispatch, second one - for lambdas. MPS stands for messages per seconds.

By the way, you can obviously combine different types of dispatchers in a single Lokad.CQRS host.

Throughput performance test (we fill queue with messages and then consume them as fast as possible in 1 thread, doing all proper ACK stuff):

  • Azure Dev Store (don't laugh) - 11 / 11 mps
  • Files - 1315 / 1350 mps
  • Memory - 79000 / 101000 mps

By the way, memory queues are equivalent of non-durable messaging, while file queues are durable.

Reaction time test (single thread sends message to itself, then pulls this message from the queue etc):

  • Azure Dev Store: 8 / 8.4 mps
  • Files - 817 / 908 mps
  • Memory - 14700 / 44000 mps

First of all, You shouldn't worry much about relatively slow performance of local Azure Queues (that's what happens when you use take the complex path). Production performance should be an order of magnitude faster for a single thread. Besides, File and Memory partitions were added precisely to compensate for this slow performance while reducing development friction for Windows Azure Platform.

BTW, there is a chance that we can come up with better queues for Azure, than Azure Queues, drawing inspiration from LMAX circular architecture and relying on the inherent Lokad.CQRS capabilities.

Second, while looking at Files/Memory performance on a single thread, I would say that this is not so bad for code that was not performance optimized for the sake of staying simple.

Caveat: this performance applies to local operations only. True tests should cover network scenario that matches your production development (preferably with a chaos monkey in the house).

Probably if we use RabbitMQ or ZeroMQ, throughput can be even better than files out-of-the box.

Third, performance improvement is just a side effect. This refactoring was actually driven by desire do simplify the core (potentially getting rid of IoC container), provide much better support for extremely simple handler composition (based on lambdas), event sourcing and strongly-typed pipes.

For example, you can actually wrap all your command handlers (this applies even for polymorphic dispatch of commands/events) with some method that apply to them all (i.e.: logging, audit, auth etc). You can check this video of Greg Young to get a hint of things where we are going from here.

« Self-Improvement Process | Main | Lokad.CQRS Can Make Windows Azure Cheaper for You »

Reader Comments

There are no comments for this journal entry. To create a new comment, use the form below.

PostPost a New Comment

Enter your information below to add a new comment.

My response is on my own website »
Author Email (optional):
Author URL (optional):
Post:
 
Some HTML allowed: <a href="" title=""> <abbr title=""> <acronym title=""> <b> <blockquote cite=""> <code> <em> <i> <strike> <strong>