This message was discovered on microsoft.public.dotnet.framework.
| Aaron |
Hi,
I've implemented an elegant way to provide a plug-in, hierarchical, architecture to our application with sub-application isolation - AppDomains. These AppDomains are loaded and unloaded as neccessary within the application. Both synchronous and asynchronous events that cross AppDomain boundaries (and even machine bouindaries) are used extensively throughout also. These events are often propogated from child applications up the application hierarchy to the root application eg: Log messages.
Recently, I noticed the handle count of the application to climb steadily when AppDomains were instantiated, usually ~20/AppDomain. The handle count does not drop when AppDomains are unloaded. My initial thought was garbage collection. However, even explicit calls to Collect() haven't halted this behaviour.
I call BeginInvoke when firing an event asynchronously without providing a Async callback. Could this the evil in our code? I've looked through the documentation and I can't find anything to indicate that EndInvoke is neccessary. My concern about providing such a callback is that if a child application fires an event and the AppDomain in which the child existed is then unloaded then undesirable behaviour may occur.
I've checked my code to ensure no resources are left open before the unloading of a domain. Is there a known leak associated with the use of AppDomains?
Help!
Aaron.
|
|
| |
| |
| David Levine |
[Original message clipped]
it depends on the object that you use it on. For objects derived from Control it should not be necessary to call EndInvoke. For other objects, especially those doing network IO work, there is a high possibility that a memory leak can occur. if you search through the archives at this site you should find a post discussing this issue.
http://blogs.msdn.com/cbrumme
[Original message clipped]
Not with AppDomains - an old post by Chris Brumme indicated that there is a leak of about 16 bytes per appdomain (or some low number like this)...it would take thousands of loads/unloads before that became a problem.
|
|
| |
| |
| Aaron |
Sijin & David,
Thanks for taking the time to have a look at this for me.
Here is a piece of code that leaks 4 handles per AppDomain:
using System; using System.Runtime; using System.Threading;
namespace AppDomainTest { public class AppDomainClass : MarshalByRefObject { public void Go() { Thread t = new Thread(new ThreadStart(ThreadProc)); t.Start(); } private void ThreadProc() { } }
class AppDomainTest { [STAThread] static void Main(string[] args) { for (int i = 0; i < 50; i++) { Console.WriteLine("Loading AppDomain..."); AppDomain domain = AppDomain.CreateDomain("TempDomain"); Console.WriteLine("Creating instance of AppDomainClass..."); AppDomainClass adc = (AppDomainClass)domain.CreateInstanceAndUnwrap("AppDomainTest", "AppDomainTest.AppDomainClass"); Console.WriteLine("Calling Go()..."); adc.Go(); Thread.Sleep(1000); Console.WriteLine("Unloading domain..."); AppDomain.Unload(domain); Console.WriteLine("Domain unloaded."); } } } }
I have found that the only way to avoid this leak is to explicitly call GC.Collect() from within the context of the AppDomain before unloading it. However, even this method does not alleviate the handle leak that occurs when registering an event callback that spans an AppDomain boundary. Seems like a bug to me...
I guess this isn't a huge issue for alot of people as AppDomains are not used in the way I am using them. Perhaps they were never *intended* to be utilised like this?
Sijin - The handles created are listed as an event with description "Thread synchronisation object" in SysInternals' Process Explorer. This true for both threads and .net events.
Ideas??
Aaron.
"David Levine" <Click here to reveal e-mail address> wrote in message news:<#Click here to reveal e-mail address>... [Original message clipped]
|
|
| |
| |
| David Levine |
My quick take on your example is that all the thread does it run and immediately die as the thread proc returns immediately. When the appdomain is unloaded all running threads that originated in the appdomain are unwound via a ThreadAbort and the thread is terminated, but since they've already terminated this should not be necessary. The thread that calls into the appdomain is unwound to the boundary of the appdomain. You should not need to call GC.Collect either because that is done for you by the runtime within the context of the doomed appdomain.
So it looks to me like the test code is fine. I don't know why this consumes 4 handles per appdomain (but I haven't verified this on my machine either).
I use appdomains but I don't create/destroy them that frequently. I load the app into a secondary appdomain, and then when the user logs out it is unloaded. As you state, this may be a bug. Have you tried this out on the Whidbey bits?
"Aaron" <Click here to reveal e-mail address> wrote in message news:Click here to reveal e-mail address... [Original message clipped]
|
|
| |
| |
| Aaron |
After *extensive* thrashing about it in the code I think I've finally realised what's going on. In my "Application Manager" class I wire up a message event. In this case the event spans app domain boundaries. If I wire up this event the object is never garbage collected, even if I unwire the event. This is where the handle leak was coming from as mutexes consume 2 handles/mutex. Failing to connect the event results in object collection, and subsequent resource deallocation.
Surely someone must know a way around this one??
Aaron.
"David Levine" <Click here to reveal e-mail address> wrote in message news:<Click here to reveal e-mail address>... [Original message clipped]
|
|
| |
| |
| David Levine |
Why don't you unsubscribe when you want to unload the appdomain? You should be able to release handles you allocated yourself.
Also, when you say the event spans appdomain boundaries it isn't clear exactly what you mean. You cannot call directly between appdomains - something must marshal the calls between them. Are you using remoting? Using a cross-appdomain calling mechanism? A mailbox scheme between two threads?
"Aaron" <Click here to reveal e-mail address> wrote in message news:Click here to reveal e-mail address... [Original message clipped]
|
|
| |
| |
| Aaron |
Hi David,
I use AppDomain.CreateInstanceAndUnWrap to create my application, so I'm using a transparent proxy to remote the object.
It might be time for some code (apologies for the length). Note: If I register *any* of the events (MessageEvent, UnhandledException, ProcessExit) the object is never queued for garbage collection, even though I explicitly deregister the class as a listener prior to releasing the reference to it. If I don't register these events then the destructor is called. Weird. This is the offending ApplicationManager code:
using System; using APT.Common.Events; using System.Threading; using System.Runtime.Remoting; using System.Security.Policy; using System.Reflection; using NetMAARS.Data;
namespace AppDomainTest { /// <summary> /// Manages the execution of an application /// </summary> /// <remarks>The class manages the loading and execution of an application object. Applications are loaded into their own application domain</remarks> /// <summary> /// Manages the execution of an application /// </summary> /// <remarks>The class manages the loading and execution of an application object. Applications are loaded into their own application domain</remarks> public class ApplicationManager : MessageFactory { /// <summary> /// application /// </summary> protected Application Application;
/// <summary> /// The application domain in which the application is loaded and executed /// </summary> protected AppDomain domain = null; /// <summary> /// state change mutex /// </summary> private Mutex StateMutex = new Mutex();
/// <summary> /// Ensure no one is acting on the domain at the same time /// </summary> private Mutex DomainMutex = new Mutex(); /// <summary> /// Application state change delegate /// </summary> delegate void ApplicationStateDelegate();
/// <summary> /// Application state /// </summary> private ApplicationState State = ApplicationState.UNKNOWN;
/// <summary> /// Internal runstate flag /// </summary> private bool Running = false; /// <summary> /// The name of the application /// </summary> public string ApplicationName { // get only get { return ApplicationMeta == null ? "<Unknown>" : ApplicationMeta.Name; } }
/// <summary> /// The default start timeout /// </summary> public const int DefaultStartTimeout = 60000;
/// <summary> /// The time to wait for starting /// </summary> public int StartTimeout = DefaultStartTimeout;
/// <summary> /// Message event handler /// </summary> /// <param name="sender">The sending object</param> /// <param name="e">The message event parameters</param> public void OnMessageEvent(object sender, MessageEventArgs e) { // refire FireMessageEvent(e); }
/// <summary> /// Internal application meta /// </summary> protected IApplicationMeta ApplicationMeta; /// <summary> /// Initialise the application /// </summary> protected virtual void InitApplication() { Application.MessageEvent += new MessageEventHandler(this.OnMessageEvent); }
/// <summary> /// Fires a message event /// </summary> /// <param name="Message">The message</param> /// <param name="MessageType">The message type</param> /// <remarks>Caller is defaulted to "ApplicationManager(APPLICATION_NAME)"</remarks> private void FireMessageEvent(string Message, MessageTypeEnum MessageType) { // fire! FireMessageEvent(String.Format("ApplicationManager({0})", ApplicationName), Message, MessageType); }
/// <summary> /// Fires a message event /// </summary> /// <param name="Message">The message event</param> /// <remarks>The message type is defaulted to INFORMATION</remarks> private void FireMessageEvent(string Message) { // fire! FireMessageEvent(Message, MessageTypeEnum.INFORMATION); }
/// <summary> /// Internal start application /// </summary> private void InternalStartApplication() { try { // start it bool bSuccess; if (!Running) { // start it bSuccess = Application.Start(); //FireMessageEvent(String.Format("Application {0} started with return code {1}", ApplicationName, bSuccess, bSuccess ? MessageTypeEnum.INFORMATION : MessageTypeEnum.WARNING)); Running = bSuccess; } else { // tell them FireMessageEvent(String.Format("Application {0} is already running", ApplicationName), MessageTypeEnum.WARNING); } } catch(ThreadAbortException) { // we timed out //FireMessageEvent(String.Format("{0} failed to start in an acceptable amount of time", ApplicationName), MessageTypeEnum.ERROR); } catch(Exception ex) { // oh dear FireMessageEvent(String.Format("An exception has occurred while attempting to start application {0}\n{1}", ApplicationName, ex.ToString()), MessageTypeEnum.ERROR); } }
private AppDomain LoadDomain() { AppDomain iad = null;
try { // one at a time DomainMutex.WaitOne();
AppDomainSetup ads = new AppDomainSetup(); Evidence ade = new Evidence(AppDomain.CurrentDomain.Evidence);
// set the base path ads.ApplicationBase = "file:///" + ApplicationMeta.BaseDirectory; ads.ConfigurationFile = ApplicationMeta.ConfigurationFile; // create the domain iad = AppDomain.CreateDomain(Guid.NewGuid().ToString(), ade, ads);
// add our process exit handler iad.ProcessExit += new EventHandler(OnDefaultApplicationExit); iad.UnhandledException += new UnhandledExceptionEventHandler(iad_UnhandledException); } catch(Exception ex) { // re throw throw new Exception(ex.ToString(), ex); } finally { // set it back DomainMutex.ReleaseMutex(); }
return iad; }
/// <summary> /// Creates the application instance /// </summary> protected virtual void CreateApplication() { // create Application = (Application)domain.CreateInstanceAndUnwrap(ApplicationMeta.AssemblyName, ApplicationMeta.ClassName); } /// <summary> /// Checks the application interface /// </summary> private void CheckApplicationInterface() { if (Application.GetType().GetInterface("NetMAARS.Data.IApplication") == null) throw new Exception("The target type does not implement the IApplication interface"); } /// <summary> /// Starts the application /// </summary> public bool StartApplication() { // success flag bool bSuccess = false; try { // one at a time please StateMutex.WaitOne(); // load the domain domain = LoadDomain(); // load the application into it CreateApplication(); // update the status State = ApplicationState.STOPPED; // ensure it derives from the appropriate class CheckApplicationInterface(); // initialise the application InitApplication(); // update the state State = ApplicationState.STARTING; // start it! ApplicationStateDelegate asd = new ApplicationStateDelegate(InternalStartApplication); // invoke IAsyncResult ar = asd.BeginInvoke(null, null); if (!ar.AsyncWaitHandle.WaitOne(StartTimeout, true)) { FireMessageEvent(String.Format("Application failed to start in the time allocated ({0}ms)", StartTimeout), MessageTypeEnum.ERROR); } // end it asd.EndInvoke(ar); // set the state State = Running ? ApplicationState.ACTIVE : ApplicationState.STOPPED; } catch(Exception ex) { // oh dear FireMessageEvent(String.Format("An exception has occurred while attempting to start application {0}\n{1}", ApplicationMeta.Name, ex.ToString()), MessageTypeEnum.ERROR); // unload the domain UnloadDomain(); } finally { // just set it to the run flag bSuccess = Running; // release the mutex StateMutex.ReleaseMutex(); } // set it return bSuccess; } /// <summary> /// unload the app domain in which the application is running /// </summary> protected void UnloadDomain() { try { DomainMutex.WaitOne(); // set the state State = ApplicationState.UNKNOWN; // ensure its all good if (domain != null) { // no more handlers pls domain.ProcessExit -= new EventHandler(OnDefaultApplicationExit); domain.UnhandledException -= new UnhandledExceptionEventHandler(iad_UnhandledException); // unload it AppDomain.Unload(domain); } } catch(ExecutionEngineException eee) { FireMessageEvent(String.Format("An engine execution exception has occurred while attempting to unload domain for {0}:\n{1}", ApplicationName, eee.ToString()), MessageTypeEnum.ERROR); } catch(Exception ex) { FireMessageEvent(String.Format("An exception has occurred while attempting to unload domain for {0}:\n{1}", ApplicationName, ex.ToString()), MessageTypeEnum.ERROR); } finally { // set it to null domain = null; // set it back DomainMutex.ReleaseMutex(); } }
/// <summary> /// Perform any shutdown tasks /// </summary> protected virtual void ShutdownApplication() { // derived classes may ant to do stuff in here }
/// <summary> /// Internal stop application worker method /// </summary> private void InternalStop() { try { bool bSuccess; // get the success flag if (Running) { // shut it down ShutdownApplication();
// stop it bSuccess = Application.Stop(); // tell everyone what happened FireMessageEvent(String.Format("Application {0} has stopped (result: {1})", ApplicationName, bSuccess), MessageTypeEnum.INFORMATION); // update running flag Running = !bSuccess; } } catch(ThreadAbortException) { // we timed out FireMessageEvent(String.Format("Aborting effort to stop application {0}", ApplicationName), MessageTypeEnum.INFORMATION); } catch(Exception ex) { // uh oh FireMessageEvent(String.Format("An exception has occurred while attempting to stop application {0}:\n{1}", ApplicationName, ex.ToString()), MessageTypeEnum.INFORMATION); } finally { // we're stopped State = ApplicationState.STOPPED; if (Application != null) { // deregister listener Application.MessageEvent -= new MessageEventHandler(OnMessageEvent); } // null it Application = null; } }
/// <summary> /// Stops the application /// </summary> public bool StopApplication() { bool bSuccess = false; try { // one at a time please StateMutex.WaitOne(); // state change State = ApplicationState.STOPPING; // start it! ApplicationStateDelegate asd = new ApplicationStateDelegate(InternalStop); // invoke IAsyncResult ar = asd.BeginInvoke(null, null); if (!ar.AsyncWaitHandle.WaitOne(ApplicationMeta.MaxShutdownWait, true)) { FireMessageEvent(String.Format("Application failed to stop in the time allocated ({0}ms)", ApplicationMeta.MaxShutdownWait), MessageTypeEnum.ERROR); } // end it asd.EndInvoke(ar); // state up State = ApplicationState.STOPPED; // set it bSuccess = !Running; } catch (Exception ex) { // uh oh FireMessageEvent(String.Format("An error has occurred while stopping application {0}\n{1}", ApplicationName, ex.ToString()), MessageTypeEnum.ERROR); } finally { // unload the domain UnloadDomain(); // let others have a go StateMutex.ReleaseMutex(); } // return it return bSuccess; } /// <summary> /// Handles the event raised when the default application domain is shut down /// </summary> /// <param name="sender">The sending object</param> /// <param name="e">Not used</param> public void OnDefaultApplicationExit(object sender, EventArgs e) { FireMessageEvent("The default application domain is exiting and this application domain has not been shut down! Application is now exiting!", MessageTypeEnum.ERROR); StopApplication(); }
/// <summary> /// Constructor /// </summary> /// <param name="applicationmeta">The application meta</param> public ApplicationManager(IApplicationMeta applicationmeta) { State = ApplicationState.UNKNOWN; this.ApplicationMeta = applicationmeta; }
~ApplicationManager() { FireMessageEvent("Destructor called!!"); }
/// <summary> /// Create default application info /// </summary> /// <returns>Current application info</returns> private ApplicationInfo CreateApplicationInfo() { ApplicationInfo ai = new ApplicationInfo();
// init ai.Name = ApplicationName; ai.State = State;
// return return ai; }
/// <summary> /// Rebuild children /// </summary> /// <param name="nai">Application info</param> /// <returns>Rebuilt application info</returns> private ApplicationInfo[] RebuildChildren(ApplicationInfo nai) { // any kids? if (nai.Children == null) return null;
// create the new array to hold the new kids ApplicationInfo[] Children = new ApplicationInfo[nai.Children.Length];
// rebuild for(int i = 0; i < nai.Children.Length; i++) { Children[i] = new ApplicationInfo(); Children[i].Name = nai.Children[i].Name; Children[i].State = nai.Children[i].State; Children[i].Children = RebuildChildren(nai.Children[i]); }
// return return Children; }
/// <summary> /// Gets the application info /// </summary> /// <returns>Application state</returns> public virtual ApplicationInfo GetApplicationInfo() { ApplicationInfo ai = null;
try { // lock it DomainMutex.WaitOne();
// exist? if (domain != null) { //FireMessageEvent(String.Format("State = {0}", Enum.GetName(typeof(ApplicationState), State))); if (State != ApplicationState.STOPPED && State != ApplicationState.UNKNOWN) { // get it ai = Application.GetApplicationInfo(); } else { // create the default info ai = CreateApplicationInfo(); } } } catch(Exception ex) { // oh dear FireMessageEvent(String.Format("An exception occurred while attempting to retrieve application info:\n{0}", ex.ToString()), MessageTypeEnum.ERROR); ai = CreateApplicationInfo(); } finally { // release DomainMutex.ReleaseMutex(); } // return it return ai; }
/// <summary> /// Unhandled exception handler /// </summary> /// <param name="sender">The sending object</param> /// <param name="e">Params</param> public void iad_UnhandledException(object sender, UnhandledExceptionEventArgs e) { // inform FireMessageEvent(String.Format("An unhandled exception has occurred (CLR {0} terminating):\n{0}", ((Exception)e.ExceptionObject).ToString(), e.IsTerminating ? "is" : "is not"), MessageTypeEnum.ERROR);
} } }
|
|
| |
| |
| David Levine |
"Aaron" <Click here to reveal e-mail address> wrote in message news:Click here to reveal e-mail address... [Original message clipped]
Couple of random observations...
You have code like this...
catch(Exception ex) { // re throw throw new Exception(ex.ToString(), ex); }
As written you'd be better off not catching this at all. It would be better to add an error message that added some meaning to it. Simply using the error message from the inner exception is not worth the effort since the message is available to whatever higher level code that catches it.
Also, when you call EndInvoke it is possible for that to throw an exception. That may not be a problem but it could skip over code you intend to execute.
There is nothing in there that leaps out as being wrong, but to be honest this code sample is too long to really get a good feel for, especially since it has references to custom assemblies that only you have, so it cannot be built as it is.
If you could post a simple but complete code sample that reproduces the problem, without requiring external DLLs, then someone could be of more assistance.
|
|
| |
|
|
|
|
|
|
|
| |
| Sijin Joseph |
You can use the Performance Monitor counters on your application to check what are the actual handles that are being created. Thatmight help you to troubleshoot your problem.
Sijin Joseph http://www.indiangeek.net http://weblogs.asp.net/sjoseph
Aaron wrote: [Original message clipped]
|
|
| |
|
|
|
|
|