mirror of
https://github.com/Radarr/Radarr.git
synced 2026-04-18 21:35:51 -04:00
imported signalr 1.1.3 into NzbDrone.
This commit is contained in:
@@ -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; }
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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; }
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
@@ -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>;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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];
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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; }
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user