Search code examples
asp.net-mvcasynchronouslong-polling

How to do long polling properly in MVC 3


I am trying to wire up an AsyncController so that when a user clicks save on an order on the order page, all users viewing the same order should get a notification that the order has changed. My approach to implement this is to do long polling ajax requests on the order page, however how to make a scalable AsyncController to deal with this is not obvious to me.

So this is what I have so far, the ID is the ID of the order that is signaled as changed or polled for changes.

public class MessageController : AsyncController
{
    static readonly ConcurrentDictionary<int, AutoResetEvent> Events = new ConcurrentDictionary<int, AutoResetEvent>();

    public ActionResult Signal(int id)
    {
        AutoResetEvent @event;
        if (Events.TryGetValue(id, out @event))
            @event.Set();

        return Content("Signal");
    }

    public void WaitAsync(int id)
    {
        Events.TryAdd(id, new AutoResetEvent(false));

        // TODO: This "works", but I should probably not block this thread.
        Events[id].WaitOne();
    }

    public ActionResult WaitCompleted()
    {
        return Content("WaitCompleted");
    }
}

I have had a look at How to do long-polling AJAX requests in ASP.NET MVC? . I am trying to understand all details about this code but as far as I understand this code it is blocking each worker thread in the thread pool which, as far as I understand would eventually lead to thread starvation.

So, how should I implement this in a nice, scalable way? Bear in mind that I do not wish to use any more third party components, I want to get a good understanding of how to implement this scenario properly.


Solution

  • Actually I was able to implement this without blocking the worker threads, the thing I was missing was ThreadPool.RegisterWaitForSingleObject.

    public class ConcurrentLookup<TKey, TValue>
    {
        private readonly Dictionary<TKey, List<TValue>> _lookup = new Dictionary<TKey, List<TValue>>();
    
        public void Add(TKey key, TValue value)
        {
            lock (_lookup)
            {
                if (!_lookup.ContainsKey(key))
                    _lookup.Add(key, new List<TValue>());
    
                _lookup[key].Add(value);
            }
        }
    
        public List<TValue> Remove(TKey key)
        {
            lock (_lookup)
            {
                if (!_lookup.ContainsKey(key))
                    return new List<TValue>();
    
                var values = _lookup[key];
                _lookup.Remove(key);
    
                return values;
            }
        }
    }
    
    [SessionState(SessionStateBehavior.Disabled)]
    public class MessageController : AsyncController
    {
        static readonly ConcurrentLookup<int, ManualResetEvent> Events = new ConcurrentLookup<int, ManualResetEvent>();
    
        public ActionResult Signal(int id)
        {
            foreach (var @event in Events.Remove(id))
                @event.Set();
    
            return Content("Signal " + id);
        }
    
        public void WaitAsync(int id)
        {
            AsyncManager.OutstandingOperations.Increment();
    
            var @event = new ManualResetEvent(false);
    
            Events.Add(id, @event);
    
            RegisteredWaitHandle handle = null;
            handle = ThreadPool.RegisterWaitForSingleObject(@event, (state, timeout) => 
            {
                handle.Unregister(@event);
                @event.Dispose();
    
                AsyncManager.Parameters["id"] = id;
                AsyncManager.Parameters["timeout"] = timeout;
                AsyncManager.OutstandingOperations.Decrement();
            }, null, new TimeSpan(0, 2, 0), false);
        }
    
    
        public ActionResult WaitCompleted(int id, bool timeout)
        {
            return Content("WaitCompleted " + id + " " + (timeout? "Timeout" : "Signaled"));
        }
    }