|
//------------------------------------------------------------------------------
// <copyright file="SuspendManager.cs" company="Microsoft">
// Copyright (c) Microsoft Corporation. All rights reserved.
// </copyright>
//------------------------------------------------------------------------------
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<ISuspendibleRegisteredObject, object> _registeredObjects = new ConcurrentDictionary<ISuspendibleRegisteredObject, object>();
public void RegisterObject(ISuspendibleRegisteredObject o) {
Debug.Assert(o != null);
_registeredObjects[o] = null;
}
public void UnregisterObject(ISuspendibleRegisteredObject o) {
Debug.Assert(o != null);
((IDictionary<ISuspendibleRegisteredObject, object>)_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<ISuspendibleRegisteredObject> 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 iCache = HttpRuntime.Cache.GetInternalCache(createIfDoesNotExist: false);
var oCache = HttpRuntime.Cache.GetObjectCache(createIfDoesNotExist: false);
if (iCache != null) {
iCache.Trim(0);
}
if (oCache != null && !oCache.Equals(iCache)) {
oCache.Trim(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<ISuspendibleRegisteredObject> _suspendibleObjects;
// these two fields should only ever be accessed under lock
private readonly List<Action> _resumeCallbacks;
private bool _resumeWasCalled;
public SuspendState(ICollection<ISuspendibleRegisteredObject> suspendibleObjects) {
_suspendibleObjects = suspendibleObjects;
_resumeCallbacks = new List<Action>(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);
}
}
}
}
|