1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162
|
//------------------------------------------------------------------------------
// <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);
}
}
}
}
|