159 lines
6.9 KiB
C#
159 lines
6.9 KiB
C#
|
//------------------------------------------------------------------------------
|
|||
|
// <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 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<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);
|
|||
|
}
|
|||
|
}
|
|||
|
}
|
|||
|
}
|