-
Notifications
You must be signed in to change notification settings - Fork 612

Description
Abstract
An active RabbitMQ.Client.Framing.Impl.AutorecoveringConnection
instance violently opposes any attempt by its enclosing System.AppDomain
to unload itself, resulting an a thrown System.CannotUnloadDomainException
and a subsequent crash of the containing OS process.
Details
Closer analysis shows that the RabbitMQ.Client.Framing.Impl.Connection
instance embedded in an AutorecoveringConnection will experience the call to AppDomain.Unload(...)
as a System.Threading.ThreadAbortException
inside its MainLoop
method, specifically in the call to InboundFrame inboundFrame = _frameHandler.ReadFrame()
.
Because AutorecoveringConnection generally tries to recover from Thread aborts, it will proceed to enter its automatic recovery procedure.
Thus, an active Connection will be restored, and in all likelyhood the persisting reference to an unmanaged resource - namely a network Socket - is what prevents the AppDomain from unloading in an orderly manner.
All of the above was observed while running RabbitMQ.Client 6.0.0 in the .NET 4.6.1 variant inside a WebService in Microsoft Internet Information Server on Windows Server 2016.
Apparently, previous versions of RabbitMQ.Client exposed a means of subscribing Connection instances to the AppDomain.DomainUnload
Event via the Connection.HandleDomainUnload(...)
method, but this method is no longer publicly accessible.
Relevance
The observed behavior is a relevant problem because IIS is in the habit of maintaining multiple AppDomain instances inside an Application Pool and recycling these AppDomains on various occasions, e.g. changes to the web.config, which will currently result in a crash of the associated Application Pool. This hurts scenarios where IIS is used as a bridging technology from WebServices to RabbitMQ.
Please consider modifying the behavior of the AutorecoveringConnection
to explicitly handle AppDomain unload scenarios.
Workaround
In the present state of affairs, the System.CannotUnloadDomainException
can be avoided by disposing all active AutorecoveringConnection instances from within an explicit AppDomain.DomainUnload
Event handler.
Repro
The code snippet attached below will raise the CannotUnloadDomainException
on AppDomain.DomainUnload(...)
unless called with the useWorkaround
parameter.
using System;
using System.Net;
using System.Reflection;
using System.Threading;
using RabbitMQ.Client;
namespace RabbitMQClientCrashOnAppDomainUnload
{
internal class Program
{
// May have to change this to relevant local port.
private const int MyAmqpsPort = 5674;
public class Worker : MarshalByRefObject
{
public void Execute(string virtualHost, string userName, string password, bool useWorkaround)
{
var connectionFactory = new ConnectionFactory() { UserName = userName, Password = password, VirtualHost = virtualHost };
var hostName = Dns.GetHostEntry(string.Empty).HostName;
Console.WriteLine($"Connecting to VirtualHost '{virtualHost}' on host '{hostName}' as user '{userName}'");
var connection = connectionFactory.CreateConnection(new[] { new AmqpTcpEndpoint(hostName, MyAmqpsPort) });
// BEGIN Workaround
if (useWorkaround)
AppDomain.CurrentDomain.DomainUnload += (_, __) => connection.Dispose();
// END Workaround
while (true)
{
Console.Write(".");
Thread.Sleep(TimeSpan.FromMilliseconds(500));
}
}
}
private static void Main(string[] args)
{
if (args.Length < 3 || args.Length > 4)
{
Console.Error.WriteLine($"Usage: {Assembly.GetExecutingAssembly().GetName().Name} <virtualHost> <userName> <password> [\"true\"]");
return;
}
var (virtualHost, userName, password) = (args[0], args[1], args[2]);
var useWorkaround = args.Length == 4 && bool.Parse(args[3]);
var childDomain = AppDomain.CreateDomain("Child");
var workerProxy = (Worker)childDomain.CreateInstanceAndUnwrap(Assembly.GetExecutingAssembly().FullName, $"{typeof(Worker).FullName}");
var workerThread = new Thread(() => workerProxy.Execute(virtualHost, userName, password, useWorkaround));
workerThread.Start();
Thread.Sleep(TimeSpan.FromSeconds(2));
try
{
Console.Out.WriteLine("\n\nUnloading child AppDomain - this may take a couple of seconds");
AppDomain.Unload(childDomain);
Console.Out.WriteLine("\n\nUnloaded child AppDomain without error\n");
}
catch (CannotUnloadAppDomainException ex)
{
Console.Error.WriteLine($"\n\nUnexpectedly received {nameof(CannotUnloadAppDomainException)}: {ex.Message}\n{ex.StackTrace}\n");
}
Console.Out.WriteLine("\nPress any key to finish.");
Console.ReadLine();
}
}
}