Radarr/src/Microsoft.AspNet.SignalR.Core/Messaging/Subscription.cs
2015-02-07 07:04:22 -08:00

338 lines
12 KiB
C#

// 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.CodeAnalysis;
using System.IO;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.AspNet.SignalR.Infrastructure;
namespace Microsoft.AspNet.SignalR.Messaging
{
public abstract class Subscription : ISubscription, IDisposable
{
private readonly Func<MessageResult, object, Task<bool>> _callback;
private readonly object _callbackState;
private readonly IPerformanceCounterManager _counters;
private int _state;
private int _subscriptionState;
private bool Alive
{
get
{
return _subscriptionState != SubscriptionState.Disposed;
}
}
public string Identity { get; private set; }
public IList<string> EventKeys { get; private set; }
public int MaxMessages { get; private set; }
public IDisposable Disposable { get; set; }
protected Subscription(string identity, IList<string> eventKeys, Func<MessageResult, object, Task<bool>> callback, int maxMessages, IPerformanceCounterManager counters, object state)
{
if (String.IsNullOrEmpty(identity))
{
throw new ArgumentNullException("identity");
}
if (eventKeys == null)
{
throw new ArgumentNullException("eventKeys");
}
if (callback == null)
{
throw new ArgumentNullException("callback");
}
if (maxMessages < 0)
{
throw new ArgumentOutOfRangeException("maxMessages");
}
if (counters == null)
{
throw new ArgumentNullException("counters");
}
Identity = identity;
_callback = callback;
EventKeys = eventKeys;
MaxMessages = maxMessages;
_counters = counters;
_callbackState = state;
_counters.MessageBusSubscribersTotal.Increment();
_counters.MessageBusSubscribersCurrent.Increment();
_counters.MessageBusSubscribersPerSec.Increment();
}
public virtual Task<bool> Invoke(MessageResult result)
{
return Invoke(result, state => { }, state: null);
}
private Task<bool> Invoke(MessageResult result, Action<object> beforeInvoke, object state)
{
// Change the state from idle to invoking callback
var prevState = Interlocked.CompareExchange(ref _subscriptionState,
SubscriptionState.InvokingCallback,
SubscriptionState.Idle);
if (prevState == SubscriptionState.Disposed)
{
// Only allow terminal messages after dispose
if (!result.Terminal)
{
return TaskAsyncHelper.False;
}
}
beforeInvoke(state);
_counters.MessageBusMessagesReceivedTotal.IncrementBy(result.TotalCount);
_counters.MessageBusMessagesReceivedPerSec.IncrementBy(result.TotalCount);
return _callback.Invoke(result, _callbackState).ContinueWith(task =>
{
// Go from invoking callback to idle
Interlocked.CompareExchange(ref _subscriptionState,
SubscriptionState.Idle,
SubscriptionState.InvokingCallback);
return task;
},
TaskContinuationOptions.ExecuteSynchronously).FastUnwrap();
}
public Task Work()
{
// Set the state to working
Interlocked.Exchange(ref _state, State.Working);
var tcs = new TaskCompletionSource<object>();
WorkImpl(tcs);
return tcs.Task;
}
public bool SetQueued()
{
return Interlocked.Increment(ref _state) == State.Working;
}
public bool UnsetQueued()
{
// If we try to set the state to idle and we were not already in the working state then keep going
return Interlocked.CompareExchange(ref _state, State.Idle, State.Working) != State.Working;
}
[SuppressMessage("Microsoft.Usage", "CA2202:Do not dispose objects multiple times", Justification = "We have a sync and async code path.")]
[SuppressMessage("Microsoft.Design", "CA1031:DoNotCatchGeneralExceptionTypes", Justification = "We want to avoid user code taking the process down.")]
private void WorkImpl(TaskCompletionSource<object> taskCompletionSource)
{
Process:
if (!Alive)
{
// If this subscription is dead then return immediately
taskCompletionSource.TrySetResult(null);
return;
}
var items = new List<ArraySegment<Message>>();
int totalCount;
object state;
PerformWork(items, out totalCount, out state);
if (items.Count > 0)
{
var messageResult = new MessageResult(items, totalCount);
Task<bool> callbackTask = Invoke(messageResult, s => BeforeInvoke(s), state);
if (callbackTask.IsCompleted)
{
try
{
// Make sure exceptions propagate
callbackTask.Wait();
if (callbackTask.Result)
{
// Sync path
goto Process;
}
else
{
// If we're done pumping messages through to this subscription
// then dispose
Dispose();
// If the callback said it's done then stop
taskCompletionSource.TrySetResult(null);
}
}
catch (Exception ex)
{
if (ex.InnerException is TaskCanceledException)
{
taskCompletionSource.TrySetCanceled();
}
else
{
taskCompletionSource.TrySetUnwrappedException(ex);
}
}
}
else
{
WorkImplAsync(callbackTask, taskCompletionSource);
}
}
else
{
taskCompletionSource.TrySetResult(null);
}
}
protected virtual void BeforeInvoke(object state)
{
}
[SuppressMessage("Microsoft.Design", "CA1002:DoNotExposeGenericLists", Justification = "The list needs to be populated")]
[SuppressMessage("Microsoft.Design", "CA1007:UseGenericsWhereAppropriate", Justification = "The caller wouldn't be able to specify what the generic type argument is")]
[SuppressMessage("Microsoft.Design", "CA1021:AvoidOutParameters", MessageId = "1#", Justification = "The count needs to be returned")]
[SuppressMessage("Microsoft.Design", "CA1021:AvoidOutParameters", MessageId = "2#", Justification = "The state needs to be set by the callee")]
protected abstract void PerformWork(IList<ArraySegment<Message>> items, out int totalCount, out object state);
private void WorkImplAsync(Task<bool> callbackTask, TaskCompletionSource<object> taskCompletionSource)
{
// Async path
callbackTask.ContinueWith(task =>
{
if (task.IsFaulted)
{
taskCompletionSource.TrySetUnwrappedException(task.Exception);
}
else if (task.IsCanceled)
{
taskCompletionSource.TrySetCanceled();
}
else if (task.Result)
{
WorkImpl(taskCompletionSource);
}
else
{
// If we're done pumping messages through to this subscription
// then dispose
Dispose();
// If the callback said it's done then stop
taskCompletionSource.TrySetResult(null);
}
});
}
public virtual bool AddEvent(string key, Topic topic)
{
return AddEventCore(key);
}
public virtual void RemoveEvent(string key)
{
lock (EventKeys)
{
EventKeys.Remove(key);
}
}
public virtual void SetEventTopic(string key, Topic topic)
{
// Don't call AddEvent since that's virtual
AddEventCore(key);
}
protected virtual void Dispose(bool disposing)
{
if (disposing)
{
// REIVIEW: Consider sleeping instead of using a tight loop, or maybe timing out after some interval
// if the client is very slow then this invoke call might not end quickly and this will make the CPU
// hot waiting for the task to return.
int disposeRetryCount = 0;
while (true)
{
// Wait until the subscription isn't working anymore
var state = Interlocked.CompareExchange(ref _subscriptionState,
SubscriptionState.Disposed,
SubscriptionState.Idle);
// If we're not working then stop
if (state != SubscriptionState.InvokingCallback || disposeRetryCount ++ > 10)
{
if (state != SubscriptionState.Disposed)
{
// Only decrement if we're not disposed already
_counters.MessageBusSubscribersCurrent.Decrement();
_counters.MessageBusSubscribersPerSec.Decrement();
}
// Raise the disposed callback
if (Disposable != null)
{
Disposable.Dispose();
}
break;
}
Thread.Sleep(500);
}
}
}
public void Dispose()
{
Dispose(true);
}
public abstract void WriteCursor(TextWriter textWriter);
private bool AddEventCore(string key)
{
lock (EventKeys)
{
if (EventKeys.Contains(key))
{
return false;
}
EventKeys.Add(key);
return true;
}
}
private static class State
{
public const int Idle = 0;
public const int Working = 1;
}
private static class SubscriptionState
{
public const int Idle = 0;
public const int InvokingCallback = 1;
public const int Disposed = 2;
}
}
}