4

Our existing implementation of domain events limits (by blocking) publishing to one thread at a time to avoid reentrant calls to handlers:

public interface IDomainEvent {}  // Marker interface

public class Dispatcher : IDisposable
{
    private readonly SemaphoreSlim semaphore = new SemaphoreSlim(1, 1);

    // Subscribe code...

    public void Publish(IDomainEvent domainEvent)
    {
        semaphore.Wait();
        try
        {
            // Get event subscriber(s) from concurrent dictionary...

            foreach (Action<IDomainEvent> subscriber in eventSubscribers)
            {
                subscriber(domainEvent);
            }
        }
        finally
        {
            semaphore.Release();
        }
    }
    // Dispose pattern...
}

If a handler publishes an event, this will deadlock.

How can I rewrite this to serialize calls to Publish? In other words, if subscribing handler A publishes event B, I'll get:

  1. Handler A called
  2. Handler B called

while preserving the condition of no reentrant calls to handlers in a multithreaded environment.

I do not want to change the public method signature; there's no place in the application to call a method to publish a queue, for instance.

TrueWill
  • 25,132
  • 10
  • 101
  • 150
  • ConcurrentQueue https://msdn.microsoft.com/en-us/library/dd267265(v=vs.110).aspx – Kevin Apr 05 '16 at 20:17
  • @Kevin I'm familiar with `ConcurrentQueue` and expect it to factor into the solution. I'm stuck on the implementation in the context of my class. – TrueWill Apr 05 '16 at 20:25
  • 1
    You need to process them synchronously? That is: when Publish exits, all subscribers should already process the message? – Evk Apr 05 '16 at 20:37
  • @Evk I'd *like* to process them synchronously but I'm not sure if that's possible. Maybe the internal publish has to be asynchronous? – TrueWill Apr 05 '16 at 20:48
  • 1
    Why do you call it a "message queue", this is just an event dispatcher. It has no queue at all. – Alexey Zimarev Apr 06 '16 at 07:04
  • @AlexeyZimarev You're correct. The original implementation used a queue, and I will happily add that back if it would buy me anything. The big issue appears to be that I wanted handlers to run on the same thread as the publisher and not be reentrant, while allowing handlers to publish. In retrospect I think the answers are correct; it can't be done with those constraints. – TrueWill Apr 06 '16 at 13:27

3 Answers3

2

You will have to make Publish asynchronous to achieve that. Naive implementation would be as simple as:

public class Dispatcher : IDisposable {
    private readonly BlockingCollection<IDomainEvent> _queue = new BlockingCollection<IDomainEvent>(new ConcurrentQueue<IDomainEvent>());
    private readonly CancellationTokenSource _cts = new CancellationTokenSource();

    public Dispatcher() {
        new Thread(Consume) {
            IsBackground = true
        }.Start();
    }

    private List<Action<IDomainEvent>> _subscribers = new List<Action<IDomainEvent>>();

    public void AddSubscriber(Action<IDomainEvent> sub) {
        _subscribers.Add(sub);
    }

    private void Consume() {            
        try {
            foreach (var @event in _queue.GetConsumingEnumerable(_cts.Token)) {
                try {
                    foreach (Action<IDomainEvent> subscriber in _subscribers) {
                        subscriber(@event);
                    }
                }
                catch (Exception ex) {
                    // log, handle                        
                }
            }
        }
        catch (OperationCanceledException) {
            // expected
        }
    }

    public void Publish(IDomainEvent domainEvent) {
        _queue.Add(domainEvent);
    }

    public void Dispose() {
        _cts.Cancel();
    }
}
Evk
  • 98,527
  • 8
  • 141
  • 191
2

We came up with a way to do it synchronously.

public class Dispatcher : IDisposable
{
    private readonly ConcurrentQueue<IDomainEvent> queue = new ConcurrentQueue<IDomainEvent>();
    private readonly SemaphoreSlim semaphore = new SemaphoreSlim(1, 1);

    // Subscribe code...

    public void Publish(IDomainEvent domainEvent)
    {
        queue.Enqueue(domainEvent);

        if (IsPublishing)
        {
            return;
        }

        PublishQueue();
    }

    private void PublishQueue()
    {
        IDomainEvent domainEvent;
        while (queue.TryDequeue(out domainEvent))
        {
            InternalPublish(domainEvent);
        }
    }

    private void InternalPublish(IDomainEvent domainEvent)
    {
        semaphore.Wait();
        try
        {
            // Get event subscriber(s) from concurrent dictionary...

            foreach (Action<IDomainEvent> subscriber in eventSubscribers)
            {
                subscriber(domainEvent);
            }
        }
        finally
        {
            semaphore.Release();
        }

        // Necessary, as calls to Publish during publishing could have queued events and returned.
        PublishQueue();
    }

    private bool IsPublishing
    {
        get { return semaphore.CurrentCount < 1; }
    }
    // Dispose pattern for semaphore...
}

}

TrueWill
  • 25,132
  • 10
  • 101
  • 150
1

It can't be done with that interface. You can process the event subscriptions asynchronously to remove the deadlock while still running them serially, but then you can't guarantee the order you described. Another call to Publish might enqueue something (event C) while the handler for event A is running but before it publishes event B. Then event B ends up behind event C in the queue.

As long as Handler A is on equal footing with other clients when it comes to getting an item in the queue, it either has to wait like everyone else (deadlock) or it has to play fairly (first come, first served). The interface you have there doesn't allow the two to be treated differently.

That's not to say you couldn't get up to some shenanigans in your logic to attempt to differentiate them (e.g. based on thread id or something else identifiable), but anything along those lines would unreliable if you don't control the subscriber code as well.

Martijn Pieters
  • 1,048,767
  • 296
  • 4,058
  • 3,343
saucecontrol
  • 1,446
  • 15
  • 17
  • The big clue is the first sentence `Would have made this a comment, but I don't have enough rep`! – DavidG Apr 05 '16 at 21:06
  • This is helpful. The order of events isn't critical; I was hoping to avoid creating threads but it appears I can't. – TrueWill Apr 05 '16 at 21:06
  • @DavidG My point there was that I would have commented to ask some clarifying questions, but since I couldn't, my only option was to answer the question as posed or continue not participating and not having enough rep to participate. It's a catch-22, hence the :/ – saucecontrol Apr 05 '16 at 21:10
  • @saucecontrol Go suggest some edits then, it really doesn't take long at all to get to 50. – DavidG Apr 05 '16 at 21:36