This behavior is only triggered by add-in code that results in the AppDomain being in an un-unloadable state. Given this, we recommend the following:
Step 3 above can result in a CannotUnloadAppDomain exception if an AppDomain thread cannot stop execution promptly. If you are writing DLL filters, it is helpful to understand the basics about AppDomain end-of-life and the CannotUnloadAppDomain exception. See the following articles on MSDN for more information: AppDomain.Unload Method CannotUnloadAppDomainException Class
This section examines two tempting patterns that can block the unloading of filter AppDomains (and potentially crash the Scheduler process). Each example provides explanatory notes.
Example 1: Objects left to the GC can hold AppDomain hostage.
To be avoided: IScheduler scheduler = new Scheduler(); scheduler.Connect("localhost"); ISchedulerJob job = scheduler.OpenJob(jobID); // more code here scheduler.Close(); scheduler.Dispose(); << potential trouble here if exceptions cause this to be skipped Preferred: using (IScheduler scheduler = new Scheduler()) { scheduler.Connect("localhost"); ISchedulerJob job = scheduler.OpenJob(jobID); // more code here } Notes: The trouble with Example 1 is that the IDisposable interaface on the remoted object is not honored promptly if there is an exception in the intermediate code. Under these circumstances, the remoting mechanics will keep the AppDomain from being unloadable. The "using" statement is an effective tool that enforces proper cleanup of such complicated objects. There are two vulnerabilities in Example 1: The GC may not get around to disposing of complicated objects before AppDomain unload. If execution flow is interrupted (ie: via exceptions), explicitly coded cleanup may not occur before AppDomain unload. Example 2: Catch blocks can hold AppDomain hostage:
To be avoided:
IScheduler scheduler = new Scheduler(); scheduler.Connect("localhost");
ISchedulerJob job = scheduler.OpenJob(jobID);
// more code here
scheduler.Close(); scheduler.Dispose(); << potential trouble here if exceptions cause this to be skipped
Preferred:
using (IScheduler scheduler = new Scheduler()) { scheduler.Connect("localhost");
// more code here }
Notes: The trouble with Example 1 is that the IDisposable interaface on the remoted object is not honored promptly if there is an exception in the intermediate code. Under these circumstances, the remoting mechanics will keep the AppDomain from being unloadable. The "using" statement is an effective tool that enforces proper cleanup of such complicated objects.
There are two vulnerabilities in Example 1:
Example 2: Catch blocks can hold AppDomain hostage:
To be avoided: public void InfiniteLoopProc(object obj) { try { LogEventMsg("Entering Infinite loop thread proc."); while (true) ; } catch (Exception ex) { LogEventMsg("Exception in infinite loop (about to loop): " + ex.ToString()); while (true) ; } finally { LogEventMsg("InifiniteLoop finally clause. About to loop"); while (true) ; } } public SubmissionFilterResponse FilterSubmission(Stream jobXmlIn, out Stream jobXmlModified) { LogEventMsg("FilterSubmission: about to spawn an infinite looping thread."); ThreadPool.QueueUserWorkItem(new WaitCallback(InfiniteLoopProc)); SubmissionFilterResponse retval = SubmissionFilterResponse.SuccessNoJobChange; jobXmlModified = null; return retval; } Notes: In Example 2 we have an implmenetation of ISubmissionFilter.FilterSubmission() that returns successNoJobChange immediately after spawning a new thread. This new thread enters into some important business logic (here represented by an infinite loop) in a try/catch/finally block. When an attempt to unload the AppDomain is made, all threads are given the abort signal. The "catch all" catch block of the business logic catches the exception and begins performing lengthy cleanup. Here the lengthy cleanup is represented by an infinite loop. Because the thread refuses to exit in a timely fashion, the AppDomain is unable to unload. Note that either the catch or finally clause can effectivly trigger this phenomenon (loops in both for this example).
To be avoided: public void InfiniteLoopProc(object obj) { try { LogEventMsg("Entering Infinite loop thread proc."); while (true) ; } catch (Exception ex) { LogEventMsg("Exception in infinite loop (about to loop): " + ex.ToString());
while (true) ; } finally { LogEventMsg("InifiniteLoop finally clause. About to loop");
while (true) ; } }
public SubmissionFilterResponse FilterSubmission(Stream jobXmlIn, out Stream jobXmlModified) { LogEventMsg("FilterSubmission: about to spawn an infinite looping thread.");
ThreadPool.QueueUserWorkItem(new WaitCallback(InfiniteLoopProc));
SubmissionFilterResponse retval = SubmissionFilterResponse.SuccessNoJobChange;
jobXmlModified = null;
return retval; }
Notes: In Example 2 we have an implmenetation of ISubmissionFilter.FilterSubmission() that returns successNoJobChange immediately after spawning a new thread. This new thread enters into some important business logic (here represented by an infinite loop) in a try/catch/finally block.
When an attempt to unload the AppDomain is made, all threads are given the abort signal. The "catch all" catch block of the business logic catches the exception and begins performing lengthy cleanup. Here the lengthy cleanup is represented by an infinite loop. Because the thread refuses to exit in a timely fashion, the AppDomain is unable to unload. Note that either the catch or finally clause can effectivly trigger this phenomenon (loops in both for this example).