Skip to content

AutorecoveringConnection prevents AppDomain.Unload() of enclosing AppDomain #826

@ghost

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();
        }
    }
}

Metadata

Metadata

Assignees

Type

No type

Projects

No projects

Milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions