// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved. See License.md in the project root for license information.
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Diagnostics.CodeAnalysis;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.AspNet.SignalR.Infrastructure;
namespace Microsoft.AspNet.SignalR.Messaging
{
///
/// This class is the main coordinator. It schedules work to be done for a particular subscription
/// and has an algorithm for choosing a number of workers (thread pool threads), to handle
/// the scheduled work.
///
public class MessageBroker : IDisposable
{
private readonly Queue _queue = new Queue();
private readonly IPerformanceCounterManager _counters;
// The maximum number of workers (threads) allowed to process all incoming messages
private readonly int _maxWorkers;
// The maximum number of workers that can be left to idle (not busy but allocated)
private readonly int _maxIdleWorkers;
// The number of allocated workers (currently running)
private int _allocatedWorkers;
// The number of workers that are *actually* doing work
private int _busyWorkers;
// Determines if the broker was disposed and should stop doing all work.
private bool _disposed;
public MessageBroker(IPerformanceCounterManager performanceCounterManager)
: this(performanceCounterManager, 3 * Environment.ProcessorCount, Environment.ProcessorCount)
{
}
public MessageBroker(IPerformanceCounterManager performanceCounterManager, int maxWorkers, int maxIdleWorkers)
{
_counters = performanceCounterManager;
_maxWorkers = maxWorkers;
_maxIdleWorkers = maxIdleWorkers;
}
public TraceSource Trace
{
get;
set;
}
public int AllocatedWorkers
{
get
{
return _allocatedWorkers;
}
}
public int BusyWorkers
{
get
{
return _busyWorkers;
}
}
public void Schedule(ISubscription subscription)
{
if (subscription == null)
{
throw new ArgumentNullException("subscription");
}
if (_disposed)
{
// Don't queue up new work if we've disposed the broker
return;
}
if (subscription.SetQueued())
{
lock (_queue)
{
_queue.Enqueue(subscription);
Monitor.Pulse(_queue);
AddWorker();
}
}
}
private void AddWorker()
{
// Only create a new worker if everyone is busy (up to the max)
if (_allocatedWorkers < _maxWorkers)
{
if (_allocatedWorkers == _busyWorkers)
{
_counters.MessageBusAllocatedWorkers.RawValue = Interlocked.Increment(ref _allocatedWorkers);
Trace.TraceEvent(TraceEventType.Verbose, 0, "Creating a worker, allocated={0}, busy={1}", _allocatedWorkers, _busyWorkers);
ThreadPool.QueueUserWorkItem(ProcessWork);
}
else
{
Trace.TraceEvent(TraceEventType.Verbose, 0, "No need to add a worker because all allocated workers are not busy, allocated={0}, busy={1}", _allocatedWorkers, _busyWorkers);
}
}
else
{
Trace.TraceEvent(TraceEventType.Verbose, 0, "Already at max workers, allocated={0}, busy={1}", _allocatedWorkers, _busyWorkers);
}
}
private void ProcessWork(object state)
{
Task pumpTask = PumpAsync();
if (pumpTask.IsCompleted)
{
ProcessWorkSync(pumpTask);
}
else
{
ProcessWorkAsync(pumpTask);
}
}
[SuppressMessage("Microsoft.Design", "CA1031:DoNotCatchGeneralExceptionTypes", Justification = "We want to avoid user code taking the process down.")]
private void ProcessWorkSync(Task pumpTask)
{
try
{
pumpTask.Wait();
}
catch (Exception ex)
{
Trace.TraceEvent(TraceEventType.Error, 0, "Failed to process work - " + ex.GetBaseException());
}
finally
{
// After the pump runs decrement the number of workers in flight
_counters.MessageBusAllocatedWorkers.RawValue = Interlocked.Decrement(ref _allocatedWorkers);
}
}
private void ProcessWorkAsync(Task pumpTask)
{
pumpTask.ContinueWith(task =>
{
// After the pump runs decrement the number of workers in flight
_counters.MessageBusAllocatedWorkers.RawValue = Interlocked.Decrement(ref _allocatedWorkers);
if (task.IsFaulted)
{
Trace.TraceEvent(TraceEventType.Error, 0, "Failed to process work - " + task.Exception.GetBaseException());
}
});
}
private Task PumpAsync()
{
var tcs = new TaskCompletionSource