Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

NUnit async test causing AppDomainUnloadedException

I have a .NET 4.5 WCF service with async operations. I have integration tests which constructs the service host using NetNamedPipeBinding and hits the operation via a client.

However, each test like this always causes NUnit to report the following:

System.AppDomainUnloadedException: Attempted to access an unloaded AppDomain. 
This can happen if the test(s) started a thread but did not stop it. 
Make sure that all the threads started by the test(s) are stopped before completion.

Everything looks ok to me. Can anyone see what might be causing this? I have a complete code sample on GitHub: https://github.com/devlife/codesamples

like image 450
devlife Avatar asked Mar 07 '14 13:03

devlife


1 Answers

I'm having the same problem. It looks as if the issue are "lenient" completion port threads (in the ThreadPool) that have been used by WCF to handle async IO.

When ServiceHost.Close() is used, it will signal all those threads that work is done, but they won't go away immediately, that is, they may outlive the end of the ServiceHost.Close() operation. Thus, the "shutdown" procedure races with the actual AppDomain unloading induced by NUnit due to the end of the test run.

Basically, a simple Thread.Sleep(<a couple of seconds>) after a ServiceHost.Close() "fixes" this :-)

After much searching around on the internet I couldn't find a robust solution for this issue (for a selection of similar issues, not all due to the same cause though, google "unit test appdomainunloadedexception"), short of having some way to suppress this warning itself.

I tried different bindings and transports (includind the NullTransport), but to no avail.

In the end I settled with this "solution":

static void PreventPrematureAppDomainUnloadHack()
{
    //
    // When NUnit unloads the test AppDomain, the WCF started IO completion port threads might
    // not have exited yet.
    // That leads to AppDomainUnloadedExceptions being raised after all is said and done.
    // While native NUnit, ReSharper oder TestDriven.NET runners don't show these, VSTest (and
    // TFS-Build) does. Resulting in very annoying noise in the form of build/test warnings.
    //
    // The following code _attempts_ to wait for all completion port threads to end. This is not
    // an exact thing one can do, however we mitigate the risk of going wrong by several factors:
    // (1) This code is only used during Unit-Tests and not for production code.
    // (2) It is only called when the AppDomain in question is about to go away anway.
    //     So the risk of someone starting new IO threads while we're waiting is very
    //     low.
    // (3) Finally, we have a timeout in place so that we don't wait forever if something
    //     goes wrong.
    //
    if (AppDomain.CurrentDomain.FriendlyName.StartsWith("test-domain-", StringComparison.Ordinal))
    {
        Console.WriteLine("AppDomainUnloadHack: enabled (use DbgView.exe for details).");
        Trace.WriteLine(string.Format("AppDomainUnloadHack: enabled for domain '{0}'.", AppDomain.CurrentDomain.FriendlyName));

        AppDomain.CurrentDomain.DomainUnload += (sender, args) =>
        {
            int activeIo;
            var sw = Stopwatch.StartNew();
            var timeout = TimeSpan.FromSeconds(3);

            do
            {
                if (sw.Elapsed > timeout)
                {
                    Trace.WriteLine("AppDomainUnloadHack: timeout waiting for threads to complete.");
                    sw.Stop();
                    break;
                }

                Thread.Sleep(5);

                int maxWorkers;
                int availWorkers;
                int maxIo;
                int availIo;
                ThreadPool.GetMaxThreads(out maxWorkers, out maxIo);
                ThreadPool.GetAvailableThreads(out availWorkers, out availIo);
                activeIo = maxIo - availIo;

                Trace.WriteLine(string.Format("AppDomainUnloadHack: active completion port threads: {0}", activeIo));

            } while (activeIo > 0);

            Trace.WriteLine(string.Format("AppDomainUnloadHack: complete after {0}", sw.Elapsed));
        };
    }
}

The timeout of 3 seconds is totally arbitrary and so is the wait of 5ms between each retry. Sometimes I do get a "timeout", but most of the time it works.

I make sure that this code is called once for every test assembly (i.e. through a static ctor of a referenced type).

As usual in such cases YMMV.

like image 178
Christian.K Avatar answered Sep 22 '22 16:09

Christian.K