Commit c6af6cf0 authored by Marc Gravell's avatar Marc Gravell

fully working now; renamed to ChannelMessageQueue

parent 1c2f2834
...@@ -301,22 +301,19 @@ public void TestQuit() ...@@ -301,22 +301,19 @@ public void TestQuit()
Assert.Throws<RedisConnectionException>(() => db.Ping()); Assert.Throws<RedisConnectionException>(() => db.Ping());
watch.Stop(); watch.Stop();
Output.WriteLine("Time to notice quit: {0}ms ({1})", watch.ElapsedMilliseconds, Output.WriteLine("Time to notice quit: {0}ms ({1})", watch.ElapsedMilliseconds,
preserveOrder ? "preserve order" : "any order"); "any order");
Thread.Sleep(20); Thread.Sleep(20);
Debug.WriteLine("Pinging..."); Debug.WriteLine("Pinging...");
Assert.Equal(key, (string)db.StringGet(key)); Assert.Equal(key, (string)db.StringGet(key));
} }
} }
[Theory] [Fact]
[InlineData(true)] public async Task TestSevered()
[InlineData(false)]
public async Task TestSevered(bool preserveOrder)
{ {
SetExpectedAmbientFailureCount(2); SetExpectedAmbientFailureCount(2);
using (var muxer = Create(allowAdmin: true)) using (var muxer = Create(allowAdmin: true))
{ {
muxer.PreserveAsyncOrder = preserveOrder;
var db = muxer.GetDatabase(); var db = muxer.GetDatabase();
string key = Guid.NewGuid().ToString(); string key = Guid.NewGuid().ToString();
db.KeyDelete(key, CommandFlags.FireAndForget); db.KeyDelete(key, CommandFlags.FireAndForget);
...@@ -326,7 +323,7 @@ public async Task TestSevered(bool preserveOrder) ...@@ -326,7 +323,7 @@ public async Task TestSevered(bool preserveOrder)
db.Ping(); db.Ping();
watch.Stop(); watch.Stop();
Output.WriteLine("Time to re-establish: {0}ms ({1})", watch.ElapsedMilliseconds, Output.WriteLine("Time to re-establish: {0}ms ({1})", watch.ElapsedMilliseconds,
preserveOrder ? "preserve order" : "any order"); "any order");
await Task.Delay(2000).ForAwait(); await Task.Delay(2000).ForAwait();
Debug.WriteLine("Pinging..."); Debug.WriteLine("Pinging...");
Assert.Equal(key, db.StringGet(key)); Assert.Equal(key, db.StringGet(key));
......
...@@ -135,8 +135,7 @@ public async Task PubSubGetAllCorrectOrder() ...@@ -135,8 +135,7 @@ public async Task PubSubGetAllCorrectOrder()
const int count = 500000; const int count = 500000;
var syncLock = new object(); var syncLock = new object();
var dataList = new List<int>(count); var data = new List<int>(count);
var dataHash = new HashSet<int>();
var subChannel = await sub.SubscribeAsync(channel); var subChannel = await sub.SubscribeAsync(channel);
await sub.PingAsync(); await sub.PingAsync();
...@@ -147,12 +146,11 @@ async Task RunLoop() ...@@ -147,12 +146,11 @@ async Task RunLoop()
{ {
var work = await subChannel.ReadAsync(); var work = await subChannel.ReadAsync();
int i = int.Parse(Encoding.UTF8.GetString(work.Value)); int i = int.Parse(Encoding.UTF8.GetString(work.Value));
lock (dataList) lock (data)
{ {
dataList.Add(i); data.Add(i);
dataHash.Add(i); if (data.Count == count) break;
if (dataList.Count == count) break; if ((data.Count % 10) == 99) Output.WriteLine(data.Count.ToString());
if ((dataList.Count % 10) == 99) Output.WriteLine(dataList.Count.ToString());
} }
} }
lock (syncLock) lock (syncLock)
...@@ -172,12 +170,11 @@ async Task RunLoop() ...@@ -172,12 +170,11 @@ async Task RunLoop()
// subChannel.Unsubscribe(); // subChannel.Unsubscribe();
if (!Monitor.Wait(syncLock, 20000)) if (!Monitor.Wait(syncLock, 20000))
{ {
throw new TimeoutException("Items: " + dataList.Count); throw new TimeoutException("Items: " + data.Count);
} }
for (int i = 0; i < count; i++) for (int i = 0; i < count; i++)
{ {
Assert.Contains(i, dataHash); Assert.Equal(i, data[i]);
// Assert.Equal(i, data[i]);
} }
} }
} }
......
using System;
using System.Threading;
using System.Threading.Tasks;
using Xunit;
namespace StackExchange.Redis.Tests
{
public class TaskTests
{
#if DEBUG
[Theory]
[InlineData(SourceOrign.NewTCS)]
[InlineData(SourceOrign.Create)]
public void VerifyIsSyncSafe(SourceOrign origin)
{
var source = Create<int>(origin);
// Yes this looks stupid, but it's the proper pattern for how we statically init now
// ...and if we're dropping NET45 support, we can just nuke it all.
#if NET462
Assert.True(TaskSource.IsSyncSafe(source.Task));
#elif NETCOREAPP2_0
Assert.True(TaskSource.IsSyncSafe(source.Task));
#endif
}
private static TaskCompletionSource<T> Create<T>(SourceOrign origin)
{
switch (origin)
{
case SourceOrign.NewTCS: return new TaskCompletionSource<T>();
case SourceOrign.Create: return TaskSource.Create<T>(null);
default: throw new ArgumentOutOfRangeException(nameof(origin));
}
}
[Theory]
// regular framework behaviour: 2 out of 3 cause hijack
[InlineData(SourceOrign.NewTCS, AttachMode.ContinueWith, true)]
[InlineData(SourceOrign.NewTCS, AttachMode.ContinueWithExecSync, false)]
[InlineData(SourceOrign.NewTCS, AttachMode.Await, true)]
// Create is just a wrapper of ^^^; expect the same
[InlineData(SourceOrign.Create, AttachMode.ContinueWith, true)]
[InlineData(SourceOrign.Create, AttachMode.ContinueWithExecSync, false)]
[InlineData(SourceOrign.Create, AttachMode.Await, true)]
public void TestContinuationHijacking(SourceOrign origin, AttachMode attachMode, bool expectHijack)
{
TaskCompletionSource<int> source = Create<int>(origin);
int settingThread = Environment.CurrentManagedThreadId;
var state = new AwaitState();
state.Attach(source.Task, attachMode);
source.TrySetResult(123);
state.Wait(); // waits for the continuation to run
int from = state.Thread;
Assert.NotEqual(-1, from); // not set
if (expectHijack)
{
Assert.True(settingThread != from, $"expected hijack; didn't happen, Origin={settingThread}, Final={from}");
}
else
{
Assert.True(settingThread == from, $"setter was hijacked, Origin={settingThread}, Final={from}");
}
}
public enum SourceOrign
{
NewTCS,
Create
}
public enum AttachMode
{
ContinueWith,
ContinueWithExecSync,
Await
}
private class AwaitState
{
public int Thread => continuationThread;
private volatile int continuationThread = -1;
private readonly ManualResetEventSlim evt = new ManualResetEventSlim();
public void Wait()
{
if (!evt.Wait(5000)) throw new TimeoutException();
}
public void Attach(Task task, AttachMode attachMode)
{
switch (attachMode)
{
case AttachMode.ContinueWith:
task.ContinueWith(Continue);
break;
case AttachMode.ContinueWithExecSync:
task.ContinueWith(Continue, TaskContinuationOptions.ExecuteSynchronously);
break;
case AttachMode.Await:
DoAwait(task);
break;
default:
throw new ArgumentOutOfRangeException(nameof(attachMode));
}
}
private void Continue(Task task)
{
continuationThread = Environment.CurrentManagedThreadId;
evt.Set();
}
private async void DoAwait(Task task)
{
await task.ConfigureAwait(false);
continuationThread = Environment.CurrentManagedThreadId;
evt.Set();
}
}
#endif
}
}
...@@ -30,7 +30,7 @@ internal ChannelMessage(RedisChannel channel, RedisValue value) ...@@ -30,7 +30,7 @@ internal ChannelMessage(RedisChannel channel, RedisValue value)
/// <summary> /// <summary>
/// Represents a message queue of pub/sub notifications /// Represents a message queue of pub/sub notifications
/// </summary> /// </summary>
public sealed class ChannelMessageChannel public sealed class ChannelMessageQueue
{ {
private readonly Channel<ChannelMessage> _channel; private readonly Channel<ChannelMessage> _channel;
private readonly RedisChannel _redisChannel; private readonly RedisChannel _redisChannel;
...@@ -41,19 +41,36 @@ public sealed class ChannelMessageChannel ...@@ -41,19 +41,36 @@ public sealed class ChannelMessageChannel
/// </summary> /// </summary>
public bool IsComplete { get; private set; } public bool IsComplete { get; private set; }
internal ChannelMessageChannel(RedisChannel redisChannel, ISubscriber parent) internal ChannelMessageQueue(RedisChannel redisChannel, ISubscriber parent)
{ {
_redisChannel = redisChannel; _redisChannel = redisChannel;
_parent = parent; _parent = parent;
_channel = Channel.CreateUnbounded<ChannelMessage>(); _channel = Channel.CreateUnbounded<ChannelMessage>(s_ChannelOptions);
_channel.Reader.Completion.ContinueWith( _channel.Reader.Completion.ContinueWith(
(t, state) => ((ChannelMessageChannel)state).IsComplete = true, this, TaskContinuationOptions.ExecuteSynchronously); (t, state) => ((ChannelMessageQueue)state).IsComplete = true, this, TaskContinuationOptions.ExecuteSynchronously);
} }
static readonly UnboundedChannelOptions s_ChannelOptions = new UnboundedChannelOptions
{
SingleWriter = true,
SingleReader = false,
AllowSynchronousContinuations = false,
};
internal void Subscribe(CommandFlags flags) => _parent.Subscribe(_redisChannel, HandleMessage, flags); internal void Subscribe(CommandFlags flags) => _parent.Subscribe(_redisChannel, HandleMessage, flags);
internal Task SubscribeAsync(CommandFlags flags) => _parent.SubscribeAsync(_redisChannel, HandleMessage, flags); internal Task SubscribeAsync(CommandFlags flags) => _parent.SubscribeAsync(_redisChannel, HandleMessage, flags);
private void HandleMessage(RedisChannel channel, RedisValue value) private void HandleMessage(RedisChannel channel, RedisValue value)
=> _channel.Writer.TryWrite(new ChannelMessage(channel, value)); {
var writer = _channel.Writer;
if (channel.IsNull && value.IsNull) // see ForSyncShutdown
{
writer.TryComplete();
}
else
{
writer.TryWrite(new ChannelMessage(channel, value));
}
}
/// <summary> /// <summary>
/// Consume a message from the channel /// Consume a message from the channel
...@@ -82,6 +99,18 @@ internal async Task UnsubscribeAsyncImpl(Exception error = null, CommandFlags fl ...@@ -82,6 +99,18 @@ internal async Task UnsubscribeAsyncImpl(Exception error = null, CommandFlags fl
} }
} }
internal static bool IsOneOf(Action<RedisChannel, RedisValue> handler)
{
try
{
return handler != null && handler.Target is ChannelMessageQueue
&& handler.Method.Name == nameof(HandleMessage);
} catch
{
return false;
}
}
/// <summary> /// <summary>
/// Stop receiving messages on this channel /// Stop receiving messages on this channel
/// </summary> /// </summary>
......
...@@ -73,7 +73,7 @@ public interface ISubscriber : IRedis ...@@ -73,7 +73,7 @@ public interface ISubscriber : IRedis
/// <returns>A channel that represents this source</returns> /// <returns>A channel that represents this source</returns>
/// <remarks>https://redis.io/commands/subscribe</remarks> /// <remarks>https://redis.io/commands/subscribe</remarks>
/// <remarks>https://redis.io/commands/psubscribe</remarks> /// <remarks>https://redis.io/commands/psubscribe</remarks>
ChannelMessageChannel Subscribe(RedisChannel channel, CommandFlags flags = CommandFlags.None); ChannelMessageQueue Subscribe(RedisChannel channel, CommandFlags flags = CommandFlags.None);
/// <summary> /// <summary>
/// Subscribe to perform some operation when a change to the preferred/active node is broadcast. /// Subscribe to perform some operation when a change to the preferred/active node is broadcast.
...@@ -93,7 +93,7 @@ public interface ISubscriber : IRedis ...@@ -93,7 +93,7 @@ public interface ISubscriber : IRedis
/// <returns>A channel that represents this source</returns> /// <returns>A channel that represents this source</returns>
/// <remarks>https://redis.io/commands/subscribe</remarks> /// <remarks>https://redis.io/commands/subscribe</remarks>
/// <remarks>https://redis.io/commands/psubscribe</remarks> /// <remarks>https://redis.io/commands/psubscribe</remarks>
Task<ChannelMessageChannel> SubscribeAsync(RedisChannel channel, CommandFlags flags = CommandFlags.None); Task<ChannelMessageQueue> SubscribeAsync(RedisChannel channel, CommandFlags flags = CommandFlags.None);
/// <summary> /// <summary>
/// Inidicate to which redis server we are actively subscribed for a given channel; returns null if /// Inidicate to which redis server we are actively subscribed for a given channel; returns null if
......
...@@ -7,35 +7,50 @@ internal sealed class MessageCompletable : ICompletable ...@@ -7,35 +7,50 @@ internal sealed class MessageCompletable : ICompletable
{ {
private readonly RedisChannel channel; private readonly RedisChannel channel;
private readonly Action<RedisChannel, RedisValue> handler; private readonly Action<RedisChannel, RedisValue> syncHandler, asyncHandler;
private readonly RedisValue message; private readonly RedisValue message;
public MessageCompletable(RedisChannel channel, RedisValue message, Action<RedisChannel, RedisValue> syncHandler, Action<RedisChannel, RedisValue> asyncHandler)
public MessageCompletable(RedisChannel channel, RedisValue message, Action<RedisChannel, RedisValue> handler)
{ {
this.channel = channel; this.channel = channel;
this.message = message; this.message = message;
this.handler = handler; this.syncHandler = syncHandler;
this.asyncHandler = asyncHandler;
} }
public override string ToString() => (string)channel; public override string ToString() => (string)channel;
public bool TryComplete(bool isAsync) public bool TryComplete(bool isAsync)
{ {
if (handler == null) return true;
if (isAsync) if (isAsync)
{ {
ConnectionMultiplexer.TraceWithoutContext("Invoking...: " + (string)channel, "Subscription"); if (asyncHandler != null)
foreach(Action<RedisChannel, RedisValue> sub in handler.GetInvocationList())
{ {
try { sub.Invoke(channel, message); } ConnectionMultiplexer.TraceWithoutContext("Invoking (async)...: " + (string)channel, "Subscription");
catch { } foreach (Action<RedisChannel, RedisValue> sub in asyncHandler.GetInvocationList())
{
try { sub.Invoke(channel, message); }
catch { }
}
ConnectionMultiplexer.TraceWithoutContext("Invoke complete (async)", "Subscription");
} }
ConnectionMultiplexer.TraceWithoutContext("Invoke complete", "Subscription");
return true; return true;
} }
// needs to be called async (unless there is nothing to do!) else
return false; {
if (syncHandler != null)
{
ConnectionMultiplexer.TraceWithoutContext("Invoking (sync)...: " + (string)channel, "Subscription");
foreach (Action<RedisChannel, RedisValue> sub in syncHandler.GetInvocationList())
{
try { sub.Invoke(channel, message); }
catch { }
}
ConnectionMultiplexer.TraceWithoutContext("Invoke complete (sync)", "Subscription");
}
return asyncHandler != null; // anything async to do?
}
} }
void ICompletable.AppendStormLog(StringBuilder sb) void ICompletable.AppendStormLog(StringBuilder sb)
......
...@@ -31,15 +31,16 @@ internal Task AddSubscription(RedisChannel channel, Action<RedisChannel, RedisVa ...@@ -31,15 +31,16 @@ internal Task AddSubscription(RedisChannel channel, Action<RedisChannel, RedisVa
{ {
if (handler != null) if (handler != null)
{ {
bool asAsync = !ChannelMessageQueue.IsOneOf(handler);
lock (subscriptions) lock (subscriptions)
{ {
if (subscriptions.TryGetValue(channel, out Subscription sub)) if (subscriptions.TryGetValue(channel, out Subscription sub))
{ {
sub.Add(handler); sub.Add(asAsync, handler);
} }
else else
{ {
sub = new Subscription(handler); sub = new Subscription(asAsync, handler);
subscriptions.Add(channel, sub); subscriptions.Add(channel, sub);
var task = sub.SubscribeToServer(this, channel, flags, asyncState, false); var task = sub.SubscribeToServer(this, channel, flags, asyncState, false);
if (task != null) return task; if (task != null) return task;
...@@ -84,7 +85,14 @@ internal Task RemoveAllSubscriptions(CommandFlags flags, object asyncState) ...@@ -84,7 +85,14 @@ internal Task RemoveAllSubscriptions(CommandFlags flags, object asyncState)
{ {
foreach (var pair in subscriptions) foreach (var pair in subscriptions)
{ {
pair.Value.Remove(null); // always wipes var msg = pair.Value.ForSyncShutdown();
if(msg != null)
{
// TODO: execute
}
pair.Value.Remove(true, null);
pair.Value.Remove(false, null);
var task = pair.Value.UnsubscribeFromServer(pair.Key, flags, asyncState, false); var task = pair.Value.UnsubscribeFromServer(pair.Key, flags, asyncState, false);
if (task != null) last = task; if (task != null) last = task;
} }
...@@ -97,7 +105,8 @@ internal Task RemoveSubscription(RedisChannel channel, Action<RedisChannel, Redi ...@@ -97,7 +105,8 @@ internal Task RemoveSubscription(RedisChannel channel, Action<RedisChannel, Redi
{ {
lock (subscriptions) lock (subscriptions)
{ {
if (subscriptions.TryGetValue(channel, out Subscription sub) && sub.Remove(handler)) bool asAsync = ChannelMessageQueue.IsOneOf(handler);
if (subscriptions.TryGetValue(channel, out Subscription sub) && sub.Remove(asAsync, handler))
{ {
subscriptions.Remove(channel); subscriptions.Remove(channel);
var task = sub.UnsubscribeFromServer(channel, flags, asyncState, false); var task = sub.UnsubscribeFromServer(channel, flags, asyncState, false);
...@@ -143,30 +152,46 @@ internal long ValidateSubscriptions() ...@@ -143,30 +152,46 @@ internal long ValidateSubscriptions()
private sealed class Subscription private sealed class Subscription
{ {
private Action<RedisChannel, RedisValue> handler; private Action<RedisChannel, RedisValue> _asyncHandler, _syncHandler;
private ServerEndPoint owner; private ServerEndPoint owner;
public Subscription(Action<RedisChannel, RedisValue> value) => handler = value; public Subscription(bool asAsync, Action<RedisChannel, RedisValue> value)
{
if (asAsync) _asyncHandler = value;
else _syncHandler = value;
}
public void Add(Action<RedisChannel, RedisValue> value) => handler += value; public void Add(bool asAsync, Action<RedisChannel, RedisValue> value)
{
if (asAsync) _asyncHandler += value;
else _syncHandler += value;
}
public ICompletable ForSyncShutdown()
{
var syncHandler = _syncHandler;
return syncHandler == null ? null : new MessageCompletable(default, default, syncHandler, null);
}
public ICompletable ForInvoke(RedisChannel channel, RedisValue message) public ICompletable ForInvoke(RedisChannel channel, RedisValue message)
{ {
var tmp = handler; var syncHandler = _syncHandler;
return tmp == null ? null : new MessageCompletable(channel, message, tmp); var asyncHandler = _asyncHandler;
return (syncHandler == null && asyncHandler == null) ? null : new MessageCompletable(channel, message, syncHandler, asyncHandler);
} }
public bool Remove(Action<RedisChannel, RedisValue> value) public bool Remove(bool asAsync, Action<RedisChannel, RedisValue> value)
{ {
if (value == null) if (value == null)
{ // treat as blanket wipe { // treat as blanket wipe
handler = null; if (asAsync) _asyncHandler = null;
return true; else _syncHandler = null;
} }
else else
{ {
return (handler -= value) == null; if (asAsync) _asyncHandler -= value;
else _syncHandler -= value;
} }
return _syncHandler == null && _asyncHandler == null;
} }
public Task SubscribeToServer(ConnectionMultiplexer multiplexer, RedisChannel channel, CommandFlags flags, object asyncState, bool internalCall) public Task SubscribeToServer(ConnectionMultiplexer multiplexer, RedisChannel channel, CommandFlags flags, object asyncState, bool internalCall)
...@@ -287,9 +312,9 @@ public void Subscribe(RedisChannel channel, Action<RedisChannel, RedisValue> han ...@@ -287,9 +312,9 @@ public void Subscribe(RedisChannel channel, Action<RedisChannel, RedisValue> han
if ((flags & CommandFlags.FireAndForget) == 0) Wait(task); if ((flags & CommandFlags.FireAndForget) == 0) Wait(task);
} }
public ChannelMessageChannel Subscribe(RedisChannel channel, CommandFlags flags = CommandFlags.None) public ChannelMessageQueue Subscribe(RedisChannel channel, CommandFlags flags = CommandFlags.None)
{ {
var c = new ChannelMessageChannel(channel, this); var c = new ChannelMessageQueue(channel, this);
c.Subscribe(flags); c.Subscribe(flags);
return c; return c;
} }
...@@ -300,9 +325,9 @@ public Task SubscribeAsync(RedisChannel channel, Action<RedisChannel, RedisValue ...@@ -300,9 +325,9 @@ public Task SubscribeAsync(RedisChannel channel, Action<RedisChannel, RedisValue
return multiplexer.AddSubscription(channel, handler, flags, asyncState); return multiplexer.AddSubscription(channel, handler, flags, asyncState);
} }
public async Task<ChannelMessageChannel> SubscribeAsync(RedisChannel channel, CommandFlags flags = CommandFlags.None) public async Task<ChannelMessageQueue> SubscribeAsync(RedisChannel channel, CommandFlags flags = CommandFlags.None)
{ {
var c = new ChannelMessageChannel(channel, this); var c = new ChannelMessageQueue(channel, this);
await c.SubscribeAsync(flags); await c.SubscribeAsync(flags);
return c; return c;
} }
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment