mirror of
https://github.com/OrchardCMS/Orchard.git
synced 2025-10-15 19:54:57 +08:00
Refactor Orchard.WarmupStarter assembly
* Remove dependency on Orchard.Framework assembly * Fix issue where a host initialization failure would result in '404' errors. We need to restart the host initialization and make sure new incoming requests are queued. * Fix concurrency issue when multiple requests are pending for the host initialization to finish (only one request would notify of a potentially error, the other ones would return a '404'). --HG-- branch : 1.x extra : transplant_source : %3Dz%E4%ADEq%91%9D%17%D2%10jut%A6%93%09t%7CR
This commit is contained in:
@@ -31,23 +31,14 @@
|
||||
<WarningLevel>4</WarningLevel>
|
||||
</PropertyGroup>
|
||||
<ItemGroup>
|
||||
<Reference Include="Autofac">
|
||||
<HintPath>..\..\lib\autofac\Autofac.dll</HintPath>
|
||||
</Reference>
|
||||
<Reference Include="System" />
|
||||
<Reference Include="System.Web" />
|
||||
<Reference Include="System.Data.DataSetExtensions" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<Compile Include="Starter.cs" />
|
||||
<Compile Include="Properties\AssemblyInfo.cs" />
|
||||
<Compile Include="WarmupHttpModule.cs" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\Orchard\Orchard.Framework.csproj">
|
||||
<Project>{2D1D92BB-4555-4CBE-8D0E-63563D6CE4C6}</Project>
|
||||
<Name>Orchard.Framework</Name>
|
||||
</ProjectReference>
|
||||
<Compile Include="WarmupUtility.cs" />
|
||||
</ItemGroup>
|
||||
<Import Project="$(MSBuildToolsPath)\Microsoft.CSharp.targets" />
|
||||
<!-- To modify your build process, add your task inside one of the targets below and uncomment it.
|
||||
|
@@ -1,67 +1,103 @@
|
||||
using System;
|
||||
using System.Threading;
|
||||
using System.Web;
|
||||
using Autofac;
|
||||
using Orchard.Environment;
|
||||
|
||||
namespace Orchard.WarmupStarter {
|
||||
public class Starter {
|
||||
private static IOrchardHost _host;
|
||||
private static Exception _error;
|
||||
public class Starter<T> where T : class {
|
||||
private readonly Func<HttpApplication, T> _initialization;
|
||||
private readonly Action<HttpApplication, T> _beginRequest;
|
||||
private readonly Action<HttpApplication, T> _endRequest;
|
||||
private readonly object _synLock = new object();
|
||||
/// <summary>
|
||||
/// The result of the initialization queued work item.
|
||||
/// Set only when initialization has completed without errors.
|
||||
/// </summary>
|
||||
private volatile T _initializationResult;
|
||||
/// <summary>
|
||||
/// The (potential) error raised by the initialization thread. This is a "one-time"
|
||||
/// error signal, so that we can restart the initialization once another request
|
||||
/// comes in.
|
||||
/// </summary>
|
||||
private volatile Exception _error;
|
||||
/// <summary>
|
||||
/// The (potential) error from the previous initiazalition. We need to
|
||||
/// keep this error active until the next initialization is finished,
|
||||
/// so that we can keep reporting the error for all incoming requests.
|
||||
/// </summary>
|
||||
private volatile Exception _previousError;
|
||||
|
||||
public static void OnBeginRequest(HttpContext context, Action<ContainerBuilder> registrations) {
|
||||
public Starter(Func<HttpApplication, T> initialization, Action<HttpApplication, T> beginRequest, Action<HttpApplication, T> endRequest) {
|
||||
_initialization = initialization;
|
||||
_beginRequest = beginRequest;
|
||||
_endRequest = endRequest;
|
||||
}
|
||||
|
||||
public void OnApplicationStart(HttpApplication application) {
|
||||
LaunchStartupThread(application);
|
||||
}
|
||||
|
||||
public void OnBeginRequest(HttpApplication application) {
|
||||
// Initialization resulted in an error
|
||||
if (_error != null) {
|
||||
// Host startup resulted in an error
|
||||
// Save error for next requests and restart async initialization.
|
||||
// Note: The reason we have to retry the initialization is that the
|
||||
// application environment may change between requests,
|
||||
// e.g. App_Data is made read-write for the AppPool.
|
||||
bool restartInitialization = false;
|
||||
|
||||
// Throw error once, and restart launch; machine state may have changed
|
||||
// so we need to simulate a "restart".
|
||||
var error = _error;
|
||||
LaunchStartupThread(registrations);
|
||||
throw new ApplicationException("Error during Orchard startup", error);
|
||||
}
|
||||
|
||||
// Only notify if the host has started up
|
||||
if (_host == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
context.Items["originalHttpContext"] = context;
|
||||
_host.BeginRequest();
|
||||
lock (_synLock) {
|
||||
if (_error != null) {
|
||||
_previousError = _error;
|
||||
_error = null;
|
||||
restartInitialization = true;
|
||||
}
|
||||
}
|
||||
|
||||
public static void OnEndRequest() {
|
||||
// Only notify if the host has started up
|
||||
if (_host != null) {
|
||||
_host.EndRequest();
|
||||
if (restartInitialization) {
|
||||
LaunchStartupThread(application);
|
||||
}
|
||||
}
|
||||
|
||||
// Previous initialization resulted in an error (and another initialization is running)
|
||||
if (_previousError != null) {
|
||||
throw new ApplicationException("Error during application initialization", _previousError);
|
||||
}
|
||||
|
||||
// Only notify if the initialization has successfully completed
|
||||
if (_initializationResult != null) {
|
||||
_beginRequest(application, _initializationResult);
|
||||
}
|
||||
}
|
||||
|
||||
public void OnEndRequest(HttpApplication application) {
|
||||
// Only notify if the initialization has successfully completed
|
||||
if (_initializationResult != null) {
|
||||
_endRequest(application, _initializationResult);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Initializes Orchard's Host in a separate thread
|
||||
/// Run the initialization delegate asynchronously in a queued work item
|
||||
/// </summary>
|
||||
public static void LaunchStartupThread(Action<ContainerBuilder> registrations) {
|
||||
_host = null;
|
||||
_error = null;
|
||||
public void LaunchStartupThread(HttpApplication application) {
|
||||
// Make sure incoming requests are queued
|
||||
WarmupHttpModule.SignalWarmupStart();
|
||||
|
||||
ThreadPool.QueueUserWorkItem(
|
||||
state => {
|
||||
try {
|
||||
var host = OrchardStarter.CreateHost(registrations);
|
||||
host.Initialize();
|
||||
_host = host;
|
||||
var result = _initialization(application);
|
||||
_initializationResult = result;
|
||||
}
|
||||
catch (Exception e) {
|
||||
lock (_synLock) {
|
||||
_error = e;
|
||||
_previousError = null;
|
||||
}
|
||||
}
|
||||
finally {
|
||||
// Execute pending actions as the host is available
|
||||
WarmupHttpModule.Signal();
|
||||
}
|
||||
|
||||
// initialize shells to speed up the first dynamic query
|
||||
if (_host != null) {
|
||||
_host.BeginRequest();
|
||||
_host.EndRequest();
|
||||
// Execute pending requests as the initialization is over
|
||||
WarmupHttpModule.SignalWarmupDone();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
@@ -1,7 +1,6 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Text;
|
||||
using System.Threading;
|
||||
using System.Web;
|
||||
using System.Web.Hosting;
|
||||
@@ -10,47 +9,82 @@ namespace Orchard.WarmupStarter {
|
||||
public class WarmupHttpModule : IHttpModule {
|
||||
private const string WarmupFilesPath = "~/App_Data/Warmup/";
|
||||
private HttpApplication _context;
|
||||
private static object _synLock = new object();
|
||||
private static IList<Action> _awaiting = new List<Action>();
|
||||
|
||||
public WarmupHttpModule() {
|
||||
}
|
||||
|
||||
public void Init(HttpApplication context) {
|
||||
_context = context;
|
||||
context.AddOnBeginRequestAsync(BeginBeginRequest, EndBeginRequest, null);
|
||||
}
|
||||
|
||||
static IList<Action> _awaiting = new List<Action>();
|
||||
public void Dispose() {
|
||||
}
|
||||
|
||||
public static bool InWarmup() {
|
||||
if (_awaiting == null) return false;
|
||||
lock (_awaiting) {
|
||||
private static bool InWarmup() {
|
||||
lock (_synLock) {
|
||||
return _awaiting != null;
|
||||
}
|
||||
}
|
||||
|
||||
public static void Signal() {
|
||||
lock(typeof(WarmupHttpModule)) {
|
||||
if (_awaiting == null) {
|
||||
return;
|
||||
}
|
||||
/// <summary>
|
||||
/// Called to unblock all pending requests, while remaining in "queue" incoming requests mode.
|
||||
/// New incoming request are queued in the "_await" list.
|
||||
/// </summary>
|
||||
public static void ProcessPendingRequests() {
|
||||
FlushAwaitingRequests(new List<Action>());
|
||||
}
|
||||
|
||||
var awaiting = _awaiting;
|
||||
_awaiting = null;
|
||||
foreach (var action in awaiting) {
|
||||
action();
|
||||
/// <summary>
|
||||
/// Pending requests in the "_await" queue are processed, and any new incoming request
|
||||
/// is now processed immediately.
|
||||
/// </summary>
|
||||
public static void SignalWarmupDone() {
|
||||
FlushAwaitingRequests(null);
|
||||
}
|
||||
|
||||
public static void SignalWarmupStart() {
|
||||
lock (_synLock) {
|
||||
if (_awaiting == null) {
|
||||
_awaiting = new List<Action>();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public static void Await(Action action) {
|
||||
if (_awaiting == null) {
|
||||
action();
|
||||
return;
|
||||
}
|
||||
private static void FlushAwaitingRequests(IList<Action> newAwaiting) {
|
||||
IList<Action> temp;
|
||||
|
||||
lock(typeof(WarmupHttpModule)) {
|
||||
lock (_synLock) {
|
||||
if (_awaiting == null) {
|
||||
action();
|
||||
return;
|
||||
}
|
||||
_awaiting.Add(action);
|
||||
|
||||
temp = _awaiting;
|
||||
_awaiting = newAwaiting;
|
||||
}
|
||||
|
||||
foreach (var action in temp) {
|
||||
action();
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Enqueue or directly process action depending on current mode.
|
||||
/// </summary>
|
||||
private void Await(Action action) {
|
||||
Action temp = action;
|
||||
|
||||
lock (_synLock) {
|
||||
if (_awaiting != null) {
|
||||
temp = null;
|
||||
_awaiting.Add(action);
|
||||
}
|
||||
}
|
||||
|
||||
if (temp != null) {
|
||||
temp();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -73,9 +107,6 @@ namespace Orchard.WarmupStarter {
|
||||
((WarmupAsyncResult)ar).Wait();
|
||||
}
|
||||
|
||||
public void Dispose() {
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// return true to put request on hold (until call to Signal()) - return false to allow pipeline to execute immediately
|
||||
/// </summary>
|
||||
@@ -84,7 +115,7 @@ namespace Orchard.WarmupStarter {
|
||||
// use the url as it was requested by the client
|
||||
// the real url might be different if it has been translated (proxy, load balancing, ...)
|
||||
var url = ToUrlString(_context.Request);
|
||||
var virtualFileCopy = EncodeUrl(url.Trim('/'));
|
||||
var virtualFileCopy = WarmupUtility.EncodeUrl(url.Trim('/'));
|
||||
var localCopy = Path.Combine(HostingEnvironment.MapPath(WarmupFilesPath), virtualFileCopy);
|
||||
|
||||
if (File.Exists(localCopy)) {
|
||||
@@ -109,65 +140,44 @@ namespace Orchard.WarmupStarter {
|
||||
return false;
|
||||
}
|
||||
|
||||
public static string EncodeUrl(string url) {
|
||||
if(String.IsNullOrWhiteSpace(url)) {
|
||||
throw new ArgumentException("url can't be empty");
|
||||
}
|
||||
|
||||
var sb = new StringBuilder();
|
||||
foreach(var c in url.ToLowerInvariant()) {
|
||||
// only accept alphanumeric chars
|
||||
if((c >= 'a' && c <= 'z') || (c >= '0' && c <= '9')) {
|
||||
sb.Append(c);
|
||||
}
|
||||
// otherwise encode them in UTF8
|
||||
else {
|
||||
sb.Append("_");
|
||||
foreach(var b in Encoding.UTF8.GetBytes(new [] {c})) {
|
||||
sb.Append(b.ToString("X"));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return sb.ToString();
|
||||
}
|
||||
|
||||
public static string ToUrlString(HttpRequest request) {
|
||||
return string.Format("{0}://{1}{2}", request.Url.Scheme, request.Headers["Host"], request.RawUrl);
|
||||
}
|
||||
|
||||
private class WarmupAsyncResult : IAsyncResult {
|
||||
readonly EventWaitHandle _eventWaitHandle = new AutoResetEvent(false);
|
||||
|
||||
private readonly EventWaitHandle _eventWaitHandle = new AutoResetEvent(false);
|
||||
private readonly AsyncCallback _cb;
|
||||
private bool _isCompleted;
|
||||
|
||||
public WarmupAsyncResult(AsyncCallback cb) {
|
||||
_cb = cb;
|
||||
IsCompleted = false;
|
||||
_isCompleted = false;
|
||||
}
|
||||
|
||||
public void Done() {
|
||||
IsCompleted = true;
|
||||
_isCompleted = true;
|
||||
_eventWaitHandle.Set();
|
||||
_cb(this);
|
||||
}
|
||||
|
||||
public object AsyncState {
|
||||
public void Wait() {
|
||||
_eventWaitHandle.WaitOne();
|
||||
}
|
||||
|
||||
object IAsyncResult.AsyncState {
|
||||
get { return null; }
|
||||
}
|
||||
|
||||
public WaitHandle AsyncWaitHandle {
|
||||
WaitHandle IAsyncResult.AsyncWaitHandle {
|
||||
get { throw new NotImplementedException(); }
|
||||
}
|
||||
|
||||
public bool CompletedSynchronously {
|
||||
get { return true; }
|
||||
bool IAsyncResult.CompletedSynchronously {
|
||||
get { return false; }
|
||||
}
|
||||
|
||||
public bool IsCompleted { get; private set; }
|
||||
|
||||
public void Wait() {
|
||||
_eventWaitHandle.WaitOne();
|
||||
bool IAsyncResult.IsCompleted {
|
||||
get { return _isCompleted; }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
29
src/Orchard.Startup/WarmupUtility.cs
Normal file
29
src/Orchard.Startup/WarmupUtility.cs
Normal file
@@ -0,0 +1,29 @@
|
||||
using System;
|
||||
using System.Text;
|
||||
|
||||
namespace Orchard.WarmupStarter {
|
||||
public static class WarmupUtility {
|
||||
public static string EncodeUrl(string url) {
|
||||
if (String.IsNullOrWhiteSpace(url)) {
|
||||
throw new ArgumentException("url can't be empty");
|
||||
}
|
||||
|
||||
var sb = new StringBuilder();
|
||||
foreach (var c in url.ToLowerInvariant()) {
|
||||
// only accept alphanumeric chars
|
||||
if ((c >= 'a' && c <= 'z') || (c >= '0' && c <= '9')) {
|
||||
sb.Append(c);
|
||||
}
|
||||
// otherwise encode them in UTF8
|
||||
else {
|
||||
sb.Append("_");
|
||||
foreach (var b in Encoding.UTF8.GetBytes(new[] { c })) {
|
||||
sb.Append(b.ToString("X"));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return sb.ToString();
|
||||
}
|
||||
}
|
||||
}
|
@@ -2,6 +2,7 @@
|
||||
using System.Web.Mvc;
|
||||
using System.Web.Routing;
|
||||
using Autofac;
|
||||
using Orchard.Environment;
|
||||
using Orchard.WarmupStarter;
|
||||
|
||||
namespace Orchard.Web {
|
||||
@@ -9,6 +10,10 @@ namespace Orchard.Web {
|
||||
// visit http://go.microsoft.com/?LinkId=9394801
|
||||
|
||||
public class MvcApplication : HttpApplication {
|
||||
private static Starter<IOrchardHost> _starter;
|
||||
|
||||
public MvcApplication() {
|
||||
}
|
||||
|
||||
public static void RegisterRoutes(RouteCollection routes) {
|
||||
routes.IgnoreRoute("{resource}.axd/{*pathInfo}");
|
||||
@@ -16,16 +21,37 @@ namespace Orchard.Web {
|
||||
|
||||
protected void Application_Start() {
|
||||
RegisterRoutes(RouteTable.Routes);
|
||||
|
||||
Starter.LaunchStartupThread(MvcSingletons);
|
||||
_starter = new Starter<IOrchardHost>(HostInitialization, HostBeginRequest, HostEndRequest);
|
||||
_starter.OnApplicationStart(this);
|
||||
}
|
||||
|
||||
protected void Application_BeginRequest() {
|
||||
Starter.OnBeginRequest(Context, MvcSingletons);
|
||||
_starter.OnBeginRequest(this);
|
||||
}
|
||||
|
||||
protected void Application_EndRequest() {
|
||||
Starter.OnEndRequest();
|
||||
_starter.OnEndRequest(this);
|
||||
}
|
||||
|
||||
private static void HostBeginRequest(HttpApplication application, IOrchardHost host) {
|
||||
application.Context.Items["originalHttpContext"] = application.Context;
|
||||
host.BeginRequest();
|
||||
}
|
||||
|
||||
private static void HostEndRequest(HttpApplication application, IOrchardHost host) {
|
||||
host.EndRequest();
|
||||
}
|
||||
|
||||
private static IOrchardHost HostInitialization(HttpApplication application) {
|
||||
var host = OrchardStarter.CreateHost(MvcSingletons);
|
||||
|
||||
host.Initialize();
|
||||
|
||||
// initialize shells to speed up the first dynamic query
|
||||
host.BeginRequest();
|
||||
host.EndRequest();
|
||||
|
||||
return host;
|
||||
}
|
||||
|
||||
static void MvcSingletons(ContainerBuilder builder) {
|
||||
|
Reference in New Issue
Block a user