//------------------------------------------------------------------------------ // // Copyright (c) Microsoft Corporation. All rights reserved. // //------------------------------------------------------------------------------ namespace System.Web.Hosting { using System; using System.Collections.Concurrent; using System.Collections.Generic; using System.Threading; using System.Web.Util; // Handles calling suspend and resume methods, including issues around // synchronization and timeout handling. internal sealed class SuspendManager { private static readonly TimeSpan _suspendMethodTimeout = TimeSpan.FromSeconds(5); private readonly ConcurrentDictionary _registeredObjects = new ConcurrentDictionary(); public void RegisterObject(ISuspendibleRegisteredObject o) { Debug.Assert(o != null); _registeredObjects[o] = null; } public void UnregisterObject(ISuspendibleRegisteredObject o) { Debug.Assert(o != null); ((IDictionary)_registeredObjects).Remove(o); } // Returns a state object that will be passed to the Resume method. public object Suspend() { // ConcurrentDictionary.Count / IsEmpty are basically as expensive as getting // the entire collection of keys, so we may as well just read the keys anyway. var allRegisteredObjects = _registeredObjects.Keys; return SuspendImpl(allRegisteredObjects); } private static SuspendState SuspendImpl(ICollection allRegisteredObjects) { // Our behavior is: // - We'll call each registered object's suspend method serially. // - All methods have a combined 5 seconds to respond, at which // point we'll forcibly return to our caller. // - If a Resume call comes in, we'll not call any Suspend methods // we haven't yet gotten around to, and we'll execute each // resume callback we got. // - Resume callbacks may fire in parallel, even if Suspend methods // fire sequentially. // - Resume methods fire asynchronously, so other events (such as // Stop or a new Suspend call) could happen while a Resume callback // is in progress. CountdownEvent countdownEvent = new CountdownEvent(2); SuspendState suspendState = new SuspendState(allRegisteredObjects); // Unsafe QUWI since occurs outside the context of a request. // We are not concerned about impersonation, identity, etc. // Invoke any registered subscribers to let them know that we're about // to suspend. This is done in parallel with ASP.NET's own cleanup below. if (allRegisteredObjects.Count > 0) { ThreadPool.UnsafeQueueUserWorkItem(_ => { suspendState.Suspend(); countdownEvent.Signal(); }, null); } else { countdownEvent.Signal(); // nobody is subscribed } // Release any unnecessary memory that we're holding on to. The GC will // be able to reclaim these, which means that we'll have to page in less // memory when the next request comes in. ThreadPool.UnsafeQueueUserWorkItem(_ => { // Release any char[] buffers we're keeping around HttpWriter.ReleaseAllPooledBuffers(); // Trim expired entries from the runtime cache var cache = HttpRuntime.GetCacheInternal(createIfDoesNotExist: false); if (cache != null) { cache.TrimCache(0); } // Trim all pooled HttpApplication instances HttpApplicationFactory.TrimApplicationInstances(removeAll: true); countdownEvent.Signal(); }, null); if (Debug.IsDebuggerPresent()) { countdownEvent.Wait(); // to assist with debugging, don't time out if a debugger is attached } else { countdownEvent.Wait(_suspendMethodTimeout); // blocking call, ok for our needs since has finite wait time } return suspendState; } public void Resume(object state) { ((SuspendState)state).Resume(); } internal sealed class SuspendState { private static readonly WaitCallback _quwiThunk = (state) => ((Action)state)(); private readonly ICollection _suspendibleObjects; // these two fields should only ever be accessed under lock private readonly List _resumeCallbacks; private bool _resumeWasCalled; public SuspendState(ICollection suspendibleObjects) { _suspendibleObjects = suspendibleObjects; _resumeCallbacks = new List(suspendibleObjects.Count); } public void Suspend() { foreach (ISuspendibleRegisteredObject suspendibleObject in _suspendibleObjects) { Action callback = suspendibleObject.Suspend(); lock (this) { // If Resume was called while the Suspend method was still executing, // this callback won't be invoked, so we need to invoke it manually. if (_resumeWasCalled) { if (callback != null) { InvokeResumeCallbackAsync(callback); } return; // don't run any other Suspend methods } if (callback != null) { _resumeCallbacks.Add(callback); } } } } public void Resume() { lock (this) { Debug.Assert(!_resumeWasCalled, "Resume was called too many times!"); _resumeWasCalled = true; foreach (Action callback in _resumeCallbacks) { InvokeResumeCallbackAsync(callback); } } } private static void InvokeResumeCallbackAsync(Action callback) { // Unsafe QUWI since occurs outside the context of a request. // We are not concerned about impersonation, identity, etc. ThreadPool.UnsafeQueueUserWorkItem(_quwiThunk, callback); } } } }