fixed deadlock + blocked thread detection

This commit is contained in:
Christian Köllner 2020-04-15 21:52:56 +02:00
parent 3252ef97f9
commit 8b88b5c5dd
7 changed files with 213 additions and 20 deletions

View File

@ -1,4 +1,5 @@
using Capnp.Rpc; using Capnp.Rpc;
using Capnp.Util;
using Microsoft.VisualStudio.TestTools.UnitTesting; using Microsoft.VisualStudio.TestTools.UnitTesting;
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
@ -11,7 +12,7 @@ using System.Threading.Tasks.Dataflow;
namespace Capnp.Net.Runtime.Tests namespace Capnp.Net.Runtime.Tests
{ {
[TestClass] [TestClass]
public class General public class General: TestBase
{ {
[TestMethod] [TestMethod]
public void AwaitOrderTest() public void AwaitOrderTest()
@ -92,5 +93,115 @@ namespace Capnp.Net.Runtime.Tests
Assert.IsTrue(t.IsCompleted); Assert.IsTrue(t.IsCompleted);
}; };
} }
[TestMethod]
public void SafeJoinCompletedThread()
{
var thread = new Thread(() =>
{
});
thread.Start();
thread.SafeJoin(null, 1000);
}
[TestMethod]
public void SafeJoinBusyThread()
{
var thread = new Thread(() =>
{
try
{
while (true) ;
}
catch (ThreadInterruptedException)
{
Console.WriteLine("Interrupted");
}
catch (ThreadAbortException)
{
Console.WriteLine("Aborted");
}
});
thread.Start();
thread.SafeJoin(null, 1000);
}
[TestMethod]
public void SafeJoinSleepingThread()
{
var thread = new Thread(() =>
{
try
{
Thread.Sleep(Timeout.Infinite);
}
catch (ThreadInterruptedException)
{
Console.WriteLine("Interrupted");
}
catch (ThreadAbortException)
{
Console.WriteLine("Aborted");
}
});
thread.Start();
thread.SafeJoin(null, 1000);
}
[TestMethod]
public void SafeJoinDeadlockedThread()
{
var lk = new object();
lock (lk)
{
var thread = new Thread(() =>
{
try
{
lock (lk)
{
}
}
catch (ThreadInterruptedException)
{
Console.WriteLine("Interrupted");
}
catch (ThreadAbortException)
{
Console.WriteLine("Aborted");
}
});
thread.Start();
thread.SafeJoin(null, 1000);
}
}
[TestMethod]
public void SafeJoinDefensiveThread()
{
var thread = new Thread(() =>
{
for (; ; )
{
try
{
Thread.Sleep(Timeout.Infinite);
}
catch (ThreadInterruptedException)
{
Console.WriteLine("Interrupted");
}
catch (ThreadAbortException)
{
Console.WriteLine("Aborted");
}
}
});
thread.Start();
thread.SafeJoin(null, 1000);
}
} }
} }

View File

@ -195,7 +195,7 @@ namespace Capnp.Net.Runtime.Tests
} }
[TestMethod] [TestMethod]
public void EagerRace2() public void AwaitNoDeadlock()
{ {
for (int i = 0; i < 100; i++) for (int i = 0; i < 100; i++)
{ {

View File

@ -0,0 +1,3 @@
using System.Runtime.CompilerServices;
[assembly: InternalsVisibleTo("Capnp.Net.Runtime.Tests")]

View File

@ -1,4 +1,5 @@
using Capnp.FrameTracing; using Capnp.FrameTracing;
using Capnp.Util;
using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging;
using System; using System;
using System.IO; using System.IO;
@ -92,6 +93,10 @@ namespace Capnp.Rpc
_pump.Run(); _pump.Run();
} }
catch (ThreadInterruptedException)
{
Logger.LogError($"{Thread.CurrentThread.Name} interrupted at {Environment.StackTrace}");
}
finally finally
{ {
State = ConnectionState.Down; State = ConnectionState.Down;
@ -186,10 +191,7 @@ namespace Capnp.Rpc
Logger.LogError(e, "Failure disposing client"); Logger.LogError(e, "Failure disposing client");
} }
if (_pumpThread != null && !_pumpThread.Join(500)) _pumpThread?.SafeJoin(Logger);
{
Logger.LogError("Unable to join pump thread within timeout");
}
GC.SuppressFinalize(this); GC.SuppressFinalize(this);
} }

View File

@ -1,4 +1,5 @@
using Capnp.FrameTracing; using Capnp.FrameTracing;
using Capnp.Util;
using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging;
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
@ -60,6 +61,8 @@ namespace Capnp.Rpc
class Connection: IConnection class Connection: IConnection
{ {
ILogger Logger { get; } = Logging.CreateLogger<Connection>();
readonly List<IFrameTracer> _tracers = new List<IFrameTracer>(); readonly List<IFrameTracer> _tracers = new List<IFrameTracer>();
readonly TcpRpcServer _server; readonly TcpRpcServer _server;
Stream _stream; Stream _stream;
@ -95,6 +98,10 @@ namespace Capnp.Rpc
Pump.Run(); Pump.Run();
} }
catch (ThreadInterruptedException)
{
Logger.LogError($"{Thread.CurrentThread.Name} interrupted at {Environment.StackTrace}");
}
finally finally
{ {
OutboundEp.Dismiss(); OutboundEp.Dismiss();
@ -199,6 +206,10 @@ namespace Capnp.Rpc
// Listener was stopped. Maybe a little bit rude, but this is // Listener was stopped. Maybe a little bit rude, but this is
// our way of shutting down the acceptor thread. // our way of shutting down the acceptor thread.
} }
catch (ThreadInterruptedException)
{
Logger.LogError($"{Thread.CurrentThread.Name} interrupted at {Environment.StackTrace}");
}
catch (System.Exception exception) catch (System.Exception exception)
{ {
// Any other exception might be due to some other problem. // Any other exception might be due to some other problem.
@ -227,7 +238,7 @@ namespace Capnp.Rpc
{ {
connection.Client.Dispose(); connection.Client.Dispose();
connection.Pump?.Dispose(); connection.Pump?.Dispose();
connection.PumpRunner?.Join(5000); connection.PumpRunner?.SafeJoin(Logger);
} }
_rpcEngine.BootstrapCap = null; _rpcEngine.BootstrapCap = null;

View File

@ -7,17 +7,21 @@ using System.Threading.Tasks;
namespace Capnp.Util namespace Capnp.Util
{ {
public class StrictlyOrderedAwaitTask<T>: INotifyCompletion internal class StrictlyOrderedAwaitTask<T>: INotifyCompletion
{ {
static readonly Action Capsule = () => throw new InvalidProgramException("Not invocable"); class Cover { }
class Seal { }
static readonly Cover s_cover = new Cover();
static readonly Seal s_seal = new Seal();
readonly Task<T> _awaitedTask; readonly Task<T> _awaitedTask;
Action? _state; object? _state;
public StrictlyOrderedAwaitTask(Task<T> awaitedTask) public StrictlyOrderedAwaitTask(Task<T> awaitedTask)
{ {
_awaitedTask = awaitedTask; _awaitedTask = awaitedTask;
AwaitInternal(); _state = s_cover;
} }
public StrictlyOrderedAwaitTask<T> GetAwaiter() public StrictlyOrderedAwaitTask<T> GetAwaiter()
@ -41,41 +45,54 @@ namespace Capnp.Util
Action? continuations; Action? continuations;
do do
{ {
continuations = Interlocked.Exchange(ref _state, null); continuations = (Action?)Interlocked.Exchange(ref _state, null);
continuations?.Invoke(); continuations?.Invoke();
} while (continuations != null); } while (continuations != null);
return Interlocked.CompareExchange(ref _state, Capsule, null) == null; return Interlocked.CompareExchange(ref _state, s_seal, null) == null;
}); });
} }
} }
public void OnCompleted(Action continuation) public void OnCompleted(Action continuation)
{ {
bool first = false;
SpinWait.SpinUntil(() => { SpinWait.SpinUntil(() => {
Action? cur, next; object? cur, next;
cur = Volatile.Read(ref _state); cur = Volatile.Read(ref _state);
first = false;
switch (cur) switch (cur)
{ {
case Cover cover:
next = continuation;
first = true;
break;
case null: case null:
next = continuation; next = continuation;
break; break;
case Action capsule when capsule == Capsule:
continuation();
return true;
case Action action: case Action action:
next = action + continuation; next = action + continuation;
break; break;
default:
continuation();
return true;
} }
return Interlocked.CompareExchange(ref _state, next, cur) == cur; return Interlocked.CompareExchange(ref _state, next, cur) == cur;
}); });
if (first)
{
AwaitInternal();
}
} }
public bool IsCompleted => _awaitedTask.IsCompleted && _state == Capsule; public bool IsCompleted => _awaitedTask.IsCompleted && _state == s_seal;
public T GetResult() => _awaitedTask.GetAwaiter().GetResult(); public T GetResult() => _awaitedTask.GetAwaiter().GetResult();
@ -84,7 +101,7 @@ namespace Capnp.Util
public Task<T> WrappedTask => _awaitedTask; public Task<T> WrappedTask => _awaitedTask;
} }
public static class StrictlyOrderedTaskExtensions internal static class StrictlyOrderedTaskExtensions
{ {
public static StrictlyOrderedAwaitTask<T> EnforceAwaitOrder<T>(this Task<T> task) public static StrictlyOrderedAwaitTask<T> EnforceAwaitOrder<T>(this Task<T> task)
{ {

View File

@ -0,0 +1,49 @@
using Microsoft.Extensions.Logging;
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Text;
using System.Threading;
namespace Capnp.Util
{
internal static class ThreadExtensions
{
class ThreadExtensionsLoggingContext
{
public ILogger Logger { get; } = Logging.CreateLogger<ThreadExtensionsLoggingContext>();
}
static Lazy<ThreadExtensionsLoggingContext> LoggingContext = new Lazy<ThreadExtensionsLoggingContext>(
() => new ThreadExtensionsLoggingContext(),
LazyThreadSafetyMode.PublicationOnly);
public static void SafeJoin(this Thread thread, ILogger? logger = null, int timeout = 5000)
{
if (!thread.Join(timeout))
{
logger ??= LoggingContext.Value.Logger;
string name = thread.Name ?? thread.ManagedThreadId.ToString();
try
{
logger.LogError($"Unable to join thread {name}. Thread is in state {thread.ThreadState}.");
thread.Interrupt();
if (!thread.Join(timeout / 10))
{
logger.LogError($"Still unable to join thread {name} after Interrupt(). Thread is in state {thread.ThreadState}.");
thread.Abort();
if (thread.Join(timeout / 10))
{
logger.LogError($"Still unable to join thread {name} after Abort(). Thread is in state {thread.ThreadState}.");
}
}
}
catch
{
}
}
}
}
}