imported signalr 1.1.3 into NzbDrone.

This commit is contained in:
kayone
2013-11-21 21:26:57 -08:00
parent 891443e05d
commit 0e623e7ce4
236 changed files with 20490 additions and 35 deletions

View File

@@ -0,0 +1,20 @@
// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved. See License.md in the project root for license information.
using System;
using System.Diagnostics.CodeAnalysis;
namespace Microsoft.AspNet.SignalR.Messaging
{
public class Command
{
public Command()
{
Id = Guid.NewGuid().ToString();
}
public bool WaitForAck { get; set; }
public string Id { get; private set; }
public CommandType CommandType { get; set; }
public string Value { get; set; }
}
}

View File

@@ -0,0 +1,12 @@
// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved. See License.md in the project root for license information.
namespace Microsoft.AspNet.SignalR.Messaging
{
public enum CommandType
{
AddToGroup,
RemoveFromGroup,
Disconnect,
Abort
}
}

View File

@@ -0,0 +1,252 @@
// 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.Globalization;
using System.IO;
using System.Text;
namespace Microsoft.AspNet.SignalR.Messaging
{
internal unsafe class Cursor
{
private static char[] _escapeChars = new[] { '\\', '|', ',' };
private string _escapedKey;
public string Key { get; private set; }
public ulong Id { get; set; }
public static Cursor Clone(Cursor cursor)
{
return new Cursor(cursor.Key, cursor.Id, cursor._escapedKey);
}
public Cursor(string key, ulong id)
: this(key, id, Escape(key))
{
}
public Cursor(string key, ulong id, string minifiedKey)
{
Key = key;
Id = id;
_escapedKey = minifiedKey;
}
public static void WriteCursors(TextWriter textWriter, IList<Cursor> cursors)
{
for (int i = 0; i < cursors.Count; i++)
{
if (i > 0)
{
textWriter.Write('|');
}
Cursor cursor = cursors[i];
textWriter.Write(cursor._escapedKey);
textWriter.Write(',');
WriteUlongAsHexToBuffer(cursor.Id, textWriter);
}
}
private static void WriteUlongAsHexToBuffer(ulong value, TextWriter textWriter)
{
// This tracks the length of the output and serves as the index for the next character to be written into the pBuffer.
// The length could reach up to 16 characters, so at least that much space should remain in the pBuffer.
int length = 0;
// Write the hex value from left to right into the buffer without zero padding.
for (int i = 0; i < 16; i++)
{
// Convert the first 4 bits of the value to a valid hex character.
char hexChar = Int32ToHex((int)(value >> 60));
value <<= 4;
// Don't increment length if it would just add zero padding
if (length != 0 || hexChar != '0')
{
textWriter.Write(hexChar);
length++;
}
}
if (length == 0)
{
textWriter.Write('0');
}
}
private static char Int32ToHex(int value)
{
return (value < 10) ? (char)(value + '0') : (char)(value - 10 + 'A');
}
private static string Escape(string value)
{
// Nothing to do, so bail
if (value.IndexOfAny(_escapeChars) == -1)
{
return value;
}
var sb = new StringBuilder();
// \\ = \
// \| = |
// \, = ,
foreach (var ch in value)
{
switch (ch)
{
case '\\':
sb.Append('\\').Append(ch);
break;
case '|':
sb.Append('\\').Append(ch);
break;
case ',':
sb.Append('\\').Append(ch);
break;
default:
sb.Append(ch);
break;
}
}
return sb.ToString();
}
public static List<Cursor> GetCursors(string cursor)
{
return GetCursors(cursor, s => s);
}
public static List<Cursor> GetCursors(string cursor, Func<string, string> keyMaximizer)
{
return GetCursors(cursor, (key, state) => ((Func<string, string>)state).Invoke(key), keyMaximizer);
}
public static List<Cursor> GetCursors(string cursor, Func<string, object, string> keyMaximizer, object state)
{
// Technically GetCursors should never be called with a null value, so this is extra cautious
if (String.IsNullOrEmpty(cursor))
{
throw new FormatException(Resources.Error_InvalidCursorFormat);
}
var signals = new HashSet<string>();
var cursors = new List<Cursor>();
string currentKey = null;
string currentEscapedKey = null;
ulong currentId;
bool escape = false;
bool consumingKey = true;
var sb = new StringBuilder();
var sbEscaped = new StringBuilder();
Cursor parsedCursor;
foreach (var ch in cursor)
{
// escape can only be true if we are consuming the key
if (escape)
{
if (ch != '\\' && ch != ',' && ch != '|')
{
throw new FormatException(Resources.Error_InvalidCursorFormat);
}
sb.Append(ch);
sbEscaped.Append(ch);
escape = false;
}
else
{
if (ch == '\\')
{
if (!consumingKey)
{
throw new FormatException(Resources.Error_InvalidCursorFormat);
}
sbEscaped.Append('\\');
escape = true;
}
else if (ch == ',')
{
if (!consumingKey)
{
throw new FormatException(Resources.Error_InvalidCursorFormat);
}
// For now String.Empty is an acceptable key, but this should change once we verify
// that empty keys cannot be created legitimately.
currentKey = keyMaximizer(sb.ToString(), state);
// If the keyMap cannot find a key, we cannot create an array of cursors.
// This most likely means there was an AppDomain restart or a misbehaving client.
if (currentKey == null)
{
return null;
}
// Don't allow duplicate keys
if (!signals.Add(currentKey))
{
throw new FormatException(Resources.Error_InvalidCursorFormat);
}
currentEscapedKey = sbEscaped.ToString();
sb.Clear();
sbEscaped.Clear();
consumingKey = false;
}
else if (ch == '|')
{
if (consumingKey)
{
throw new FormatException(Resources.Error_InvalidCursorFormat);
}
ParseCursorId(sb, out currentId);
parsedCursor = new Cursor(currentKey, currentId, currentEscapedKey);
cursors.Add(parsedCursor);
sb.Clear();
consumingKey = true;
}
else
{
sb.Append(ch);
if (consumingKey)
{
sbEscaped.Append(ch);
}
}
}
}
if (consumingKey)
{
throw new FormatException(Resources.Error_InvalidCursorFormat);
}
ParseCursorId(sb, out currentId);
parsedCursor = new Cursor(currentKey, currentId, currentEscapedKey);
cursors.Add(parsedCursor);
return cursors;
}
private static void ParseCursorId(StringBuilder sb, out ulong id)
{
string value = sb.ToString();
id = UInt64.Parse(value, NumberStyles.HexNumber, CultureInfo.InvariantCulture);
}
public override string ToString()
{
return Key;
}
}
}

View File

@@ -0,0 +1,219 @@
// 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.Tasks;
using Microsoft.AspNet.SignalR.Infrastructure;
namespace Microsoft.AspNet.SignalR.Messaging
{
internal class DefaultSubscription : Subscription
{
private List<Cursor> _cursors;
private List<Topic> _cursorTopics;
private readonly IStringMinifier _stringMinifier;
public DefaultSubscription(string identity,
IList<string> eventKeys,
TopicLookup topics,
string cursor,
Func<MessageResult, object, Task<bool>> callback,
int maxMessages,
IStringMinifier stringMinifier,
IPerformanceCounterManager counters,
object state) :
base(identity, eventKeys, callback, maxMessages, counters, state)
{
_stringMinifier = stringMinifier;
if (String.IsNullOrEmpty(cursor))
{
_cursors = GetCursorsFromEventKeys(EventKeys, topics);
}
else
{
// Ensure delegate continues to use the C# Compiler static delegate caching optimization.
_cursors = Cursor.GetCursors(cursor, (k, s) => UnminifyCursor(k, s), stringMinifier) ?? GetCursorsFromEventKeys(EventKeys, topics);
}
_cursorTopics = new List<Topic>();
if (!String.IsNullOrEmpty(cursor))
{
// Update all of the cursors so we're within the range
for (int i = _cursors.Count - 1; i >= 0; i--)
{
Cursor c = _cursors[i];
Topic topic;
if (!EventKeys.Contains(c.Key))
{
_cursors.Remove(c);
}
else if (!topics.TryGetValue(_cursors[i].Key, out topic) || _cursors[i].Id > topic.Store.GetMessageCount())
{
UpdateCursor(c.Key, 0);
}
}
}
// Add dummy entries so they can be filled in
for (int i = 0; i < _cursors.Count; i++)
{
_cursorTopics.Add(null);
}
}
private static string UnminifyCursor(string key, object state)
{
return ((IStringMinifier)state).Unminify(key);
}
public override bool AddEvent(string eventKey, Topic topic)
{
base.AddEvent(eventKey, topic);
lock (_cursors)
{
// O(n), but small n and it's not common
var index = _cursors.FindIndex(c => c.Key == eventKey);
if (index == -1)
{
_cursors.Add(new Cursor(eventKey, GetMessageId(topic), _stringMinifier.Minify(eventKey)));
_cursorTopics.Add(topic);
return true;
}
return false;
}
}
public override void RemoveEvent(string eventKey)
{
base.RemoveEvent(eventKey);
lock (_cursors)
{
var index = _cursors.FindIndex(c => c.Key == eventKey);
if (index != -1)
{
_cursors.RemoveAt(index);
_cursorTopics.RemoveAt(index);
}
}
}
public override void SetEventTopic(string eventKey, Topic topic)
{
base.SetEventTopic(eventKey, topic);
lock (_cursors)
{
// O(n), but small n and it's not common
var index = _cursors.FindIndex(c => c.Key == eventKey);
if (index != -1)
{
_cursorTopics[index] = topic;
}
}
}
public override void WriteCursor(TextWriter textWriter)
{
lock (_cursors)
{
Cursor.WriteCursors(textWriter, _cursors);
}
}
[SuppressMessage("Microsoft.Design", "CA1062:Validate arguments of public methods", MessageId = "0", Justification = "It is called from the base class")]
protected override void PerformWork(IList<ArraySegment<Message>> items, out int totalCount, out object state)
{
totalCount = 0;
lock (_cursors)
{
var cursors = new ulong[_cursors.Count];
for (int i = 0; i < _cursors.Count; i++)
{
MessageStoreResult<Message> storeResult = _cursorTopics[i].Store.GetMessages(_cursors[i].Id, MaxMessages);
cursors[i] = storeResult.FirstMessageId + (ulong)storeResult.Messages.Count;
if (storeResult.Messages.Count > 0)
{
items.Add(storeResult.Messages);
totalCount += storeResult.Messages.Count;
}
}
// Return the state as a list of cursors
state = cursors;
}
}
protected override void BeforeInvoke(object state)
{
lock (_cursors)
{
// Update the list of cursors before invoking anything
var nextCursors = (ulong[])state;
for (int i = 0; i < _cursors.Count; i++)
{
_cursors[i].Id = nextCursors[i];
}
}
}
private bool UpdateCursor(string key, ulong id)
{
lock (_cursors)
{
// O(n), but small n and it's not common
var index = _cursors.FindIndex(c => c.Key == key);
if (index != -1)
{
_cursors[index].Id = id;
return true;
}
return false;
}
}
private List<Cursor> GetCursorsFromEventKeys(IList<string> eventKeys, TopicLookup topics)
{
var list = new List<Cursor>(eventKeys.Count);
foreach (var eventKey in eventKeys)
{
var cursor = new Cursor(eventKey, GetMessageId(topics, eventKey), _stringMinifier.Minify(eventKey));
list.Add(cursor);
}
return list;
}
private static ulong GetMessageId(TopicLookup topics, string key)
{
Topic topic;
if (topics.TryGetValue(key, out topic))
{
return GetMessageId(topic);
}
return 0;
}
private static ulong GetMessageId(Topic topic)
{
if (topic == null)
{
return 0;
}
return topic.Store.GetMessageCount();
}
}
}

View File

@@ -0,0 +1,29 @@
// 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.Threading.Tasks;
namespace Microsoft.AspNet.SignalR.Messaging
{
public interface IMessageBus
{
/// <summary>
///
/// </summary>
/// <param name="message"></param>
/// <returns></returns>
Task Publish(Message message);
/// <summary>
///
/// </summary>
/// <param name="subscriber"></param>
/// <param name="cursor"></param>
/// <param name="callback"></param>
/// <param name="maxMessages"></param>
/// <param name="state"></param>
/// <returns></returns>
IDisposable Subscribe(ISubscriber subscriber, string cursor, Func<MessageResult, object, Task<bool>> callback, int maxMessages, object state);
}
}

View File

@@ -0,0 +1,23 @@
// 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.IO;
namespace Microsoft.AspNet.SignalR.Messaging
{
public interface ISubscriber
{
IList<string> EventKeys { get; }
Action<TextWriter> WriteCursor { get; set; }
string Identity { get; }
event Action<ISubscriber, string> EventKeyAdded;
event Action<ISubscriber, string> EventKeyRemoved;
Subscription Subscription { get; set; }
}
}

View File

@@ -0,0 +1,16 @@
// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved. See License.md in the project root for license information.
using System.Threading.Tasks;
namespace Microsoft.AspNet.SignalR.Messaging
{
public interface ISubscription
{
string Identity { get; }
bool SetQueued();
bool UnsetQueued();
Task Work();
}
}

View File

@@ -0,0 +1,30 @@
// 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;
namespace Microsoft.AspNet.SignalR.Messaging
{
public class LocalEventKeyInfo
{
private readonly WeakReference _storeReference;
public LocalEventKeyInfo(string key, ulong id, MessageStore<Message> store)
{
// Don't hold onto MessageStores that would otherwise be GC'd
_storeReference = new WeakReference(store);
Key = key;
Id = id;
}
public string Key { get; private set; }
public ulong Id { get; private set; }
public MessageStore<Message> MessageStore
{
get
{
return _storeReference.Target as MessageStore<Message>;
}
}
}
}

View File

@@ -0,0 +1,155 @@
// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved. See License.md in the project root for license information.
using System;
using System.Diagnostics.CodeAnalysis;
using System.IO;
using System.Text;
namespace Microsoft.AspNet.SignalR.Messaging
{
public class Message
{
private static readonly byte[] _zeroByteBuffer = new byte[0];
private static readonly UTF8Encoding _encoding = new UTF8Encoding();
public Message()
{
Encoding = _encoding;
}
public Message(string source, string key, string value)
{
if (source == null)
{
throw new ArgumentNullException("source");
}
if (key == null)
{
throw new ArgumentNullException("key");
}
Source = source;
Key = key;
Encoding = _encoding;
Value = value == null ? new ArraySegment<byte>(_zeroByteBuffer) : new ArraySegment<byte>(Encoding.GetBytes(value));
}
public Message(string source, string key, ArraySegment<byte> value)
: this()
{
if (source == null)
{
throw new ArgumentNullException("source");
}
if (key == null)
{
throw new ArgumentNullException("key");
}
Source = source;
Key = key;
Value = value;
}
/// <summary>
/// Which connection the message originated from
/// </summary>
public string Source { get; set; }
/// <summary>
/// The signal for the message (connection id, group, etc)
/// </summary>
public string Key { get; set; }
/// <summary>
/// The message payload
/// </summary>
public ArraySegment<byte> Value { get; set; }
/// <summary>
/// The command id if this message is a command
/// </summary>
public string CommandId { get; set; }
/// <summary>
/// Determines if the caller should wait for acknowledgement for this message
/// </summary>
public bool WaitForAck { get; set; }
/// <summary>
/// Determines if this message is itself an ACK
/// </summary>
public bool IsAck { get; set; }
/// <summary>
/// A list of connection ids to filter out
/// </summary>
public string Filter { get; set; }
/// <summary>
/// The encoding of the message
/// </summary>
public Encoding Encoding { get; private set; }
/// <summary>
/// The payload id. Only used in scaleout scenarios
/// </summary>
public ulong MappingId { get; set; }
/// <summary>
/// The stream index this message came from. Only used the scaleout scenarios.
/// </summary>
public int StreamIndex { get; set; }
public bool IsCommand
{
get
{
return !String.IsNullOrEmpty(CommandId);
}
}
[SuppressMessage("Microsoft.Design", "CA1024:UsePropertiesWhereAppropriate", Justification = "This may be expensive")]
public string GetString()
{
// If there's no encoding this is a raw binary payload
if (Encoding == null)
{
throw new NotSupportedException();
}
return Encoding.GetString(Value.Array, Value.Offset, Value.Count);
}
public void WriteTo(Stream stream)
{
var binaryWriter = new BinaryWriter(stream);
binaryWriter.Write(Source);
binaryWriter.Write(Key);
binaryWriter.Write(Value.Count);
binaryWriter.Write(Value.Array, Value.Offset, Value.Count);
binaryWriter.Write(CommandId ?? String.Empty);
binaryWriter.Write(WaitForAck);
binaryWriter.Write(IsAck);
binaryWriter.Write(Filter ?? String.Empty);
}
public static Message ReadFrom(Stream stream)
{
var message = new Message();
var binaryReader = new BinaryReader(stream);
message.Source = binaryReader.ReadString();
message.Key = binaryReader.ReadString();
int bytes = binaryReader.ReadInt32();
message.Value = new ArraySegment<byte>(binaryReader.ReadBytes(bytes));
message.CommandId = binaryReader.ReadString();
message.WaitForAck = binaryReader.ReadBoolean();
message.IsAck = binaryReader.ReadBoolean();
message.Filter = binaryReader.ReadString();
return message;
}
}
}

View File

@@ -0,0 +1,325 @@
// 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
{
/// <summary>
/// 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.
/// </summary>
public class MessageBroker : IDisposable
{
private readonly Queue<ISubscription> _queue = new Queue<ISubscription>();
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<object>();
PumpImpl(tcs);
return tcs.Task;
}
[SuppressMessage("Microsoft.Design", "CA1031:DoNotCatchGeneralExceptionTypes", Justification = "We want to avoid user code taking the process down.")]
private void PumpImpl(TaskCompletionSource<object> taskCompletionSource, ISubscription subscription = null)
{
Process:
// If we were doing work before and now we've been disposed just kill this worker early
if (_disposed)
{
taskCompletionSource.TrySetResult(null);
return;
}
Debug.Assert(_allocatedWorkers <= _maxWorkers, "How did we pass the max?");
// If we're withing the acceptable limit of idleness, just keep running
int idleWorkers = _allocatedWorkers - _busyWorkers;
if (subscription != null || idleWorkers <= _maxIdleWorkers)
{
// We already have a subscription doing work so skip the queue
if (subscription == null)
{
lock (_queue)
{
while (_queue.Count == 0)
{
Monitor.Wait(_queue);
// When disposing, all workers are pulsed so that they can quit
// if they're waiting for things to do (idle)
if (_disposed)
{
taskCompletionSource.TrySetResult(null);
return;
}
}
subscription = _queue.Dequeue();
}
}
_counters.MessageBusBusyWorkers.RawValue = Interlocked.Increment(ref _busyWorkers);
Task workTask = subscription.Work();
if (workTask.IsCompleted)
{
try
{
workTask.Wait();
goto Process;
}
catch (Exception ex)
{
Trace.TraceEvent(TraceEventType.Error, 0, "Work failed for " + subscription.Identity + ": " + ex.GetBaseException());
goto Process;
}
finally
{
if (!subscription.UnsetQueued() || workTask.IsFaulted)
{
// If we don't have more work to do just make the subscription null
subscription = null;
}
_counters.MessageBusBusyWorkers.RawValue = Interlocked.Decrement(ref _busyWorkers);
Debug.Assert(_busyWorkers >= 0, "The number of busy workers has somehow gone negative");
}
}
else
{
PumpImplAsync(workTask, subscription, taskCompletionSource);
}
}
else
{
taskCompletionSource.TrySetResult(null);
}
}
private void PumpImplAsync(Task workTask, ISubscription subscription, TaskCompletionSource<object> taskCompletionSource)
{
// Async path
workTask.ContinueWith(task =>
{
bool moreWork = subscription.UnsetQueued();
_counters.MessageBusBusyWorkers.RawValue = Interlocked.Decrement(ref _busyWorkers);
Debug.Assert(_busyWorkers >= 0, "The number of busy workers has somehow gone negative");
if (task.IsFaulted)
{
Trace.TraceEvent(TraceEventType.Error, 0, "Work failed for " + subscription.Identity + ": " + task.Exception.GetBaseException());
}
if (moreWork && !task.IsFaulted)
{
PumpImpl(taskCompletionSource, subscription);
}
else
{
// Don't reference the subscription anymore
subscription = null;
PumpImpl(taskCompletionSource);
}
});
}
protected virtual void Dispose(bool disposing)
{
if (disposing)
{
if (!_disposed)
{
_disposed = true;
Trace.TraceEvent(TraceEventType.Verbose, 0, "Dispoing the broker");
// Wait for all threads to stop working
WaitForDrain();
Trace.TraceEvent(TraceEventType.Verbose, 0, "Disposed the broker");
}
}
}
public void Dispose()
{
Dispose(true);
}
private void WaitForDrain()
{
while (_allocatedWorkers > 0)
{
lock (_queue)
{
// Tell all workers we're done
Monitor.PulseAll(_queue);
}
Thread.Sleep(250);
}
}
}
}

View File

@@ -0,0 +1,588 @@
// 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.Configuration;
using Microsoft.AspNet.SignalR.Infrastructure;
using Microsoft.AspNet.SignalR.Tracing;
namespace Microsoft.AspNet.SignalR.Messaging
{
/// <summary>
///
/// </summary>
public class MessageBus : IMessageBus, IDisposable
{
private readonly MessageBroker _broker;
// The size of the messages store we allocate per topic.
private readonly uint _messageStoreSize;
// By default, topics are cleaned up after having no subscribers and after
// an interval based on the disconnect timeout has passed. While this works in normal cases
// it's an issue when the rate of incoming connections is too high.
// This is the maximum number of un-expired topics with no subscribers
// we'll leave hanging around. The rest will be cleaned up on an the gc interval.
private readonly int _maxTopicsWithNoSubscriptions;
private readonly IStringMinifier _stringMinifier;
private readonly ITraceManager _traceManager;
private readonly TraceSource _trace;
private Timer _gcTimer;
private int _gcRunning;
private static readonly TimeSpan _gcInterval = TimeSpan.FromSeconds(5);
private readonly TimeSpan _topicTtl;
// For unit testing
internal Action<string, Topic> BeforeTopicGarbageCollected;
internal Action<string, Topic> AfterTopicGarbageCollected;
internal Action<string, Topic> BeforeTopicMarked;
internal Action<string> BeforeTopicCreated;
internal Action<string, Topic> AfterTopicMarkedSuccessfully;
internal Action<string, Topic, int> AfterTopicMarked;
private const int DefaultMaxTopicsWithNoSubscriptions = 1000;
private readonly Func<string, Topic> _createTopic;
private readonly Action<ISubscriber, string> _addEvent;
private readonly Action<ISubscriber, string> _removeEvent;
private readonly Action<object> _disposeSubscription;
/// <summary>
///
/// </summary>
/// <param name="resolver"></param>
public MessageBus(IDependencyResolver resolver)
: this(resolver.Resolve<IStringMinifier>(),
resolver.Resolve<ITraceManager>(),
resolver.Resolve<IPerformanceCounterManager>(),
resolver.Resolve<IConfigurationManager>(),
DefaultMaxTopicsWithNoSubscriptions)
{
}
/// <summary>
///
/// </summary>
/// <param name="stringMinifier"></param>
/// <param name="traceManager"></param>
/// <param name="performanceCounterManager"></param>
/// <param name="configurationManager"></param>
/// <param name="maxTopicsWithNoSubscriptions"></param>
[SuppressMessage("Microsoft.Reliability", "CA2000:Dispose objects before losing scope", Justification = "The message broker is disposed when the bus is disposed.")]
public MessageBus(IStringMinifier stringMinifier,
ITraceManager traceManager,
IPerformanceCounterManager performanceCounterManager,
IConfigurationManager configurationManager,
int maxTopicsWithNoSubscriptions)
{
if (stringMinifier == null)
{
throw new ArgumentNullException("stringMinifier");
}
if (traceManager == null)
{
throw new ArgumentNullException("traceManager");
}
if (performanceCounterManager == null)
{
throw new ArgumentNullException("performanceCounterManager");
}
if (configurationManager == null)
{
throw new ArgumentNullException("configurationManager");
}
if (configurationManager.DefaultMessageBufferSize < 0)
{
throw new ArgumentOutOfRangeException(Resources.Error_BufferSizeOutOfRange);
}
_stringMinifier = stringMinifier;
_traceManager = traceManager;
Counters = performanceCounterManager;
_trace = _traceManager["SignalR." + typeof(MessageBus).Name];
_maxTopicsWithNoSubscriptions = maxTopicsWithNoSubscriptions;
_gcTimer = new Timer(_ => GarbageCollectTopics(), state: null, dueTime: _gcInterval, period: _gcInterval);
_broker = new MessageBroker(Counters)
{
Trace = _trace
};
// The default message store size
_messageStoreSize = (uint)configurationManager.DefaultMessageBufferSize;
_topicTtl = configurationManager.TopicTtl();
_createTopic = CreateTopic;
_addEvent = AddEvent;
_removeEvent = RemoveEvent;
_disposeSubscription = DisposeSubscription;
Topics = new TopicLookup();
}
protected virtual TraceSource Trace
{
get
{
return _trace;
}
}
protected internal TopicLookup Topics { get; private set; }
protected IPerformanceCounterManager Counters { get; private set; }
public int AllocatedWorkers
{
get
{
return _broker.AllocatedWorkers;
}
}
public int BusyWorkers
{
get
{
return _broker.BusyWorkers;
}
}
/// <summary>
/// Publishes a new message to the specified event on the bus.
/// </summary>
/// <param name="message">The message to publish.</param>
public virtual Task Publish(Message message)
{
if (message == null)
{
throw new ArgumentNullException("message");
}
Topic topic;
if (Topics.TryGetValue(message.Key, out topic))
{
topic.Store.Add(message);
ScheduleTopic(topic);
}
Counters.MessageBusMessagesPublishedTotal.Increment();
Counters.MessageBusMessagesPublishedPerSec.Increment();
return TaskAsyncHelper.Empty;
}
protected ulong Save(Message message)
{
if (message == null)
{
throw new ArgumentNullException("message");
}
// GetTopic will return a topic for the given key. If topic exists and is Dying,
// it will revive it and mark it as NoSubscriptions
Topic topic = GetTopic(message.Key);
// Mark the topic as used so it doesn't immediately expire (if it was in that state before).
topic.MarkUsed();
return topic.Store.Add(message);
}
/// <summary>
///
/// </summary>
/// <param name="subscriber"></param>
/// <param name="cursor"></param>
/// <param name="callback"></param>
/// <param name="maxMessages"></param>
/// <param name="state"></param>
/// <returns></returns>
[SuppressMessage("Microsoft.Reliability", "CA2000:Dispose objects before losing scope", Justification = "The disposable object is returned to the caller")]
public virtual IDisposable Subscribe(ISubscriber subscriber, string cursor, Func<MessageResult, object, Task<bool>> callback, int maxMessages, object state)
{
if (subscriber == null)
{
throw new ArgumentNullException("subscriber");
}
if (callback == null)
{
throw new ArgumentNullException("callback");
}
Subscription subscription = CreateSubscription(subscriber, cursor, callback, maxMessages, state);
// Set the subscription for this subscriber
subscriber.Subscription = subscription;
var topics = new HashSet<Topic>();
foreach (var key in subscriber.EventKeys)
{
// Create or retrieve topic and set it as HasSubscriptions
Topic topic = SubscribeTopic(key);
// Set the subscription for this topic
subscription.SetEventTopic(key, topic);
topics.Add(topic);
}
subscriber.EventKeyAdded += _addEvent;
subscriber.EventKeyRemoved += _removeEvent;
subscriber.WriteCursor = subscription.WriteCursor;
var subscriptionState = new SubscriptionState(subscriber);
var disposable = new DisposableAction(_disposeSubscription, subscriptionState);
// When the subscription itself is disposed then dispose it
subscription.Disposable = disposable;
// Add the subscription when it's all set and can be scheduled
// for work. It's important to do this after everything is wired up for the
// subscription so that publishes can schedule work at the right time.
foreach (var topic in topics)
{
topic.AddSubscription(subscription);
}
subscriptionState.Initialized.Set();
// If there's a cursor then schedule work for this subscription
if (!String.IsNullOrEmpty(cursor))
{
_broker.Schedule(subscription);
}
return disposable;
}
[SuppressMessage("Microsoft.Design", "CA1062:Validate arguments of public methods", MessageId = "0", Justification = "Called from derived class")]
protected virtual Subscription CreateSubscription(ISubscriber subscriber, string cursor, Func<MessageResult, object, Task<bool>> callback, int messageBufferSize, object state)
{
return new DefaultSubscription(subscriber.Identity, subscriber.EventKeys, Topics, cursor, callback, messageBufferSize, _stringMinifier, Counters, state);
}
protected void ScheduleEvent(string eventKey)
{
Topic topic;
if (Topics.TryGetValue(eventKey, out topic))
{
ScheduleTopic(topic);
}
}
private void ScheduleTopic(Topic topic)
{
try
{
topic.SubscriptionLock.EnterReadLock();
for (int i = 0; i < topic.Subscriptions.Count; i++)
{
ISubscription subscription = topic.Subscriptions[i];
_broker.Schedule(subscription);
}
}
finally
{
topic.SubscriptionLock.ExitReadLock();
}
}
/// <summary>
/// Creates a topic for the specified key.
/// </summary>
/// <param name="key">The key to create the topic for.</param>
/// <returns>A <see cref="Topic"/> for the specified key.</returns>
protected virtual Topic CreateTopic(string key)
{
// REVIEW: This can be called multiple times, should we guard against it?
Counters.MessageBusTopicsCurrent.Increment();
return new Topic(_messageStoreSize, _topicTtl);
}
protected virtual void Dispose(bool disposing)
{
if (disposing)
{
// Stop the broker from doing any work
_broker.Dispose();
// Spin while we wait for the timer to finish if it's currently running
while (Interlocked.Exchange(ref _gcRunning, 1) == 1)
{
Thread.Sleep(250);
}
// Remove all topics
Topics.Clear();
if (_gcTimer != null)
{
_gcTimer.Dispose();
}
}
}
public void Dispose()
{
Dispose(true);
}
internal void GarbageCollectTopics()
{
if (Interlocked.Exchange(ref _gcRunning, 1) == 1)
{
return;
}
int topicsWithNoSubs = 0;
foreach (var pair in Topics)
{
if (pair.Value.IsExpired)
{
if (BeforeTopicGarbageCollected != null)
{
BeforeTopicGarbageCollected(pair.Key, pair.Value);
}
// Mark the topic as dead
DestroyTopic(pair.Key, pair.Value);
}
else if (pair.Value.State == TopicState.NoSubscriptions)
{
// Keep track of the number of topics with no subscriptions
topicsWithNoSubs++;
}
}
int overflow = topicsWithNoSubs - _maxTopicsWithNoSubscriptions;
if (overflow > 0)
{
// If we've overflowed the max the collect topics that don't have
// subscribers
var candidates = new List<KeyValuePair<string, Topic>>();
foreach (var pair in Topics)
{
if (pair.Value.State == TopicState.NoSubscriptions)
{
candidates.Add(pair);
}
}
// We want to remove the overflow but oldest first
candidates.Sort((leftPair, rightPair) => leftPair.Value.LastUsed.CompareTo(rightPair.Value.LastUsed));
// Clear up to the overflow and stay within bounds
for (int i = 0; i < overflow && i < candidates.Count; i++)
{
var pair = candidates[i];
// We only want to kill the topic if it's in the NoSubscriptions or Dying state.
if (InterlockedHelper.CompareExchangeOr(ref pair.Value.State, TopicState.Dead, TopicState.NoSubscriptions, TopicState.Dying))
{
// Kill it
DestroyTopicCore(pair.Key, pair.Value);
}
}
}
Interlocked.Exchange(ref _gcRunning, 0);
}
private void DestroyTopic(string key, Topic topic)
{
// The goal of this function is to destroy topics after 2 garbage collect cycles
// This first if statement will transition a topic into the dying state on the first GC cycle
// but it will prevent the code path from hitting the second if statement
if (Interlocked.CompareExchange(ref topic.State, TopicState.Dying, TopicState.NoSubscriptions) == TopicState.Dying)
{
// If we've hit this if statement we're on the second GC cycle with this soon to be
// destroyed topic. At this point we move the Topic State into the Dead state as
// long as it has not been revived from the dying state. We check if the state is
// still dying again to ensure that the topic has not been transitioned into a new
// state since we've decided to destroy it.
if (Interlocked.CompareExchange(ref topic.State, TopicState.Dead, TopicState.Dying) == TopicState.Dying)
{
DestroyTopicCore(key, topic);
}
}
}
private void DestroyTopicCore(string key, Topic topic)
{
Topics.TryRemove(key);
_stringMinifier.RemoveUnminified(key);
Counters.MessageBusTopicsCurrent.Decrement();
Trace.TraceInformation("RemoveTopic(" + key + ")");
if (AfterTopicGarbageCollected != null)
{
AfterTopicGarbageCollected(key, topic);
}
}
internal Topic GetTopic(string key)
{
Topic topic;
int oldState;
do
{
if (BeforeTopicCreated != null)
{
BeforeTopicCreated(key);
}
topic = Topics.GetOrAdd(key, _createTopic);
if (BeforeTopicMarked != null)
{
BeforeTopicMarked(key, topic);
}
// If the topic was dying revive it to the NoSubscriptions state. This is used to ensure
// that in the scaleout case that even if we're publishing to a topic with no subscriptions
// that we keep it around in case a user hops nodes.
oldState = Interlocked.CompareExchange(ref topic.State, TopicState.NoSubscriptions, TopicState.Dying);
if (AfterTopicMarked != null)
{
AfterTopicMarked(key, topic, topic.State);
}
// If the topic is currently dead then we're racing with the DestroyTopicCore function, therefore
// loop around until we're able to create a new topic
} while (oldState == TopicState.Dead);
if (AfterTopicMarkedSuccessfully != null)
{
AfterTopicMarkedSuccessfully(key, topic);
}
return topic;
}
internal Topic SubscribeTopic(string key)
{
Topic topic;
do
{
if (BeforeTopicCreated != null)
{
BeforeTopicCreated(key);
}
topic = Topics.GetOrAdd(key, _createTopic);
if (BeforeTopicMarked != null)
{
BeforeTopicMarked(key, topic);
}
// Transition into the HasSubscriptions state as long as the topic is not dead
InterlockedHelper.CompareExchangeOr(ref topic.State, TopicState.HasSubscriptions, TopicState.NoSubscriptions, TopicState.Dying);
if (AfterTopicMarked != null)
{
AfterTopicMarked(key, topic, topic.State);
}
// If we were unable to transition into the HasSubscription state that means we're in the Dead state.
// Loop around until we're able to create the topic new
} while (topic.State != TopicState.HasSubscriptions);
if (AfterTopicMarkedSuccessfully != null)
{
AfterTopicMarkedSuccessfully(key, topic);
}
return topic;
}
private void AddEvent(ISubscriber subscriber, string eventKey)
{
Topic topic = SubscribeTopic(eventKey);
// Add or update the cursor (in case it already exists)
if (subscriber.Subscription.AddEvent(eventKey, topic))
{
// Add it to the list of subs
topic.AddSubscription(subscriber.Subscription);
}
}
private void RemoveEvent(ISubscriber subscriber, string eventKey)
{
Topic topic;
if (Topics.TryGetValue(eventKey, out topic))
{
topic.RemoveSubscription(subscriber.Subscription);
subscriber.Subscription.RemoveEvent(eventKey);
}
}
[SuppressMessage("Microsoft.Design", "CA1031:DoNotCatchGeneralExceptionTypes", Justification = "Failure to invoke the callback should be ignored")]
private void DisposeSubscription(object state)
{
var subscriptionState = (SubscriptionState)state;
var subscriber = subscriptionState.Subscriber;
// This will stop work from continuting to happen
subscriber.Subscription.Dispose();
try
{
// Invoke the terminal callback
subscriber.Subscription.Invoke(MessageResult.TerminalMessage).Wait();
}
catch
{
// We failed to talk to the subscriber because they are already gone
// so the terminal message isn't required.
}
subscriptionState.Initialized.Wait();
subscriber.EventKeyAdded -= _addEvent;
subscriber.EventKeyRemoved -= _removeEvent;
subscriber.WriteCursor = null;
for (int i = subscriber.EventKeys.Count - 1; i >= 0; i--)
{
string eventKey = subscriber.EventKeys[i];
RemoveEvent(subscriber, eventKey);
}
}
private class SubscriptionState
{
public ISubscriber Subscriber { get; private set; }
public ManualResetEventSlim Initialized { get; private set; }
public SubscriptionState(ISubscriber subscriber)
{
Initialized = new ManualResetEventSlim();
Subscriber = subscriber;
}
}
}
}

View File

@@ -0,0 +1,90 @@
// 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.Threading;
using System.Threading.Tasks;
using Microsoft.AspNet.SignalR.Infrastructure;
namespace Microsoft.AspNet.SignalR.Messaging
{
public static class MessageBusExtensions
{
public static Task Publish(this IMessageBus bus, string source, string key, string value)
{
if (bus == null)
{
throw new ArgumentNullException("bus");
}
if (source == null)
{
throw new ArgumentNullException("source");
}
if (String.IsNullOrEmpty(key))
{
throw new ArgumentNullException("key");
}
return bus.Publish(new Message(source, key, value));
}
internal static Task Ack(this IMessageBus bus, string connectionId, string commandId)
{
// Prepare the ack
var message = new Message(connectionId, PrefixHelper.GetAck(connectionId), null);
message.CommandId = commandId;
message.IsAck = true;
return bus.Publish(message);
}
public static void Enumerate(this IList<ArraySegment<Message>> messages, Action<Message> onMessage)
{
if (messages == null)
{
throw new ArgumentNullException("messages");
}
if (onMessage == null)
{
throw new ArgumentNullException("onMessage");
}
Enumerate<object>(messages, message => true, (state, message) => onMessage(message), state: null);
}
public static void Enumerate<T>(this IList<ArraySegment<Message>> messages, Func<Message, bool> filter, Action<T, Message> onMessage, T state)
{
if (messages == null)
{
throw new ArgumentNullException("messages");
}
if (filter == null)
{
throw new ArgumentNullException("filter");
}
if (onMessage == null)
{
throw new ArgumentNullException("onMessage");
}
for (int i = 0; i < messages.Count; i++)
{
ArraySegment<Message> segment = messages[i];
for (int j = segment.Offset; j < segment.Offset + segment.Count; j++)
{
Message message = segment.Array[j];
if (filter(message))
{
onMessage(state, message);
}
}
}
}
}
}

View File

@@ -0,0 +1,44 @@
// 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.Linq;
namespace Microsoft.AspNet.SignalR.Messaging
{
/// <summary>
///
/// </summary>
[SuppressMessage("Microsoft.Performance", "CA1815:OverrideEqualsAndOperatorEqualsOnValueTypes", Justification = "Messages are never compared")]
public struct MessageResult
{
private static readonly List<ArraySegment<Message>> _emptyList = new List<ArraySegment<Message>>();
public readonly static MessageResult TerminalMessage = new MessageResult(terminal: true);
[SuppressMessage("Microsoft.Design", "CA1006:DoNotNestGenericTypesInMemberSignatures", Justification = "This is an optimization to avoid allocations.")]
public IList<ArraySegment<Message>> Messages { get; private set; }
public int TotalCount { get; private set; }
public bool Terminal { get; set; }
public MessageResult(bool terminal)
: this(_emptyList, 0)
{
Terminal = terminal;
}
/// <summary>
/// Initializes a new instance of the <see cref="MessageResult"/> struct.
/// </summary>
/// <param name="messages">The array of messages associated with this <see cref="MessageResult"/>.</param>
/// <param name="totalCount">The amount of messages populated in the messages array.</param>
public MessageResult(IList<ArraySegment<Message>> messages, int totalCount)
: this()
{
Messages = messages;
TotalCount = totalCount;
}
}
}

View File

@@ -0,0 +1,209 @@
// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved. See License.md in the project root for license information.
using System;
using System.Threading;
namespace Microsoft.AspNet.SignalR.Messaging
{
// Represents a message store that is backed by a ring buffer.
public sealed class MessageStore<T> where T : class
{
private static readonly uint _minFragmentCount = 4;
private static readonly uint _maxFragmentSize = (IntPtr.Size == 4) ? (uint)16384 : (uint)8192; // guarantees that fragments never end up in the LOH
private static readonly ArraySegment<T> _emptyArraySegment = new ArraySegment<T>(new T[0]);
private readonly uint _offset;
private Fragment[] _fragments;
private readonly uint _fragmentSize;
private long _nextFreeMessageId;
// Creates a message store with the specified capacity. The actual capacity will be *at least* the
// specified value. That is, GetMessages may return more data than 'capacity'.
public MessageStore(uint capacity, uint offset)
{
// set a minimum capacity
if (capacity < 32)
{
capacity = 32;
}
_offset = offset;
// Dynamically choose an appropriate number of fragments and the size of each fragment.
// This is chosen to avoid allocations on the large object heap and to minimize contention
// in the store. We allocate a small amount of additional space to act as an overflow
// buffer; this increases throughput of the data structure.
checked
{
uint fragmentCount = Math.Max(_minFragmentCount, capacity / _maxFragmentSize);
_fragmentSize = Math.Min((capacity + fragmentCount - 1) / fragmentCount, _maxFragmentSize);
_fragments = new Fragment[fragmentCount + 1]; // +1 for the overflow buffer
}
}
public MessageStore(uint capacity)
: this(capacity, offset: 0)
{
}
// only for testing purposes
[System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Design", "CA1024:UsePropertiesWhereAppropriate", Justification = "Only for testing")]
public ulong GetMessageCount()
{
return (ulong)Volatile.Read(ref _nextFreeMessageId);
}
// Adds a message to the store. Returns the ID of the newly added message.
public ulong Add(T message)
{
// keep looping in TryAddImpl until it succeeds
ulong newMessageId;
while (!TryAddImpl(message, out newMessageId)) ;
// When TryAddImpl succeeds, record the fact that a message was just added to the
// store. We increment the next free id rather than set it explicitly since
// multiple threads might be trying to write simultaneously. There is a nifty
// side effect to this: _nextFreeMessageId will *always* return the total number
// of messages that *all* threads agree have ever been added to the store. (The
// actual number may be higher, but this field will eventually catch up as threads
// flush data.)
Interlocked.Increment(ref _nextFreeMessageId);
return newMessageId;
}
private void GetFragmentOffsets(ulong messageId, out ulong fragmentNum, out int idxIntoFragmentsArray, out int idxIntoFragment)
{
fragmentNum = messageId / _fragmentSize;
// from the bucket number, we can figure out where in _fragments this data sits
idxIntoFragmentsArray = (int)(fragmentNum % (uint)_fragments.Length);
idxIntoFragment = (int)(messageId % _fragmentSize);
}
private ulong GetMessageId(ulong fragmentNum, uint offset)
{
return fragmentNum * _fragmentSize + offset;
}
// Gets the next batch of messages, beginning with the specified ID.
// This function may return an empty array or an array of length greater than the capacity
// specified in the ctor. The client may also miss messages. See MessageStoreResult.
public MessageStoreResult<T> GetMessages(ulong firstMessageId, int maxMessages)
{
return GetMessagesImpl(firstMessageId, maxMessages);
}
private MessageStoreResult<T> GetMessagesImpl(ulong firstMessageIdRequestedByClient, int maxMessages)
{
ulong nextFreeMessageId = (ulong)Volatile.Read(ref _nextFreeMessageId);
// Case 1:
// The client is already up-to-date with the message store, so we return no data.
if (nextFreeMessageId <= firstMessageIdRequestedByClient)
{
return new MessageStoreResult<T>(firstMessageIdRequestedByClient, _emptyArraySegment, hasMoreData: false);
}
// look for the fragment containing the start of the data requested by the client
ulong fragmentNum;
int idxIntoFragmentsArray, idxIntoFragment;
GetFragmentOffsets(firstMessageIdRequestedByClient, out fragmentNum, out idxIntoFragmentsArray, out idxIntoFragment);
Fragment thisFragment = _fragments[idxIntoFragmentsArray];
ulong firstMessageIdInThisFragment = GetMessageId(thisFragment.FragmentNum, offset: _offset);
ulong firstMessageIdInNextFragment = firstMessageIdInThisFragment + _fragmentSize;
// Case 2:
// This fragment contains the first part of the data the client requested.
if (firstMessageIdInThisFragment <= firstMessageIdRequestedByClient && firstMessageIdRequestedByClient < firstMessageIdInNextFragment)
{
int count = (int)(Math.Min(nextFreeMessageId, firstMessageIdInNextFragment) - firstMessageIdRequestedByClient);
// Limit the number of messages the caller sees
count = Math.Min(count, maxMessages);
ArraySegment<T> retMessages = new ArraySegment<T>(thisFragment.Data, idxIntoFragment, count);
return new MessageStoreResult<T>(firstMessageIdRequestedByClient, retMessages, hasMoreData: (nextFreeMessageId > firstMessageIdInNextFragment));
}
// Case 3:
// The client has missed messages, so we need to send him the earliest fragment we have.
while (true)
{
GetFragmentOffsets(nextFreeMessageId, out fragmentNum, out idxIntoFragmentsArray, out idxIntoFragment);
Fragment tailFragment = _fragments[(idxIntoFragmentsArray + 1) % _fragments.Length];
if (tailFragment.FragmentNum < fragmentNum)
{
firstMessageIdInThisFragment = GetMessageId(tailFragment.FragmentNum, offset: _offset);
int count = Math.Min(maxMessages, tailFragment.Data.Length);
return new MessageStoreResult<T>(firstMessageIdInThisFragment, new ArraySegment<T>(tailFragment.Data, 0, count), hasMoreData: true);
}
nextFreeMessageId = (ulong)Volatile.Read(ref _nextFreeMessageId);
}
}
private bool TryAddImpl(T message, out ulong newMessageId)
{
ulong nextFreeMessageId = (ulong)Volatile.Read(ref _nextFreeMessageId);
// locate the fragment containing the next free id, which is where we should write
ulong fragmentNum;
int idxIntoFragmentsArray, idxIntoFragment;
GetFragmentOffsets(nextFreeMessageId, out fragmentNum, out idxIntoFragmentsArray, out idxIntoFragment);
Fragment fragment = _fragments[idxIntoFragmentsArray];
if (fragment == null || fragment.FragmentNum < fragmentNum)
{
// the fragment is outdated (or non-existent) and must be replaced
if (idxIntoFragment == 0)
{
// this thread is responsible for creating the fragment
Fragment newFragment = new Fragment(fragmentNum, _fragmentSize);
newFragment.Data[0] = message;
Fragment existingFragment = Interlocked.CompareExchange(ref _fragments[idxIntoFragmentsArray], newFragment, fragment);
if (existingFragment == fragment)
{
newMessageId = GetMessageId(fragmentNum, offset: _offset);
return true;
}
}
// another thread is responsible for updating the fragment, so fall to bottom of method
}
else if (fragment.FragmentNum == fragmentNum)
{
// the fragment is valid, and we can just try writing into it until we reach the end of the fragment
T[] fragmentData = fragment.Data;
for (int i = idxIntoFragment; i < fragmentData.Length; i++)
{
T originalMessage = Interlocked.CompareExchange(ref fragmentData[i], message, null);
if (originalMessage == null)
{
newMessageId = GetMessageId(fragmentNum, offset: (uint)i);
return true;
}
}
// another thread used the last open space in this fragment, so fall to bottom of method
}
// failure; caller will retry operation
newMessageId = 0;
return false;
}
private sealed class Fragment
{
public readonly ulong FragmentNum;
public readonly T[] Data;
public Fragment(ulong fragmentNum, uint fragmentSize)
{
FragmentNum = fragmentNum;
Data = new T[fragmentSize];
}
}
}
}

View File

@@ -0,0 +1,53 @@
// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved. See License.md in the project root for license information.
using System;
using System.Diagnostics.CodeAnalysis;
namespace Microsoft.AspNet.SignalR.Messaging
{
// Represents the result of a call to MessageStore<T>.GetMessages.
[SuppressMessage("Microsoft.Performance", "CA1815:OverrideEqualsAndOperatorEqualsOnValueTypes", Justification = "This is never compared")]
public struct MessageStoreResult<T> where T : class
{
// The first message ID in the result set. Messages in the result set have sequentually increasing IDs.
// If FirstMessageId = 20 and Messages.Length = 4, then the messages have IDs { 20, 21, 22, 23 }.
private readonly ulong _firstMessageId;
// If this is true, the backing MessageStore contains more messages, and the client should call GetMessages again.
private readonly bool _hasMoreData;
// The actual result set. May be empty.
private readonly ArraySegment<T> _messages;
public MessageStoreResult(ulong firstMessageId, ArraySegment<T> messages, bool hasMoreData)
{
_firstMessageId = firstMessageId;
_messages = messages;
_hasMoreData = hasMoreData;
}
public ulong FirstMessageId
{
get
{
return _firstMessageId;
}
}
public bool HasMoreData
{
get
{
return _hasMoreData;
}
}
public ArraySegment<T> Messages
{
get
{
return _messages;
}
}
}
}

View File

@@ -0,0 +1,39 @@
// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved. See License.md in the project root for license information.
using System;
namespace Microsoft.AspNet.SignalR.Messaging
{
/// <summary>
/// Common settings for scale-out message bus implementations.
/// </summary>
public class ScaleoutConfiguration
{
public static readonly int DisableQueuing = 0;
private int _maxQueueLength;
/// <summary>
/// The maximum length of the outgoing send queue. Messages being sent to the backplane are queued
/// up to this length. After the max length is reached, further sends will throw an <see cref="System.InvalidOperationException">InvalidOperationException</see>.
/// Set to <see cref="Microsoft.AspNet.SignalR.Messaging.ScaleoutConfiguration.DisableQueuing">ScaleoutConfiguration.DisableQueuing</see> to disable queing.
/// Defaults to disabled.
/// </summary>
public virtual int MaxQueueLength
{
get
{
return _maxQueueLength;
}
set
{
if (value < 0)
{
throw new ArgumentOutOfRangeException("value");
}
_maxQueueLength = value;
}
}
}
}

View File

@@ -0,0 +1,37 @@
// 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 Microsoft.AspNet.SignalR.Infrastructure;
namespace Microsoft.AspNet.SignalR.Messaging
{
public class ScaleoutMapping
{
public ScaleoutMapping(ulong id, ScaleoutMessage message)
: this(id, message, ListHelper<LocalEventKeyInfo>.Empty)
{
}
public ScaleoutMapping(ulong id, ScaleoutMessage message, IList<LocalEventKeyInfo> localKeyInfo)
{
if (message == null)
{
throw new ArgumentNullException("message");
}
if (localKeyInfo == null)
{
throw new ArgumentNullException("localKeyInfo");
}
Id = id;
LocalKeyInfo = localKeyInfo;
ServerCreationTime = message.ServerCreationTime;
}
public ulong Id { get; private set; }
public IList<LocalEventKeyInfo> LocalKeyInfo { get; private set; }
public DateTime ServerCreationTime { get; private set; }
}
}

View File

@@ -0,0 +1,124 @@
// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved. See License.md in the project root for license information.
using System;
using System.Collections;
using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis;
namespace Microsoft.AspNet.SignalR.Messaging
{
public class ScaleoutMappingStore
{
private const int MaxMessages = 1000000;
private ScaleoutStore _store;
public ScaleoutMappingStore()
{
_store = new ScaleoutStore(MaxMessages);
}
public void Add(ulong id, ScaleoutMessage message, IList<LocalEventKeyInfo> localKeyInfo)
{
if (MaxMapping != null && id < MaxMapping.Id)
{
_store = new ScaleoutStore(MaxMessages);
}
_store.Add(new ScaleoutMapping(id, message, localKeyInfo));
}
public ScaleoutMapping MaxMapping
{
get
{
return _store.MaxMapping;
}
}
public IEnumerator<ScaleoutMapping> GetEnumerator(ulong id)
{
MessageStoreResult<ScaleoutMapping> result = _store.GetMessagesByMappingId(id);
return new ScaleoutStoreEnumerator(_store, result);
}
private struct ScaleoutStoreEnumerator : IEnumerator<ScaleoutMapping>, IEnumerator
{
private readonly WeakReference _storeReference;
private MessageStoreResult<ScaleoutMapping> _result;
private int _offset;
private int _length;
private ulong _nextId;
public ScaleoutStoreEnumerator(ScaleoutStore store, MessageStoreResult<ScaleoutMapping> result)
: this()
{
_storeReference = new WeakReference(store);
Initialize(result);
}
public ScaleoutMapping Current
{
get
{
return _result.Messages.Array[_offset];
}
}
public void Dispose()
{
}
object IEnumerator.Current
{
get { return Current; }
}
public bool MoveNext()
{
_offset++;
if (_offset < _length)
{
return true;
}
if (!_result.HasMoreData)
{
return false;
}
// If the store falls out of scope
var store = (ScaleoutStore)_storeReference.Target;
if (store == null)
{
return false;
}
// Get the next result
MessageStoreResult<ScaleoutMapping> result = store.GetMessages(_nextId);
Initialize(result);
_offset++;
return _offset < _length;
}
public void Reset()
{
throw new NotSupportedException();
}
private void Initialize(MessageStoreResult<ScaleoutMapping> result)
{
_result = result;
_offset = _result.Messages.Offset - 1;
_length = _result.Messages.Offset + _result.Messages.Count;
_nextId = _result.FirstMessageId + (ulong)_result.Messages.Count;
}
}
}
}

View File

@@ -0,0 +1,75 @@
using System;
using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis;
using System.Globalization;
using System.IO;
namespace Microsoft.AspNet.SignalR.Messaging
{
/// <summary>
/// Represents a message to the scaleout backplane
/// </summary>
public class ScaleoutMessage
{
public ScaleoutMessage(IList<Message> messages)
{
Messages = messages;
ServerCreationTime = DateTime.UtcNow;
}
public ScaleoutMessage()
{
}
/// <summary>
/// The messages from SignalR
/// </summary>
[SuppressMessage("Microsoft.Usage", "CA2227:CollectionPropertiesShouldBeReadOnly", Justification = "This type is used for serialization")]
public IList<Message> Messages { get; set; }
/// <summary>
/// The time the message was created on the origin server
/// </summary>
public DateTime ServerCreationTime { get; set; }
public byte[] ToBytes()
{
using (var ms = new MemoryStream())
{
var binaryWriter = new BinaryWriter(ms);
binaryWriter.Write(Messages.Count);
for (int i = 0; i < Messages.Count; i++)
{
Messages[i].WriteTo(ms);
}
binaryWriter.Write(ServerCreationTime.Ticks);
return ms.ToArray();
}
}
public static ScaleoutMessage FromBytes(byte[] data)
{
if (data == null)
{
throw new ArgumentNullException("data");
}
using (var stream = new MemoryStream(data))
{
var binaryReader = new BinaryReader(stream);
var message = new ScaleoutMessage();
message.Messages = new List<Message>();
int count = binaryReader.ReadInt32();
for (int i = 0; i < count; i++)
{
message.Messages.Add(Message.ReadFrom(stream));
}
message.ServerCreationTime = new DateTime(binaryReader.ReadInt64());
return message;
}
}
}
}

View File

@@ -0,0 +1,233 @@
// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved. See License.md in the project root for license information.
using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Diagnostics;
using System.Diagnostics.CodeAnalysis;
using System.Linq;
using System.Threading.Tasks;
using Microsoft.AspNet.SignalR.Infrastructure;
using Microsoft.AspNet.SignalR.Tracing;
namespace Microsoft.AspNet.SignalR.Messaging
{
/// <summary>
/// Common base class for scaleout message bus implementations.
/// </summary>
public abstract class ScaleoutMessageBus : MessageBus
{
private readonly SipHashBasedStringEqualityComparer _sipHashBasedComparer = new SipHashBasedStringEqualityComparer(0, 0);
private readonly TraceSource _trace;
private readonly Lazy<ScaleoutStreamManager> _streamManager;
private readonly IPerformanceCounterManager _perfCounters;
protected ScaleoutMessageBus(IDependencyResolver resolver, ScaleoutConfiguration configuration)
: base(resolver)
{
if (configuration == null)
{
throw new ArgumentNullException("configuration");
}
var traceManager = resolver.Resolve<ITraceManager>();
_trace = traceManager["SignalR." + typeof(ScaleoutMessageBus).Name];
_perfCounters = resolver.Resolve<IPerformanceCounterManager>();
_streamManager = new Lazy<ScaleoutStreamManager>(() => new ScaleoutStreamManager(Send, OnReceivedCore, StreamCount, _trace, _perfCounters, configuration));
}
/// <summary>
/// The number of streams can't change for the lifetime of this instance.
/// </summary>
protected virtual int StreamCount
{
get
{
return 1;
}
}
private ScaleoutStreamManager StreamManager
{
get
{
return _streamManager.Value;
}
}
/// <summary>
/// Opens the specified queue for sending messages.
/// <param name="streamIndex">The index of the stream to open.</param>
/// </summary>
protected void Open(int streamIndex)
{
StreamManager.Open(streamIndex);
}
/// <summary>
/// Closes the specified queue.
/// <param name="streamIndex">The index of the stream to close.</param>
/// </summary>
protected void Close(int streamIndex)
{
StreamManager.Close(streamIndex);
}
/// <summary>
/// Closes the specified queue for sending messages making all sends fail asynchronously.
/// </summary>
/// <param name="streamIndex">The index of the stream to close.</param>
/// <param name="exception">The error that occurred.</param>
protected void OnError(int streamIndex, Exception exception)
{
StreamManager.OnError(streamIndex, exception);
}
/// <summary>
/// Sends messages to the backplane
/// </summary>
/// <param name="messages">The list of messages to send</param>
/// <returns></returns>
protected virtual Task Send(IList<Message> messages)
{
// If we're only using a single stream then just send
if (StreamCount == 1)
{
return StreamManager.Send(0, messages);
}
var taskCompletionSource = new TaskCompletionSource<object>();
// Group messages by source (connection id)
var messagesBySource = messages.GroupBy(m => m.Source);
SendImpl(messagesBySource.GetEnumerator(), taskCompletionSource);
return taskCompletionSource.Task;
}
protected virtual Task Send(int streamIndex, IList<Message> messages)
{
throw new NotImplementedException();
}
[SuppressMessage("Microsoft.Design", "CA1031:DoNotCatchGeneralExceptionTypes", Justification = "We return a faulted tcs")]
private void SendImpl(IEnumerator<IGrouping<string, Message>> enumerator, TaskCompletionSource<object> taskCompletionSource)
{
send:
if (!enumerator.MoveNext())
{
taskCompletionSource.TrySetResult(null);
}
else
{
IGrouping<string, Message> group = enumerator.Current;
// Get the channel index we're going to use for this message
int index = (int)((uint)_sipHashBasedComparer.GetHashCode(group.Key) % StreamCount);
Debug.Assert(index >= 0, "Hash function resulted in an index < 0.");
Task sendTask = StreamManager.Send(index, group.ToArray()).Catch();
if (sendTask.IsCompleted)
{
try
{
sendTask.Wait();
goto send;
}
catch (Exception ex)
{
taskCompletionSource.SetUnwrappedException(ex);
}
}
else
{
sendTask.Then((enumer, tcs) => SendImpl(enumer, tcs), enumerator, taskCompletionSource)
.ContinueWithNotComplete(taskCompletionSource);
}
}
}
/// <summary>
/// Invoked when a payload is received from the backplane. There should only be one active call at any time.
/// </summary>
/// <param name="streamIndex">id of the stream.</param>
/// <param name="id">id of the payload within that stream.</param>
/// <param name="message">The scaleout message.</param>
/// <returns></returns>
protected virtual void OnReceived(int streamIndex, ulong id, ScaleoutMessage message)
{
StreamManager.OnReceived(streamIndex, id, message);
}
[SuppressMessage("Microsoft.Design", "CA1062:Validate arguments of public methods", MessageId = "2", Justification = "Called from derived class")]
[SuppressMessage("Microsoft.Design", "CA1062:Validate arguments of public methods", MessageId = "0", Justification = "Called from derived class")]
private void OnReceivedCore(int streamIndex, ulong id, ScaleoutMessage scaleoutMessage)
{
Counters.ScaleoutMessageBusMessagesReceivedPerSec.IncrementBy(scaleoutMessage.Messages.Count);
_trace.TraceInformation("OnReceived({0}, {1}, {2})", streamIndex, id, scaleoutMessage.Messages.Count);
var localMapping = new LocalEventKeyInfo[scaleoutMessage.Messages.Count];
var keys = new HashSet<string>();
for (var i = 0; i < scaleoutMessage.Messages.Count; ++i)
{
Message message = scaleoutMessage.Messages[i];
// Remember where this message came from
message.MappingId = id;
message.StreamIndex = streamIndex;
keys.Add(message.Key);
ulong localId = Save(message);
MessageStore<Message> messageStore = Topics[message.Key].Store;
localMapping[i] = new LocalEventKeyInfo(message.Key, localId, messageStore);
}
// Get the stream for this payload
ScaleoutMappingStore store = StreamManager.Streams[streamIndex];
// Publish only after we've setup the mapping fully
store.Add(id, scaleoutMessage, localMapping);
// Schedule after we're done
foreach (var eventKey in keys)
{
ScheduleEvent(eventKey);
}
}
public override Task Publish(Message message)
{
Counters.MessageBusMessagesPublishedTotal.Increment();
Counters.MessageBusMessagesPublishedPerSec.Increment();
// TODO: Implement message batching here
return Send(new[] { message });
}
protected override void Dispose(bool disposing)
{
// Close all streams
for (int i = 0; i < StreamCount; i++)
{
Close(i);
}
base.Dispose(disposing);
}
[SuppressMessage("Microsoft.Design", "CA1062:Validate arguments of public methods", MessageId = "0", Justification = "Called from derived class")]
protected override Subscription CreateSubscription(ISubscriber subscriber, string cursor, Func<MessageResult, object, Task<bool>> callback, int messageBufferSize, object state)
{
return new ScaleoutSubscription(subscriber.Identity, subscriber.EventKeys, cursor, StreamManager.Streams, callback, messageBufferSize, Counters, state);
}
}
}

View File

@@ -0,0 +1,441 @@
using System;
using System.Diagnostics.CodeAnalysis;
using System.Threading;
using Microsoft.AspNet.SignalR.Infrastructure;
namespace Microsoft.AspNet.SignalR.Messaging
{
// Represents a message store that is backed by a ring buffer.
[SuppressMessage("Microsoft.Design", "CA1001:TypesThatOwnDisposableFieldsShouldBeDisposable", Justification = "The rate sampler doesn't need to be disposed")]
public sealed class ScaleoutStore
{
private const uint _minFragmentCount = 4;
[SuppressMessage("Microsoft.Performance", "CA1802:UseLiteralsWhereAppropriate", Justification = "It's conditional based on architecture")]
private static readonly uint _maxFragmentSize = (IntPtr.Size == 4) ? (uint)16384 : (uint)8192; // guarantees that fragments never end up in the LOH
private static readonly ArraySegment<ScaleoutMapping> _emptyArraySegment = new ArraySegment<ScaleoutMapping>(new ScaleoutMapping[0]);
private Fragment[] _fragments;
private readonly uint _fragmentSize;
private long _minMessageId;
private long _nextFreeMessageId;
private ulong _minMappingId;
private ScaleoutMapping _maxMapping;
// Creates a message store with the specified capacity. The actual capacity will be *at least* the
// specified value. That is, GetMessages may return more data than 'capacity'.
public ScaleoutStore(uint capacity)
{
// set a minimum capacity
if (capacity < 32)
{
capacity = 32;
}
// Dynamically choose an appropriate number of fragments and the size of each fragment.
// This is chosen to avoid allocations on the large object heap and to minimize contention
// in the store. We allocate a small amount of additional space to act as an overflow
// buffer; this increases throughput of the data structure.
checked
{
uint fragmentCount = Math.Max(_minFragmentCount, capacity / _maxFragmentSize);
_fragmentSize = Math.Min((capacity + fragmentCount - 1) / fragmentCount, _maxFragmentSize);
_fragments = new Fragment[fragmentCount + 1]; // +1 for the overflow buffer
}
}
internal ulong MinMappingId
{
get
{
return _minMappingId;
}
}
public ScaleoutMapping MaxMapping
{
get
{
return _maxMapping;
}
}
public uint FragmentSize
{
get
{
return _fragmentSize;
}
}
public int FragmentCount
{
get
{
return _fragments.Length;
}
}
// Adds a message to the store. Returns the ID of the newly added message.
public ulong Add(ScaleoutMapping mapping)
{
// keep looping in TryAddImpl until it succeeds
ulong newMessageId;
while (!TryAddImpl(mapping, out newMessageId)) ;
// When TryAddImpl succeeds, record the fact that a message was just added to the
// store. We increment the next free id rather than set it explicitly since
// multiple threads might be trying to write simultaneously. There is a nifty
// side effect to this: _nextFreeMessageId will *always* return the total number
// of messages that *all* threads agree have ever been added to the store. (The
// actual number may be higher, but this field will eventually catch up as threads
// flush data.)
Interlocked.Increment(ref _nextFreeMessageId);
return newMessageId;
}
private void GetFragmentOffsets(ulong messageId, out ulong fragmentNum, out int idxIntoFragmentsArray, out int idxIntoFragment)
{
fragmentNum = messageId / _fragmentSize;
// from the bucket number, we can figure out where in _fragments this data sits
idxIntoFragmentsArray = (int)(fragmentNum % (uint)_fragments.Length);
idxIntoFragment = (int)(messageId % _fragmentSize);
}
private int GetFragmentOffset(ulong messageId)
{
ulong fragmentNum = messageId / _fragmentSize;
return (int)(fragmentNum % (uint)_fragments.Length);
}
private ulong GetMessageId(ulong fragmentNum, uint offset)
{
return fragmentNum * _fragmentSize + offset;
}
private bool TryAddImpl(ScaleoutMapping mapping, out ulong newMessageId)
{
ulong nextFreeMessageId = (ulong)Volatile.Read(ref _nextFreeMessageId);
// locate the fragment containing the next free id, which is where we should write
ulong fragmentNum;
int idxIntoFragmentsArray, idxIntoFragment;
GetFragmentOffsets(nextFreeMessageId, out fragmentNum, out idxIntoFragmentsArray, out idxIntoFragment);
Fragment fragment = _fragments[idxIntoFragmentsArray];
if (fragment == null || fragment.FragmentNum < fragmentNum)
{
// the fragment is outdated (or non-existent) and must be replaced
bool overwrite = fragment != null && fragment.FragmentNum < fragmentNum;
if (idxIntoFragment == 0)
{
// this thread is responsible for creating the fragment
Fragment newFragment = new Fragment(fragmentNum, _fragmentSize);
newFragment.Data[0] = mapping;
Fragment existingFragment = Interlocked.CompareExchange(ref _fragments[idxIntoFragmentsArray], newFragment, fragment);
if (existingFragment == fragment)
{
newMessageId = GetMessageId(fragmentNum, offset: 0);
newFragment.MinId = newMessageId;
newFragment.Length = 1;
newFragment.MaxId = GetMessageId(fragmentNum, offset: _fragmentSize - 1);
_maxMapping = mapping;
// Move the minimum id when we overwrite
if (overwrite)
{
_minMessageId = (long)(existingFragment.MaxId + 1);
_minMappingId = existingFragment.MaxId;
}
else if (idxIntoFragmentsArray == 0)
{
_minMappingId = mapping.Id;
}
return true;
}
}
// another thread is responsible for updating the fragment, so fall to bottom of method
}
else if (fragment.FragmentNum == fragmentNum)
{
// the fragment is valid, and we can just try writing into it until we reach the end of the fragment
ScaleoutMapping[] fragmentData = fragment.Data;
for (int i = idxIntoFragment; i < fragmentData.Length; i++)
{
ScaleoutMapping originalMapping = Interlocked.CompareExchange(ref fragmentData[i], mapping, null);
if (originalMapping == null)
{
newMessageId = GetMessageId(fragmentNum, offset: (uint)i);
fragment.Length++;
_maxMapping = fragmentData[i];
return true;
}
}
// another thread used the last open space in this fragment, so fall to bottom of method
}
// failure; caller will retry operation
newMessageId = 0;
return false;
}
public MessageStoreResult<ScaleoutMapping> GetMessages(ulong firstMessageIdRequestedByClient)
{
ulong nextFreeMessageId = (ulong)Volatile.Read(ref _nextFreeMessageId);
// Case 1:
// The client is already up-to-date with the message store, so we return no data.
if (nextFreeMessageId <= firstMessageIdRequestedByClient)
{
return new MessageStoreResult<ScaleoutMapping>(firstMessageIdRequestedByClient, _emptyArraySegment, hasMoreData: false);
}
// look for the fragment containing the start of the data requested by the client
ulong fragmentNum;
int idxIntoFragmentsArray, idxIntoFragment;
GetFragmentOffsets(firstMessageIdRequestedByClient, out fragmentNum, out idxIntoFragmentsArray, out idxIntoFragment);
Fragment thisFragment = _fragments[idxIntoFragmentsArray];
ulong firstMessageIdInThisFragment = GetMessageId(thisFragment.FragmentNum, offset: 0);
ulong firstMessageIdInNextFragment = firstMessageIdInThisFragment + _fragmentSize;
// Case 2:
// This fragment contains the first part of the data the client requested.
if (firstMessageIdInThisFragment <= firstMessageIdRequestedByClient && firstMessageIdRequestedByClient < firstMessageIdInNextFragment)
{
int count = (int)(Math.Min(nextFreeMessageId, firstMessageIdInNextFragment) - firstMessageIdRequestedByClient);
var retMessages = new ArraySegment<ScaleoutMapping>(thisFragment.Data, idxIntoFragment, count);
return new MessageStoreResult<ScaleoutMapping>(firstMessageIdRequestedByClient, retMessages, hasMoreData: (nextFreeMessageId > firstMessageIdInNextFragment));
}
// Case 3:
// The client has missed messages, so we need to send him the earliest fragment we have.
while (true)
{
GetFragmentOffsets(nextFreeMessageId, out fragmentNum, out idxIntoFragmentsArray, out idxIntoFragment);
Fragment tailFragment = _fragments[(idxIntoFragmentsArray + 1) % _fragments.Length];
if (tailFragment.FragmentNum < fragmentNum)
{
firstMessageIdInThisFragment = GetMessageId(tailFragment.FragmentNum, offset: 0);
return new MessageStoreResult<ScaleoutMapping>(firstMessageIdInThisFragment, new ArraySegment<ScaleoutMapping>(tailFragment.Data, 0, tailFragment.Length), hasMoreData: true);
}
nextFreeMessageId = (ulong)Volatile.Read(ref _nextFreeMessageId);
}
}
public MessageStoreResult<ScaleoutMapping> GetMessagesByMappingId(ulong mappingId)
{
var minMessageId = (ulong)Volatile.Read(ref _minMessageId);
int idxIntoFragment;
// look for the fragment containing the start of the data requested by the client
Fragment thisFragment;
if (TryGetFragmentFromMappingId(mappingId, out thisFragment))
{
int lastSearchIndex;
ulong lastSearchId;
if (thisFragment.TrySearch(mappingId,
out idxIntoFragment,
out lastSearchIndex,
out lastSearchId))
{
// Skip the first message
idxIntoFragment++;
ulong firstMessageIdRequestedByClient = GetMessageId(thisFragment.FragmentNum, (uint)idxIntoFragment);
return GetMessages(firstMessageIdRequestedByClient);
}
else
{
if (mappingId > lastSearchId)
{
lastSearchIndex++;
}
var segment = new ArraySegment<ScaleoutMapping>(thisFragment.Data,
lastSearchIndex,
thisFragment.Length - lastSearchIndex);
var firstMessageIdInThisFragment = GetMessageId(thisFragment.FragmentNum, offset: (uint)lastSearchIndex);
return new MessageStoreResult<ScaleoutMapping>(firstMessageIdInThisFragment,
segment,
hasMoreData: true);
}
}
// If we're expired or we're at the first mapping or we're lower than the
// min then get everything
if (mappingId < _minMappingId || mappingId == UInt64.MaxValue)
{
return GetAllMessages(minMessageId);
}
// We're up to date so do nothing
return new MessageStoreResult<ScaleoutMapping>(0, _emptyArraySegment, hasMoreData: false);
}
private MessageStoreResult<ScaleoutMapping> GetAllMessages(ulong minMessageId)
{
ulong fragmentNum;
int idxIntoFragmentsArray, idxIntoFragment;
GetFragmentOffsets(minMessageId, out fragmentNum, out idxIntoFragmentsArray, out idxIntoFragment);
Fragment fragment = _fragments[idxIntoFragmentsArray];
if (fragment == null)
{
return new MessageStoreResult<ScaleoutMapping>(minMessageId, _emptyArraySegment, hasMoreData: false);
}
var firstMessageIdInThisFragment = GetMessageId(fragment.FragmentNum, offset: 0);
var messages = new ArraySegment<ScaleoutMapping>(fragment.Data, 0, fragment.Length);
return new MessageStoreResult<ScaleoutMapping>(firstMessageIdInThisFragment, messages, hasMoreData: true);
}
internal bool TryGetFragmentFromMappingId(ulong mappingId, out Fragment fragment)
{
long low = _minMessageId;
long high = _nextFreeMessageId;
while (low <= high)
{
var mid = (ulong)((low + high) / 2);
int midOffset = GetFragmentOffset(mid);
fragment = _fragments[midOffset];
if (fragment == null)
{
return false;
}
if (mappingId < fragment.MinValue)
{
high = (long)(fragment.MinId - 1);
}
else if (mappingId > fragment.MaxValue)
{
low = (long)(fragment.MaxId + 1);
}
else if (fragment.HasValue(mappingId))
{
return true;
}
}
fragment = null;
return false;
}
internal sealed class Fragment
{
public readonly ulong FragmentNum;
public readonly ScaleoutMapping[] Data;
public int Length;
public ulong MinId;
public ulong MaxId;
public Fragment(ulong fragmentNum, uint fragmentSize)
{
FragmentNum = fragmentNum;
Data = new ScaleoutMapping[fragmentSize];
}
public ulong? MinValue
{
get
{
var mapping = Data[0];
if (mapping != null)
{
return mapping.Id;
}
return null;
}
}
public ulong? MaxValue
{
get
{
ScaleoutMapping mapping = null;
if (Length == 0)
{
mapping = Data[Length];
}
else
{
mapping = Data[Length - 1];
}
if (mapping != null)
{
return mapping.Id;
}
return null;
}
}
public bool HasValue(ulong id)
{
return id >= MinValue && id <= MaxValue;
}
public bool TrySearch(ulong id, out int index, out int lastSearchIndex, out ulong lastSearchId)
{
lastSearchIndex = 0;
lastSearchId = id;
var low = 0;
var high = Length;
while (low <= high)
{
int mid = (low + high) / 2;
ScaleoutMapping mapping = Data[mid];
lastSearchIndex = mid;
lastSearchId = mapping.Id;
if (id < mapping.Id)
{
high = mid - 1;
}
else if (id > mapping.Id)
{
low = mid + 1;
}
else if (id == mapping.Id)
{
index = mid;
return true;
}
}
index = -1;
return false;
}
}
}
}

View File

@@ -0,0 +1,316 @@
// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved. See License.md in the project root for license information.
using System;
using System.Diagnostics;
using System.Diagnostics.CodeAnalysis;
using System.Threading.Tasks;
using Microsoft.AspNet.SignalR.Infrastructure;
namespace Microsoft.AspNet.SignalR.Messaging
{
internal class ScaleoutStream
{
private TaskCompletionSource<object> _taskCompletionSource;
private TaskQueue _queue;
private StreamState _state;
private Exception _error;
private readonly int _size;
private readonly TraceSource _trace;
private readonly string _tracePrefix;
private readonly IPerformanceCounterManager _perfCounters;
private readonly object _lockObj = new object();
public ScaleoutStream(TraceSource trace, string tracePrefix, int size, IPerformanceCounterManager performanceCounters)
{
if (trace == null)
{
throw new ArgumentNullException("trace");
}
_trace = trace;
_tracePrefix = tracePrefix;
_size = size;
_perfCounters = performanceCounters;
InitializeCore();
}
private bool UsingTaskQueue
{
get
{
return _size > 0;
}
}
public void Open()
{
lock (_lockObj)
{
if (ChangeState(StreamState.Open))
{
_perfCounters.ScaleoutStreamCountOpen.Increment();
_perfCounters.ScaleoutStreamCountBuffering.Decrement();
_error = null;
if (UsingTaskQueue)
{
_taskCompletionSource.TrySetResult(null);
}
}
}
}
public Task Send(Func<object, Task> send, object state)
{
lock (_lockObj)
{
if (_error != null)
{
throw _error;
}
// If the queue is closed then stop sending
if (_state == StreamState.Closed)
{
throw new InvalidOperationException(Resources.Error_StreamClosed);
}
if (_state == StreamState.Initial)
{
throw new InvalidOperationException(Resources.Error_StreamNotOpen);
}
var context = new SendContext(this, send, state);
if (UsingTaskQueue)
{
Task task = _queue.Enqueue(Send, context);
if (task == null)
{
// The task is null if the queue is full
throw new InvalidOperationException(Resources.Error_TaskQueueFull);
}
// Always observe the task in case the user doesn't handle it
return task.Catch();
}
_perfCounters.ScaleoutSendQueueLength.Increment();
return Send(context).Finally(counter =>
{
((IPerformanceCounter)counter).Decrement();
},
_perfCounters.ScaleoutSendQueueLength);
}
}
public void SetError(Exception error)
{
Trace("Error has happened with the following exception: {0}.", error);
lock (_lockObj)
{
_perfCounters.ScaleoutErrorsTotal.Increment();
_perfCounters.ScaleoutErrorsPerSec.Increment();
Buffer();
_error = error;
}
}
public void Close()
{
Task task = TaskAsyncHelper.Empty;
lock (_lockObj)
{
if (ChangeState(StreamState.Closed))
{
_perfCounters.ScaleoutStreamCountOpen.RawValue = 0;
_perfCounters.ScaleoutStreamCountBuffering.RawValue = 0;
if (UsingTaskQueue)
{
// Ensure the queue is started
EnsureQueueStarted();
// Drain the queue to stop all sends
task = Drain(_queue);
}
}
}
if (UsingTaskQueue)
{
// Block until the queue is drained so no new work can be done
task.Wait();
}
}
private static Task Send(object state)
{
var context = (SendContext)state;
context.InvokeSend().Then(tcs =>
{
// Complete the task if the send is successful
tcs.TrySetResult(null);
},
context.TaskCompletionSource)
.Catch((ex, obj) =>
{
var ctx = (SendContext)obj;
ctx.Stream.Trace("Send failed: {0}", ex);
lock (ctx.Stream._lockObj)
{
// Set the queue into buffering state
ctx.Stream.SetError(ex.InnerException);
// Otherwise just set this task as failed
ctx.TaskCompletionSource.TrySetUnwrappedException(ex);
}
},
context);
return context.TaskCompletionSource.Task;
}
private void Buffer()
{
lock (_lockObj)
{
if (ChangeState(StreamState.Buffering))
{
_perfCounters.ScaleoutStreamCountOpen.Decrement();
_perfCounters.ScaleoutStreamCountBuffering.Increment();
InitializeCore();
}
}
}
private void InitializeCore()
{
if (UsingTaskQueue)
{
Task task = DrainQueue();
_queue = new TaskQueue(task, _size);
_queue.QueueSizeCounter = _perfCounters.ScaleoutSendQueueLength;
}
}
private Task DrainQueue()
{
// If the tcs is null or complete then create a new one
if (_taskCompletionSource == null ||
_taskCompletionSource.Task.IsCompleted)
{
_taskCompletionSource = new TaskCompletionSource<object>();
}
if (_queue != null)
{
// Drain the queue when the new queue is open
return _taskCompletionSource.Task.Then(q => Drain(q), _queue);
}
// Nothing to drain
return _taskCompletionSource.Task;
}
private void EnsureQueueStarted()
{
if (_taskCompletionSource != null)
{
_taskCompletionSource.TrySetResult(null);
}
}
private bool ChangeState(StreamState newState)
{
// Do nothing if the state is closed
if (_state == StreamState.Closed)
{
return false;
}
if (_state != newState)
{
Trace("Changed state from {0} to {1}", _state, newState);
_state = newState;
return true;
}
return false;
}
private static Task Drain(TaskQueue queue)
{
if (queue == null)
{
return TaskAsyncHelper.Empty;
}
var tcs = new TaskCompletionSource<object>();
queue.Drain().Catch().ContinueWith(task =>
{
tcs.SetResult(null);
});
return tcs.Task;
}
private void Trace(string value, params object[] args)
{
_trace.TraceInformation(_tracePrefix + " - " + value, args);
}
private class SendContext
{
private readonly Func<object, Task> _send;
private readonly object _state;
public readonly ScaleoutStream Stream;
public readonly TaskCompletionSource<object> TaskCompletionSource;
public SendContext(ScaleoutStream stream, Func<object, Task> send, object state)
{
Stream = stream;
TaskCompletionSource = new TaskCompletionSource<object>();
_send = send;
_state = state;
}
[SuppressMessage("Microsoft.Design", "CA1031:DoNotCatchGeneralExceptionTypes", Justification = "The exception flows to the caller")]
public Task InvokeSend()
{
try
{
return _send(_state);
}
catch (Exception ex)
{
return TaskAsyncHelper.FromError(ex);
}
}
}
private enum StreamState
{
Initial,
Open,
Buffering,
Closed
}
}
}

View File

@@ -0,0 +1,98 @@
// 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.Collections.ObjectModel;
using System.Diagnostics;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.AspNet.SignalR.Infrastructure;
namespace Microsoft.AspNet.SignalR.Messaging
{
internal class ScaleoutStreamManager
{
private readonly Func<int, IList<Message>, Task> _send;
private readonly Action<int, ulong, ScaleoutMessage> _receive;
private readonly ScaleoutStream[] _streams;
public ScaleoutStreamManager(Func<int, IList<Message>, Task> send,
Action<int, ulong, ScaleoutMessage> receive,
int streamCount,
TraceSource trace,
IPerformanceCounterManager performanceCounters,
ScaleoutConfiguration configuration)
{
_streams = new ScaleoutStream[streamCount];
_send = send;
_receive = receive;
var receiveMapping = new ScaleoutMappingStore[streamCount];
performanceCounters.ScaleoutStreamCountTotal.RawValue = streamCount;
performanceCounters.ScaleoutStreamCountBuffering.RawValue = streamCount;
performanceCounters.ScaleoutStreamCountOpen.RawValue = 0;
for (int i = 0; i < streamCount; i++)
{
_streams[i] = new ScaleoutStream(trace, "Stream(" + i + ")", configuration.MaxQueueLength, performanceCounters);
receiveMapping[i] = new ScaleoutMappingStore();
}
Streams = new ReadOnlyCollection<ScaleoutMappingStore>(receiveMapping);
}
public IList<ScaleoutMappingStore> Streams { get; private set; }
public void Open(int streamIndex)
{
_streams[streamIndex].Open();
}
public void Close(int streamIndex)
{
_streams[streamIndex].Close();
}
public void OnError(int streamIndex, Exception exception)
{
_streams[streamIndex].SetError(exception);
}
public Task Send(int streamIndex, IList<Message> messages)
{
var context = new SendContext(this, streamIndex, messages);
return _streams[streamIndex].Send(state => Send(state), context);
}
public void OnReceived(int streamIndex, ulong id, ScaleoutMessage message)
{
_receive(streamIndex, id, message);
// We assume if a message has come in then the stream is open
Open(streamIndex);
}
private static Task Send(object state)
{
var context = (SendContext)state;
return context.StreamManager._send(context.Index, context.Messages);
}
private class SendContext
{
public ScaleoutStreamManager StreamManager;
public int Index;
public IList<Message> Messages;
public SendContext(ScaleoutStreamManager scaleoutStream, int index, IList<Message> messages)
{
StreamManager = scaleoutStream;
Index = index;
Messages = messages;
}
}
}
}

View File

@@ -0,0 +1,275 @@
// 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.Globalization;
using System.IO;
using System.Threading.Tasks;
using Microsoft.AspNet.SignalR.Infrastructure;
namespace Microsoft.AspNet.SignalR.Messaging
{
public class ScaleoutSubscription : Subscription
{
private readonly IList<ScaleoutMappingStore> _streams;
private readonly List<Cursor> _cursors;
public ScaleoutSubscription(string identity,
IList<string> eventKeys,
string cursor,
IList<ScaleoutMappingStore> streams,
Func<MessageResult, object, Task<bool>> callback,
int maxMessages,
IPerformanceCounterManager counters,
object state)
: base(identity, eventKeys, callback, maxMessages, counters, state)
{
if (streams == null)
{
throw new ArgumentNullException("streams");
}
_streams = streams;
List<Cursor> cursors = null;
if (String.IsNullOrEmpty(cursor))
{
cursors = new List<Cursor>();
}
else
{
cursors = Cursor.GetCursors(cursor);
// If the streams don't match the cursors then throw it out
if (cursors.Count != _streams.Count)
{
cursors.Clear();
}
}
// No cursors so we need to populate them from the list of streams
if (cursors.Count == 0)
{
for (int streamIndex = 0; streamIndex < _streams.Count; streamIndex++)
{
AddCursorForStream(streamIndex, cursors);
}
}
_cursors = cursors;
}
public override void WriteCursor(TextWriter textWriter)
{
Cursor.WriteCursors(textWriter, _cursors);
}
[SuppressMessage("Microsoft.Design", "CA1002:DoNotExposeGenericLists", Justification = "The list needs to be populated")]
[SuppressMessage("Microsoft.Design", "CA1062:Validate arguments of public methods", MessageId = "0", Justification = "It is called from the base class")]
protected override void PerformWork(IList<ArraySegment<Message>> items, out int totalCount, out object state)
{
// The list of cursors represent (streamid, payloadid)
var nextCursors = new ulong?[_cursors.Count];
totalCount = 0;
// Get the enumerator so that we can extract messages for this subscription
IEnumerator<Tuple<ScaleoutMapping, int>> enumerator = GetMappings().GetEnumerator();
while (totalCount < MaxMessages && enumerator.MoveNext())
{
ScaleoutMapping mapping = enumerator.Current.Item1;
int streamIndex = enumerator.Current.Item2;
ulong? nextCursor = nextCursors[streamIndex];
// Only keep going with this stream if the cursor we're looking at is bigger than
// anything we already processed
if (nextCursor == null || mapping.Id > nextCursor)
{
ulong mappingId = ExtractMessages(streamIndex, mapping, items, ref totalCount);
// Update the cursor id
nextCursors[streamIndex] = mappingId;
}
}
state = nextCursors;
}
protected override void BeforeInvoke(object state)
{
// Update the list of cursors before invoking anything
var nextCursors = (ulong?[])state;
for (int i = 0; i < _cursors.Count; i++)
{
// Only update non-null entries
ulong? nextCursor = nextCursors[i];
if (nextCursor.HasValue)
{
Cursor cursor = _cursors[i];
cursor.Id = nextCursor.Value;
}
}
}
private IEnumerable<Tuple<ScaleoutMapping, int>> GetMappings()
{
var enumerators = new List<CachedStreamEnumerator>();
for (var streamIndex = 0; streamIndex < _streams.Count; ++streamIndex)
{
// Get the mapping for this stream
ScaleoutMappingStore store = _streams[streamIndex];
Cursor cursor = _cursors[streamIndex];
// Try to find a local mapping for this payload
var enumerator = new CachedStreamEnumerator(store.GetEnumerator(cursor.Id),
streamIndex);
enumerators.Add(enumerator);
}
while (enumerators.Count > 0)
{
ScaleoutMapping minMapping = null;
CachedStreamEnumerator minEnumerator = null;
for (int i = enumerators.Count - 1; i >= 0; i--)
{
CachedStreamEnumerator enumerator = enumerators[i];
ScaleoutMapping mapping;
if (enumerator.TryMoveNext(out mapping))
{
if (minMapping == null || mapping.ServerCreationTime < minMapping.ServerCreationTime)
{
minMapping = mapping;
minEnumerator = enumerator;
}
}
else
{
enumerators.RemoveAt(i);
}
}
if (minMapping != null)
{
minEnumerator.ClearCachedValue();
yield return Tuple.Create(minMapping, minEnumerator.StreamIndex);
}
}
}
private ulong ExtractMessages(int streamIndex, ScaleoutMapping mapping, IList<ArraySegment<Message>> items, ref int totalCount)
{
// For each of the event keys we care about, extract all of the messages
// from the payload
lock (EventKeys)
{
for (var i = 0; i < EventKeys.Count; ++i)
{
string eventKey = EventKeys[i];
for (int j = 0; j < mapping.LocalKeyInfo.Count; j++)
{
LocalEventKeyInfo info = mapping.LocalKeyInfo[j];
if (info.MessageStore != null && info.Key.Equals(eventKey, StringComparison.OrdinalIgnoreCase))
{
MessageStoreResult<Message> storeResult = info.MessageStore.GetMessages(info.Id, 1);
if (storeResult.Messages.Count > 0)
{
// TODO: Figure out what to do when we have multiple event keys per mapping
Message message = storeResult.Messages.Array[storeResult.Messages.Offset];
// Only add the message to the list if the stream index matches
if (message.StreamIndex == streamIndex)
{
items.Add(storeResult.Messages);
totalCount += storeResult.Messages.Count;
// We got a mapping id bigger than what we expected which
// means we missed messages. Use the new mappingId.
if (message.MappingId > mapping.Id)
{
return message.MappingId;
}
}
else
{
// REVIEW: When the stream indexes don't match should we leave the mapping id as is?
// If we do nothing then we'll end up querying old cursor ids until
// we eventually find a message id that matches this stream index.
}
}
}
}
}
}
return mapping.Id;
}
private void AddCursorForStream(int streamIndex, List<Cursor> cursors)
{
ScaleoutMapping maxMapping = _streams[streamIndex].MaxMapping;
ulong id = UInt64.MaxValue;
string key = streamIndex.ToString(CultureInfo.InvariantCulture);
if (maxMapping != null)
{
id = maxMapping.Id;
}
var newCursor = new Cursor(key, id);
cursors.Add(newCursor);
}
private class CachedStreamEnumerator
{
private readonly IEnumerator<ScaleoutMapping> _enumerator;
private ScaleoutMapping _cachedValue;
public CachedStreamEnumerator(IEnumerator<ScaleoutMapping> enumerator, int streamIndex)
{
_enumerator = enumerator;
StreamIndex = streamIndex;
}
public int StreamIndex { get; private set; }
public bool TryMoveNext(out ScaleoutMapping mapping)
{
mapping = null;
if (_cachedValue != null)
{
mapping = _cachedValue;
return true;
}
if (_enumerator.MoveNext())
{
mapping = _enumerator.Current;
_cachedValue = mapping;
return true;
}
return false;
}
public void ClearCachedValue()
{
_cachedValue = null;
}
}
}
}

View File

@@ -0,0 +1,347 @@
// 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.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);
// Fast Path
if (tcs.Task.IsCompleted)
{
return tcs.Task;
}
return FinishAsync(tcs);
}
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;
}
private static Task FinishAsync(TaskCompletionSource<object> tcs)
{
return tcs.Task.ContinueWith(task =>
{
if (task.IsFaulted)
{
return TaskAsyncHelper.FromError(task.Exception);
}
return TaskAsyncHelper.Empty;
}).FastUnwrap();
}
[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)
{
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.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.
var spinWait = new SpinWait();
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)
{
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;
}
spinWait.SpinOnce();
}
}
}
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;
}
}
}

View File

@@ -0,0 +1,119 @@
// 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.Threading;
namespace Microsoft.AspNet.SignalR.Messaging
{
public class Topic
{
private readonly TimeSpan _lifespan;
// Keeps track of the last time this subscription was used
private DateTime _lastUsed = DateTime.UtcNow;
public IList<ISubscription> Subscriptions { get; private set; }
public MessageStore<Message> Store { get; private set; }
public ReaderWriterLockSlim SubscriptionLock { get; private set; }
// State of the topic
internal int State;
public virtual bool IsExpired
{
get
{
try
{
SubscriptionLock.EnterReadLock();
TimeSpan timeSpan = DateTime.UtcNow - _lastUsed;
return Subscriptions.Count == 0 && timeSpan > _lifespan;
}
finally
{
SubscriptionLock.ExitReadLock();
}
}
}
public DateTime LastUsed
{
get
{
return _lastUsed;
}
}
public Topic(uint storeSize, TimeSpan lifespan)
{
_lifespan = lifespan;
Subscriptions = new List<ISubscription>();
Store = new MessageStore<Message>(storeSize);
SubscriptionLock = new ReaderWriterLockSlim();
}
public void MarkUsed()
{
this._lastUsed = DateTime.UtcNow;
}
public void AddSubscription(ISubscription subscription)
{
if (subscription == null)
{
throw new ArgumentNullException("subscription");
}
try
{
SubscriptionLock.EnterWriteLock();
MarkUsed();
Subscriptions.Add(subscription);
// Created -> HasSubscriptions
Interlocked.CompareExchange(ref State,
TopicState.HasSubscriptions,
TopicState.NoSubscriptions);
}
finally
{
SubscriptionLock.ExitWriteLock();
}
}
public void RemoveSubscription(ISubscription subscription)
{
if (subscription == null)
{
throw new ArgumentNullException("subscription");
}
try
{
SubscriptionLock.EnterWriteLock();
MarkUsed();
Subscriptions.Remove(subscription);
if (Subscriptions.Count == 0)
{
// HasSubscriptions -> NoSubscriptions
Interlocked.CompareExchange(ref State,
TopicState.NoSubscriptions,
TopicState.HasSubscriptions);
}
}
finally
{
SubscriptionLock.ExitWriteLock();
}
}
}
}

View File

@@ -0,0 +1,98 @@
// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved. See License.md in the project root for license information.
using System;
using System.Collections;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Linq;
using Microsoft.AspNet.SignalR.Infrastructure;
namespace Microsoft.AspNet.SignalR.Messaging
{
public sealed class TopicLookup : IEnumerable<KeyValuePair<string, Topic>>
{
// General topics
private readonly ConcurrentDictionary<string, Topic> _topics = new ConcurrentDictionary<string, Topic>();
// All group topics
private readonly ConcurrentDictionary<string, Topic> _groupTopics = new ConcurrentDictionary<string, Topic>(new SipHashBasedStringEqualityComparer());
public int Count
{
get
{
return _topics.Count + _groupTopics.Count;
}
}
public Topic this[string key]
{
get
{
Topic topic;
if (TryGetValue(key, out topic))
{
return topic;
}
return null;
}
}
public bool ContainsKey(string key)
{
if (PrefixHelper.HasGroupPrefix(key))
{
return _groupTopics.ContainsKey(key);
}
return _topics.ContainsKey(key);
}
public bool TryGetValue(string key, out Topic topic)
{
if (PrefixHelper.HasGroupPrefix(key))
{
return _groupTopics.TryGetValue(key, out topic);
}
return _topics.TryGetValue(key, out topic);
}
public IEnumerator<KeyValuePair<string, Topic>> GetEnumerator()
{
return _topics.Concat(_groupTopics).GetEnumerator();
}
IEnumerator IEnumerable.GetEnumerator()
{
return GetEnumerator();
}
public bool TryRemove(string key)
{
Topic topic;
if (PrefixHelper.HasGroupPrefix(key))
{
return _groupTopics.TryRemove(key, out topic);
}
return _topics.TryRemove(key, out topic);
}
public Topic GetOrAdd(string key, Func<string, Topic> factory)
{
if (PrefixHelper.HasGroupPrefix(key))
{
return _groupTopics.GetOrAdd(key, factory);
}
return _topics.GetOrAdd(key, factory);
}
public void Clear()
{
_topics.Clear();
_groupTopics.Clear();
}
}
}

View File

@@ -0,0 +1,12 @@
// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved. See License.md in the project root for license information.
namespace Microsoft.AspNet.SignalR.Messaging
{
internal class TopicState
{
public const int NoSubscriptions = 0;
public const int HasSubscriptions = 1;
public const int Dying = 2;
public const int Dead = 3;
}
}

View File

@@ -0,0 +1,17 @@
// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved. See License.md in the project root for license information.
using System;
using System.Threading;
namespace Microsoft.AspNet.SignalR.Messaging
{
// All methods here are guaranteed both volatile + atomic.
// TODO: Make this use the .NET 4.5 'Volatile' type.
internal static class Volatile
{
public static long Read(ref long location)
{
return Interlocked.Read(ref location);
}
}
}