mirror of
https://github.com/OrchardCMS/Orchard.git
synced 2026-02-09 09:16:41 +08:00
Refactoring Warmup to an HttpModule
--HG-- branch : dev
This commit is contained in:
@@ -4,7 +4,7 @@ using Orchard.Environment.Warmup;
|
|||||||
|
|
||||||
namespace Orchard.Tests.Environment.Warmup {
|
namespace Orchard.Tests.Environment.Warmup {
|
||||||
[TestFixture]
|
[TestFixture]
|
||||||
public class WarmUpUtilityTests {
|
public class WarmupUtilityTests {
|
||||||
|
|
||||||
[Test]
|
[Test]
|
||||||
public void EmptyStringsAreNotAllowed() {
|
public void EmptyStringsAreNotAllowed() {
|
||||||
|
|||||||
@@ -240,7 +240,7 @@
|
|||||||
<Compile Include="Environment\RunningShellTableTests.cs" />
|
<Compile Include="Environment\RunningShellTableTests.cs" />
|
||||||
<Compile Include="Environment\StubHostEnvironment.cs" />
|
<Compile Include="Environment\StubHostEnvironment.cs" />
|
||||||
<Compile Include="Environment\Utility\Build.cs" />
|
<Compile Include="Environment\Utility\Build.cs" />
|
||||||
<Compile Include="Environment\WarmUp\WarmUpUtilityTests.cs" />
|
<Compile Include="Environment\Warmup\WarmupUtilityTests.cs" />
|
||||||
<Compile Include="FileSystems\AppData\AppDataFolderTests.cs" />
|
<Compile Include="FileSystems\AppData\AppDataFolderTests.cs" />
|
||||||
<Compile Include="Environment\Configuration\DefaultTenantManagerTests.cs" />
|
<Compile Include="Environment\Configuration\DefaultTenantManagerTests.cs" />
|
||||||
<Compile Include="Environment\DefaultCompositionStrategyTests.cs" />
|
<Compile Include="Environment\DefaultCompositionStrategyTests.cs" />
|
||||||
|
|||||||
@@ -1,22 +1,18 @@
|
|||||||
using System;
|
using System;
|
||||||
using System.IO;
|
|
||||||
using System.Threading;
|
using System.Threading;
|
||||||
using System.Web;
|
using System.Web;
|
||||||
using System.Web.Hosting;
|
|
||||||
using System.Web.Mvc;
|
using System.Web.Mvc;
|
||||||
using System.Web.Routing;
|
using System.Web.Routing;
|
||||||
using Autofac;
|
using Autofac;
|
||||||
using Orchard.Environment;
|
using Orchard.Environment;
|
||||||
using Orchard.Environment.Warmup;
|
|
||||||
using Orchard.Utility.Extensions;
|
|
||||||
|
|
||||||
namespace Orchard.Web {
|
namespace Orchard.Web {
|
||||||
// Note: For instructions on enabling IIS6 or IIS7 classic mode,
|
// Note: For instructions on enabling IIS6 or IIS7 classic mode,
|
||||||
// visit http://go.microsoft.com/?LinkId=9394801
|
// visit http://go.microsoft.com/?LinkId=9394801
|
||||||
|
|
||||||
public class MvcApplication : HttpApplication {
|
public class MvcApplication : HttpApplication {
|
||||||
private static StartupResult _startupResult;
|
private static IOrchardHost _host;
|
||||||
private static EventWaitHandle _waitHandle;
|
private static Exception _error;
|
||||||
|
|
||||||
public static void RegisterRoutes(RouteCollection routes) {
|
public static void RegisterRoutes(RouteCollection routes) {
|
||||||
routes.IgnoreRoute("{resource}.axd/{*pathInfo}");
|
routes.IgnoreRoute("{resource}.axd/{*pathInfo}");
|
||||||
@@ -26,77 +22,30 @@ namespace Orchard.Web {
|
|||||||
LaunchStartupThread();
|
LaunchStartupThread();
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Initializes Orchard's Host in a separate thread
|
|
||||||
/// </summary>
|
|
||||||
private static void LaunchStartupThread() {
|
|
||||||
_startupResult = new StartupResult();
|
|
||||||
_waitHandle = new AutoResetEvent(false);
|
|
||||||
|
|
||||||
ThreadPool.QueueUserWorkItem(
|
|
||||||
state => {
|
|
||||||
try {
|
|
||||||
RegisterRoutes(RouteTable.Routes);
|
|
||||||
var host = OrchardStarter.CreateHost(MvcSingletons);
|
|
||||||
host.Initialize();
|
|
||||||
_startupResult.Host = host;
|
|
||||||
}
|
|
||||||
catch (Exception e) {
|
|
||||||
_startupResult.Error = e;
|
|
||||||
}
|
|
||||||
finally {
|
|
||||||
_waitHandle.Set();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
protected void Application_BeginRequest() {
|
protected void Application_BeginRequest() {
|
||||||
// Host is still starting up?
|
if (_error != null) {
|
||||||
if (_startupResult.Host == null && _startupResult.Error == null) {
|
// Host startup resulted in an error
|
||||||
|
|
||||||
// use the url as it was requested by the client
|
// Throw error once, and restart launch; machine state may have changed
|
||||||
// the real url might be different if it has been translated (proxy, load balancing, ...)
|
// so we need to simulate a "restart".
|
||||||
var url = Request.ToUrlString();
|
var error = _error;
|
||||||
var virtualFileCopy = "~/App_Data/WarmUp/" + WarmupUtility.EncodeUrl(url.Trim('/'));
|
LaunchStartupThread();
|
||||||
var localCopy = HostingEnvironment.MapPath(virtualFileCopy);
|
throw error;
|
||||||
|
|
||||||
if (File.Exists(localCopy)) {
|
|
||||||
// result should not be cached, even on proxies
|
|
||||||
Context.Response.Cache.SetExpires(DateTime.UtcNow.AddDays(-1));
|
|
||||||
Context.Response.Cache.SetValidUntilExpires(false);
|
|
||||||
Context.Response.Cache.SetRevalidation(HttpCacheRevalidation.AllCaches);
|
|
||||||
Context.Response.Cache.SetCacheability(HttpCacheability.NoCache);
|
|
||||||
Context.Response.Cache.SetNoStore();
|
|
||||||
|
|
||||||
Context.Response.WriteFile(localCopy);
|
|
||||||
Context.Response.End();
|
|
||||||
}
|
|
||||||
else if(!File.Exists(Request.PhysicalPath)) {
|
|
||||||
// there is no local copy and the host is not running
|
|
||||||
// wait for the host to initialize
|
|
||||||
_waitHandle.WaitOne();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
else {
|
|
||||||
if (_startupResult.Error != null) {
|
|
||||||
// Host startup resulted in an error
|
|
||||||
|
|
||||||
// Throw error once, and restart launch (machine state may have changed
|
// Only notify if the host has started up
|
||||||
// so we need to simulate a "restart".
|
if (_host == null) {
|
||||||
var error = _startupResult.Error;
|
return;
|
||||||
LaunchStartupThread();
|
|
||||||
throw error;
|
|
||||||
}
|
|
||||||
|
|
||||||
Context.Items["originalHttpContext"] = Context;
|
|
||||||
_startupResult.Host.BeginRequest();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Context.Items["originalHttpContext"] = Context;
|
||||||
|
_host.BeginRequest();
|
||||||
}
|
}
|
||||||
|
|
||||||
protected void Application_EndRequest() {
|
protected void Application_EndRequest() {
|
||||||
// Only notify if the host has started up
|
// Only notify if the host has started up
|
||||||
if (_startupResult.Host != null) {
|
if (_host != null) {
|
||||||
_startupResult.Host.EndRequest();
|
_host.EndRequest();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -105,5 +54,31 @@ namespace Orchard.Web {
|
|||||||
builder.Register(ctx => ModelBinders.Binders).SingleInstance();
|
builder.Register(ctx => ModelBinders.Binders).SingleInstance();
|
||||||
builder.Register(ctx => ViewEngines.Engines).SingleInstance();
|
builder.Register(ctx => ViewEngines.Engines).SingleInstance();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Initializes Orchard's Host in a separate thread
|
||||||
|
/// </summary>
|
||||||
|
private static void LaunchStartupThread() {
|
||||||
|
RegisterRoutes(RouteTable.Routes);
|
||||||
|
|
||||||
|
_host = null;
|
||||||
|
_error = null;
|
||||||
|
|
||||||
|
ThreadPool.QueueUserWorkItem(
|
||||||
|
state => {
|
||||||
|
try {
|
||||||
|
var host = OrchardStarter.CreateHost(MvcSingletons);
|
||||||
|
host.Initialize();
|
||||||
|
_host = host;
|
||||||
|
}
|
||||||
|
catch (Exception e) {
|
||||||
|
_error = e;
|
||||||
|
}
|
||||||
|
finally {
|
||||||
|
// Execute pending actions as the host is available
|
||||||
|
WarmupHttpModule.Signal();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -127,6 +127,7 @@
|
|||||||
<Compile Include="Global.asax.cs">
|
<Compile Include="Global.asax.cs">
|
||||||
<DependentUpon>Global.asax</DependentUpon>
|
<DependentUpon>Global.asax</DependentUpon>
|
||||||
</Compile>
|
</Compile>
|
||||||
|
<Compile Include="WarmupHttpModule.cs" />
|
||||||
<Content Include="Config\log4net.config" />
|
<Content Include="Config\log4net.config" />
|
||||||
<Content Include="Config\Sample.HostComponents.config">
|
<Content Include="Config\Sample.HostComponents.config">
|
||||||
<SubType>Designer</SubType>
|
<SubType>Designer</SubType>
|
||||||
|
|||||||
@@ -0,0 +1,170 @@
|
|||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.IO;
|
||||||
|
using System.Text;
|
||||||
|
using System.Threading;
|
||||||
|
using System.Web;
|
||||||
|
using System.Web.Hosting;
|
||||||
|
|
||||||
|
namespace Orchard.Web {
|
||||||
|
public class WarmupHttpModule : IHttpModule {
|
||||||
|
private const string WarmupFilesPath = "~/App_Data/Warmup/";
|
||||||
|
private HttpApplication _context;
|
||||||
|
|
||||||
|
public void Init(HttpApplication context) {
|
||||||
|
_context = context;
|
||||||
|
context.AddOnBeginRequestAsync(BeginBeginRequest, EndBeginRequest, null);
|
||||||
|
}
|
||||||
|
|
||||||
|
static IList<Action> _awaiting = new List<Action>();
|
||||||
|
|
||||||
|
public static bool InWarmup() {
|
||||||
|
if (_awaiting == null) return false;
|
||||||
|
lock (_awaiting) {
|
||||||
|
return _awaiting != null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void Signal() {
|
||||||
|
lock(typeof(WarmupHttpModule)) {
|
||||||
|
var awaiting = _awaiting;
|
||||||
|
_awaiting = null;
|
||||||
|
foreach (var action in awaiting) {
|
||||||
|
action();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void Await(Action action) {
|
||||||
|
if (_awaiting == null) {
|
||||||
|
action();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
lock(typeof(WarmupHttpModule)) {
|
||||||
|
if (_awaiting == null) {
|
||||||
|
action();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
_awaiting.Add(action);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private IAsyncResult BeginBeginRequest(object sender, EventArgs e, AsyncCallback cb, object extradata) {
|
||||||
|
var asyncResult = new WarmupAsyncResult(cb);
|
||||||
|
|
||||||
|
// host is available, process every requests, or file is processed
|
||||||
|
if (!InWarmup() || DoBeginRequest()) {
|
||||||
|
asyncResult.Done();
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
// this is the "on hold" execution path
|
||||||
|
Await(asyncResult.Done);
|
||||||
|
}
|
||||||
|
|
||||||
|
return asyncResult;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void EndBeginRequest(IAsyncResult ar) {
|
||||||
|
((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>
|
||||||
|
/// <returns></returns>
|
||||||
|
private bool DoBeginRequest() {
|
||||||
|
// 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 = WarmupFilesPath + EncodeUrl(url.Trim('/'));
|
||||||
|
var localCopy = HostingEnvironment.MapPath(virtualFileCopy);
|
||||||
|
|
||||||
|
if (localCopy != null && File.Exists(localCopy)) {
|
||||||
|
// result should not be cached, even on proxies
|
||||||
|
_context.Response.Cache.SetExpires(DateTime.UtcNow.AddDays(-1));
|
||||||
|
_context.Response.Cache.SetValidUntilExpires(false);
|
||||||
|
_context.Response.Cache.SetRevalidation(HttpCacheRevalidation.AllCaches);
|
||||||
|
_context.Response.Cache.SetCacheability(HttpCacheability.NoCache);
|
||||||
|
_context.Response.Cache.SetNoStore();
|
||||||
|
|
||||||
|
_context.Response.WriteFile(localCopy);
|
||||||
|
_context.Response.End();
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// there is no local copy and the file exists
|
||||||
|
// serve the static file
|
||||||
|
if (File.Exists(_context.Request.PhysicalPath)) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
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 AsyncCallback _cb;
|
||||||
|
|
||||||
|
public WarmupAsyncResult(AsyncCallback cb) {
|
||||||
|
_cb = cb;
|
||||||
|
IsCompleted = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Done() {
|
||||||
|
IsCompleted = true;
|
||||||
|
_eventWaitHandle.Set();
|
||||||
|
_cb(this);
|
||||||
|
}
|
||||||
|
|
||||||
|
public object AsyncState {
|
||||||
|
get { return null; }
|
||||||
|
}
|
||||||
|
|
||||||
|
public WaitHandle AsyncWaitHandle {
|
||||||
|
get { throw new NotImplementedException(); }
|
||||||
|
}
|
||||||
|
|
||||||
|
public bool CompletedSynchronously {
|
||||||
|
get { return true; }
|
||||||
|
}
|
||||||
|
|
||||||
|
public bool IsCompleted { get; private set; }
|
||||||
|
|
||||||
|
public void Wait() {
|
||||||
|
_eventWaitHandle.WaitOne();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -120,6 +120,10 @@
|
|||||||
<add path="*" verb="*" type="System.Web.HttpNotFoundHandler"/>
|
<add path="*" verb="*" type="System.Web.HttpNotFoundHandler"/>
|
||||||
|
|
||||||
</httpHandlers>
|
</httpHandlers>
|
||||||
|
|
||||||
|
<httpModules>
|
||||||
|
<add name="WarmupHttpModule" type="Orchard.Web.WarmupHttpModule, Orchard.Web"/>
|
||||||
|
</httpModules>
|
||||||
</system.web>
|
</system.web>
|
||||||
<!--
|
<!--
|
||||||
The system.webServer section is required for running ASP.NET AJAX under Internet
|
The system.webServer section is required for running ASP.NET AJAX under Internet
|
||||||
@@ -128,7 +132,10 @@
|
|||||||
<system.webServer>
|
<system.webServer>
|
||||||
|
|
||||||
<validation validateIntegratedModeConfiguration="false" />
|
<validation validateIntegratedModeConfiguration="false" />
|
||||||
<modules runAllManagedModulesForAllRequests="true" />
|
<modules runAllManagedModulesForAllRequests="true">
|
||||||
|
<remove name="WarmupHttpModule" />
|
||||||
|
<add name="WarmupHttpModule" type="Orchard.Web.WarmupHttpModule, Orchard.Web"/>
|
||||||
|
</modules>
|
||||||
<handlers accessPolicy="Script">
|
<handlers accessPolicy="Script">
|
||||||
<!-- clear all handlers, prevents executing code file extensions, prevents returning any file contents -->
|
<!-- clear all handlers, prevents executing code file extensions, prevents returning any file contents -->
|
||||||
<clear/>
|
<clear/>
|
||||||
|
|||||||
@@ -1,8 +0,0 @@
|
|||||||
using System;
|
|
||||||
|
|
||||||
namespace Orchard.Environment.Warmup {
|
|
||||||
public class StartupResult {
|
|
||||||
public IOrchardHost Host { get; set; }
|
|
||||||
public Exception Error { get; set; }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,19 +1,29 @@
|
|||||||
using System;
|
using System;
|
||||||
using System.Linq;
|
|
||||||
using System.Text;
|
using System.Text;
|
||||||
using System.Text.RegularExpressions;
|
|
||||||
|
|
||||||
namespace Orchard.Environment.Warmup {
|
namespace Orchard.Environment.Warmup {
|
||||||
public static class WarmupUtility {
|
public static class WarmupUtility {
|
||||||
private const string EncodingPattern = "[^a-z0-9]";
|
|
||||||
|
|
||||||
public static string EncodeUrl(string url) {
|
public static string EncodeUrl(string url) {
|
||||||
if(String.IsNullOrWhiteSpace(url)) {
|
if(String.IsNullOrWhiteSpace(url)) {
|
||||||
throw new ArgumentException("url can't be empty");
|
throw new ArgumentException("url can't be empty");
|
||||||
}
|
}
|
||||||
|
|
||||||
return Regex.Replace(url.ToLower(), EncodingPattern, m => "_" + Encoding.UTF8.GetBytes(m.Value).Select(b => b.ToString("X")).Aggregate((a, b) => a + b));
|
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();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -187,7 +187,6 @@
|
|||||||
<Compile Include="Environment\HostComponentsConfigModule.cs" />
|
<Compile Include="Environment\HostComponentsConfigModule.cs" />
|
||||||
<Compile Include="Environment\IOrchardFrameworkAssemblies.cs" />
|
<Compile Include="Environment\IOrchardFrameworkAssemblies.cs" />
|
||||||
<Compile Include="Environment\ViewsBackgroundCompilation.cs" />
|
<Compile Include="Environment\ViewsBackgroundCompilation.cs" />
|
||||||
<Compile Include="Environment\Warmup\StartupResult.cs" />
|
|
||||||
<Compile Include="Environment\Warmup\WarmupUtility.cs" />
|
<Compile Include="Environment\Warmup\WarmupUtility.cs" />
|
||||||
<Compile Include="Environment\WorkContextImplementation.cs" />
|
<Compile Include="Environment\WorkContextImplementation.cs" />
|
||||||
<Compile Include="Environment\WorkContextModule.cs" />
|
<Compile Include="Environment\WorkContextModule.cs" />
|
||||||
|
|||||||
Reference in New Issue
Block a user