Compare commits

...

1 Commits

Author SHA1 Message Date
Sebastien Ros
8aa84ef048 Creating async cache middleware 2015-08-06 15:03:35 -07:00
19 changed files with 1107 additions and 4 deletions

View File

@@ -13,6 +13,7 @@ using System.Web.Routing;
using Orchard.Caching;
using Orchard.ContentManagement;
using Orchard.Environment.Configuration;
using Orchard.Environment.Extensions;
using Orchard.Logging;
using Orchard.Mvc.Extensions;
using Orchard.Mvc.Filters;
@@ -24,6 +25,7 @@ using Orchard.UI.Admin;
using Orchard.Utility.Extensions;
namespace Orchard.OutputCache.Filters {
[OrchardFeature("Orchard.OutputCacheOld")]
public class OutputCacheFilter : FilterProvider, IActionFilter, IResultFilter {
private static string _refreshKey = "__r";
@@ -604,8 +606,4 @@ namespace Orchard.OutputCache.Filters {
return null;
}
}
public class ViewDataContainer : IViewDataContainer {
public ViewDataDictionary ViewData { get; set; }
}
}

View File

@@ -2,10 +2,12 @@
using Orchard.ContentManagement;
using Orchard.ContentManagement.Handlers;
using Orchard.Core.Common.Models;
using Orchard.Environment.Extensions;
using Orchard.OutputCache.Models;
using Orchard.OutputCache.Services;
namespace Orchard.OutputCache.Handlers {
[OrchardFeature("Orchard.OutputCacheOld")]
public class CacheSettingsPartHandler : ContentHandler {
private readonly ICacheService _cacheService;

View File

@@ -3,11 +3,13 @@ using System.Collections.ObjectModel;
using System.Linq;
using Orchard.OutputCache.Services;
using Orchard.ContentManagement.Handlers;
using Orchard.Environment.Extensions;
namespace Orchard.OutputCache.Handlers {
/// <summary>
/// Saves references to content items which have been displayed during a request
/// </summary>
[OrchardFeature("Orchard.OutputCacheOld")]
public class DisplayedContentItemHandler : ContentHandler, IDisplayedContentItemHandler {
private readonly Collection<int> _itemIds = new Collection<int>();

View File

@@ -7,6 +7,14 @@ namespace Orchard.OutputCache.Models {
public class CacheSettings : ContentPart {
public const string CacheKey = "Orchard_OutputCache_CacheSettings";
public CacheSettings() {
DefaultCacheDuration = 300;
DefaultCacheGraceTime = 60;
VaryByRequestHeaders = new HashSet<string>();
VaryByQueryStringParameters = new List<string>();
IgnoredUrls = new List<string>();
}
public CacheSettings(CacheSettingsPart part) {
DefaultCacheDuration = part.DefaultCacheDuration;
DefaultCacheGraceTime = part.DefaultCacheGraceTime;

View File

@@ -10,6 +10,10 @@ Features:
Name: Output Cache
Description: Adds output caching functionality.
Category: Performance
Orchard.OutputCacheOld:
Name: Output Cache
Description: OLD cache.
Category: Performance
Orchard.OutputCache.Database:
Name: Database Output Cache
Description: Activates a provider that stores output cache data in the database.
@@ -20,3 +24,8 @@ Features:
Description: Activates a provider that stores output cache data in the App_Data folder.
Category: Performance
Dependencies: Orchard.OutputCache
Orchard.OutputCache2:
Name: Output Cache 2
Description: Adds output caching functionality.
Category: Performance
Dependencies: Orchard.OutputCache

View File

@@ -54,6 +54,14 @@
<HintPath>..\..\..\..\lib\autofac\Autofac.dll</HintPath>
</Reference>
<Reference Include="Microsoft.CSharp" />
<Reference Include="Microsoft.Owin, Version=3.0.0.0, Culture=neutral, PublicKeyToken=31bf3856ad364e35, processorArchitecture=MSIL">
<SpecificVersion>False</SpecificVersion>
<HintPath>..\..\..\..\lib\owin\Microsoft.Owin.dll</HintPath>
</Reference>
<Reference Include="Owin, Version=1.0.0.0, Culture=neutral, PublicKeyToken=f0ebd12fd5e55cc5, processorArchitecture=MSIL">
<SpecificVersion>False</SpecificVersion>
<HintPath>..\..\..\..\lib\owin\Owin.dll</HintPath>
</Reference>
<Reference Include="System" />
<Reference Include="System.Data" />
<Reference Include="System.ComponentModel.DataAnnotations">
@@ -107,8 +115,13 @@
<Compile Include="Models\CacheSettingsPart.cs" />
<Compile Include="Models\CacheParameterRecord.cs" />
<Compile Include="Services\CacheService.cs" />
<Compile Include="Services\MemoryAsyncTagCache.cs" />
<Compile Include="Services\IAsyncTagCache.cs" />
<Compile Include="Services\MemoryAsyncOutputCacheStorageProvider.cs" />
<Compile Include="Services\FileSystemOutputCacheBackgroundTask.cs" />
<Compile Include="Services\DatabaseOutputCacheBackgroundTask.cs" />
<Compile Include="Services\IAsyncOutputCacheStorageProvider.cs" />
<Compile Include="Services\OutputCacheOwinMiddleware.cs" />
<Compile Include="Services\FileSystemOutputCacheProvider.cs" />
<Compile Include="Services\DatabaseOutputCacheProvider.cs" />
<Compile Include="Services\DefaultCacheControlStrategy.cs" />

View File

@@ -11,8 +11,10 @@ using Orchard;
using Orchard.Caching;
using Orchard.Data;
using Orchard.Utility.Extensions;
using Orchard.Environment.Extensions;
namespace Orchard.OutputCache.Services {
[OrchardFeature("Orchard.OutputCacheOld")]
public class CacheService : ICacheService {
private const string RouteConfigsCacheKey = "OutputCache_RouteConfigs";

View File

@@ -4,8 +4,10 @@ using System.Collections.Generic;
using System.Linq;
using Orchard.OutputCache.Models;
using Orchard.Environment.Configuration;
using Orchard.Environment.Extensions;
namespace Orchard.OutputCache.Services {
[OrchardFeature("Orchard.OutputCacheOld")]
public class DefaultCacheStorageProvider : IOutputCacheStorageProvider {
private readonly string _tenantName;
private readonly WorkContext _workContext;

View File

@@ -2,6 +2,7 @@
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Linq;
using Orchard.Environment.Extensions;
using Orchard.OutputCache.Models;
using Orchard.Utility.Extensions;
@@ -9,6 +10,7 @@ namespace Orchard.OutputCache.Services {
/// <summary>
/// Tenant wide case insensitive reverse index for <see cref="CacheItem"/> tags.
/// </summary>
[OrchardFeature("Orchard.OutputCacheOld")]
public class DefaultTagCache : ITagCache {
private readonly ConcurrentDictionary<string, HashSet<string>> _dictionary;

View File

@@ -0,0 +1,14 @@
using System.Collections.Generic;
using System.Threading.Tasks;
using Orchard.OutputCache.Models;
namespace Orchard.OutputCache.Services {
public interface IAsyncOutputCacheStorageProvider : IDependency {
Task<CacheItem> GetAsync(string key);
Task SetAsync(string key, CacheItem cacheItem);
Task RemoveAsync(string key);
Task<IEnumerable<CacheItem>> GetAllAsync(int skip, int count);
Task RemoveAllAsync();
Task<int> CountAsync();
}
}

View File

@@ -0,0 +1,10 @@
using System.Collections.Generic;
using System.Threading.Tasks;
namespace Orchard.OutputCache.Services {
public interface IAsyncTagCache : ISingletonDependency {
Task TagAsync(string tag, params string[] keys);
Task<IEnumerable<string>> GetTaggedItemsAsync(string tag);
Task RemoveTagAsync(string tag);
}
}

View File

@@ -0,0 +1,70 @@
using System;
using System.Collections;
using System.Collections.Generic;
using System.Linq;
using Orchard.OutputCache.Models;
using Orchard.Environment.Configuration;
using System.Threading.Tasks;
using Orchard.Environment.Extensions;
namespace Orchard.OutputCache.Services {
[OrchardFeature("Orchard.OutputCache2")]
public class MemoryAsyncOutputCacheStorageProvider : IAsyncOutputCacheStorageProvider {
private readonly string _tenantName;
private readonly WorkContext _workContext;
public MemoryAsyncOutputCacheStorageProvider(IWorkContextAccessor workContextAccessor, ShellSettings shellSettings) {
_workContext = workContextAccessor.GetContext();
_tenantName = shellSettings.Name;
}
public async Task SetAsync(string key, CacheItem cacheItem) {
_workContext.HttpContext.Cache.Add(
key,
cacheItem,
null,
cacheItem.StoredUntilUtc,
System.Web.Caching.Cache.NoSlidingExpiration,
System.Web.Caching.CacheItemPriority.Normal,
null);
}
public async Task<CacheItem> GetAsync(string key) {
return _workContext.HttpContext.Cache.Get(key) as CacheItem;
}
public async Task RemoveAsync(string key) {
_workContext.HttpContext.Cache.Remove(key);
}
public async Task RemoveAllAsync() {
var items = (await GetAllAsync(0, 100)).ToList();
while (items.Any()) {
foreach (var item in items) {
await RemoveAsync(item.CacheKey);
}
items = (await GetAllAsync(0, 100)).ToList();
}
}
public async Task<IEnumerable<CacheItem>> GetAllAsync(int skip, int count) {
// the ASP.NET cache can also contain other types of items
return _workContext.HttpContext.Cache.AsParallel()
.Cast<DictionaryEntry>()
.Select(x => x.Value)
.OfType<CacheItem>()
.Where(x => x.Tenant.Equals(_tenantName, StringComparison.OrdinalIgnoreCase))
.Skip(skip)
.Take(count);
}
public async Task<int> CountAsync() {
return _workContext.HttpContext.Cache.AsParallel()
.Cast<DictionaryEntry>()
.Select(x => x.Value)
.OfType<CacheItem>()
.Count(x => x.Tenant.Equals(_tenantName, StringComparison.OrdinalIgnoreCase));
}
}
}

View File

@@ -0,0 +1,48 @@
using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Orchard.Environment.Extensions;
using Orchard.Utility.Extensions;
namespace Orchard.OutputCache.Services {
/// <summary>
/// Tenant wide case insensitive reverse index for <see cref="CacheItem"/> tags.
/// </summary>
[OrchardFeature("Orchard.OutputCache2")]
public class MemoryAsyncTagCache : IAsyncTagCache {
private readonly ConcurrentDictionary<string, HashSet<string>> _dictionary;
public MemoryAsyncTagCache() {
_dictionary = new ConcurrentDictionary<string, HashSet<string>>(StringComparer.OrdinalIgnoreCase);
}
public async Task TagAsync(string tag, params string[] keys) {
var set = _dictionary.GetOrAdd(tag, x => new HashSet<string>());
lock (set) {
foreach (var key in keys) {
set.Add(key);
}
}
}
public async Task<IEnumerable<string>> GetTaggedItemsAsync(string tag) {
HashSet<string> set;
if (_dictionary.TryGetValue(tag, out set)) {
lock (set) {
return set.ToReadOnlyCollection();
}
}
return Enumerable.Empty<string>();
}
public async Task RemoveTagAsync(string tag) {
HashSet<string> set;
_dictionary.TryRemove(tag, out set);
}
}
}

View File

@@ -0,0 +1,695 @@
using System;
using System.Collections.Generic;
using Orchard.OutputCache.Models;
using Orchard.Environment.Extensions;
using Orchard.Logging;
using Orchard.Services;
using Orchard.FileSystems.AppData;
using Orchard.Environment.Configuration;
using System.Web;
using Orchard.Owin;
using Orchard.Environment;
using Owin;
using Orchard.OutputCache.Filters;
using Orchard.Caching;
using Orchard.Themes;
using Microsoft.Owin;
using Orchard.UI.Admin;
using System.Web.Routing;
using Orchard.ContentManagement;
using System.Threading;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Globalization;
using Orchard.Utility.Extensions;
using System.Web.Mvc;
using Orchard.Mvc.Extensions;
using System.Collections.Specialized;
namespace Orchard.OutputCache.Services {
[OrchardFeature("Orchard.OutputCache2")]
public class OutputCacheOwinMiddleware : IOwinMiddlewareProvider {
private readonly IWorkContextAccessor _wca;
private readonly Work<ICacheManager> _cacheManager;
private readonly Work<IAsyncTagCache> _tagCache;
private readonly Work<IAsyncOutputCacheStorageProvider> _cacheStorageProvider;
//private readonly Work<IDisplayedContentItemHandler> _displayedContentItemHandler;
private readonly Work<IWorkContextAccessor> _workContextAccessor;
private readonly Work<IThemeManager> _themeManager;
private readonly Work<IClock> _clock;
//private readonly Work<ICacheService> _cacheService;
private readonly Work<ISignals> _signals;
private readonly Work<ShellSettings> _shellSettings;
private static string _refreshKey = "__r";
private static long _epoch = new DateTime(2014, DateTimeKind.Utc).Ticks;
public OutputCacheOwinMiddleware(
IWorkContextAccessor wca,
Work<ICacheManager> cacheManager,
Work<IAsyncOutputCacheStorageProvider> cacheStorageProvider,
Work<IAsyncTagCache> tagCache,
//Work<IDisplayedContentItemHandler> displayedContentItemHandler,
Work<IWorkContextAccessor> workContextAccessor,
Work<IThemeManager> themeManager,
Work<IClock> clock,
//Work<ICacheService> cacheService,
Work<ISignals> signals,
Work<ShellSettings> shellSettings) {
_cacheManager = cacheManager;
_cacheStorageProvider = cacheStorageProvider;
_tagCache = tagCache;
//_displayedContentItemHandler = displayedContentItemHandler;
_workContextAccessor = workContextAccessor;
_themeManager = themeManager;
_clock = clock;
//_cacheService = cacheService;
_signals = signals;
_shellSettings = shellSettings;
_wca = wca;
Logger = NullLogger.Instance;
}
public ILogger Logger { get; set; }
public IEnumerable<OwinMiddlewareRegistration> GetOwinMiddlewares() {
return new[]
{
// Although we only construct a single OwinMiddlewareRegistration here, you could return multiple ones of course.
new OwinMiddlewareRegistration
{
// The priority value decides the order of OwinMiddlewareRegistrations. I.e. "0" will run before "10", but registrations
// without a priority value will run before the ones that have it set.
// Note that this priority notation is the same as the one for shape placement (so you can e.g. use ":before").
Priority = "0",
// This is the delegate that sets up middlewares.
Configure = app =>
app.Use( async (context, next) => {
var httpContext = context.Environment["System.Web.HttpContextBase"] as HttpContextBase;
var workContext = _wca.GetContext(httpContext);
var cacheManager = new DefaultCacheManager(typeof(OutputCacheOwinMiddleware), workContext.Resolve<ICacheHolder>()); // _cacheManager.Value;
var cacheStorageProvider = _cacheStorageProvider.Value;
//var displayedContentItemHandler = _displayedContentItemHandler.Value;
var themeManager = _themeManager.Value;
var clock = _clock.Value;
//var cacheService = _cacheService.Value;
var signals = _signals.Value;
var tagCache = _tagCache.Value;
var shellSettings = _shellSettings.Value;
var cacheSettings = cacheManager.Get(CacheSettings.CacheKey, ctx => {
ctx.Monitor(signals.When(CacheSettings.CacheKey));
// return new CacheSettings(workContext.CurrentSite.As<CacheSettingsPart>());
return new CacheSettings();
});
// State.
DateTime now;
string cacheKey;
string invariantCacheKey;
bool transformRedirect;
bool isCachingRequest;
bool isCacheableRequest;
#region OnActionExecuting
Logger.Debug("Incoming request for URL '{0}'.", httpContext.Request.RawUrl);
// This filter is not reentrant (multiple executions within the same request are
// not supported) so child actions are ignored completely.
//if (filterContext.IsChildAction) {
// Logger.Debug("Action '{0}' ignored because it's a child action.", filterContext.ActionDescriptor.ActionName);
// return;
//}
now = clock.UtcNow;
isCacheableRequest = RequestIsCacheable(context, workContext, cacheSettings);
// if the request can't be cached, process normally
if(!isCacheableRequest) {
await next.Invoke();
return;
}
// Computing the cache key after we know that the request is cacheable means that we are only performing this calculation on requests that require it
cacheKey = ComputeCacheKey(context, shellSettings, GetCacheKeyParameters(context, themeManager, workContext, cacheSettings));
invariantCacheKey = ComputeCacheKey(context, shellSettings, null);
Logger.Debug("Cache key '{0}' was created.", cacheKey);
try {
// Is there a cached item, and are we allowed to serve it?
var allowServeFromCache = httpContext.Request.Headers["Cache-Control"] != "no-cache" || cacheSettings.IgnoreNoCache;
var cacheItem = await GetCacheItemAsync(cacheKey, cacheStorageProvider);
if (allowServeFromCache && cacheItem != null) {
Logger.Debug("Item '{0}' was found in cache.", cacheKey);
// Is the cached item in its grace period?
if (cacheItem.IsInGracePeriod(now)) {
// Render the content unless another request is already doing so.
if (Monitor.TryEnter(String.Intern(cacheKey))) {
Logger.Debug("Item '{0}' is in grace period and not currently being rendered; rendering item...", cacheKey);
BeginRenderItem(context, cacheSettings, out isCachingRequest);
return;
}
}
// Cached item is not yet in its grace period, or is already being
// rendered by another request; serve it from cache.
Logger.Debug("Serving item '{0}' from cache.", cacheKey);
await ServeCachedItemAsync(context, cacheSettings, cacheItem);
return;
}
// No cached item found, or client doesn't want it; acquire the cache key
// lock to render the item.
Logger.Debug("Item '{0}' was not found in cache or client refuses it. Acquiring cache key lock...", cacheKey);
if (Monitor.TryEnter(String.Intern(cacheKey), TimeSpan.FromSeconds(20))) {
Logger.Debug("Cache key lock for item '{0}' was acquired.", cacheKey);
// Item might now have been rendered and cached by another request; if so serve it from cache.
if (allowServeFromCache) {
cacheItem = await GetCacheItemAsync(cacheKey, cacheStorageProvider);
if (cacheItem != null) {
Logger.Debug("Item '{0}' was now found; releasing cache key lock and serving from cache.", cacheKey);
Monitor.Exit(String.Intern(cacheKey));
await ServeCachedItemAsync(context, cacheSettings, cacheItem);
return;
}
}
}
// Either we acquired the cache key lock and the item was still not in cache, or
// the lock acquisition timed out. In either case render the item.
Logger.Debug("Rendering item '{0}'...", cacheKey);
BeginRenderItem(context, cacheSettings, out isCachingRequest);
}
catch {
// Remember to release the cache key lock in the event of an exception!
Logger.Debug("Exception occurred for item '{0}'; releasing any acquired lock.", cacheKey);
if (Monitor.IsEntered(String.Intern(cacheKey)))
Monitor.Exit(String.Intern(cacheKey));
throw;
}
#endregion
//var response = httpContext.Response;
//var captureStream = new CaptureStream(response.Filter);
//response.Filter = captureStream;
//captureStream.Captured += (output) => {
// cacheItem = new CacheItem() {
// Output = output,
// ContentType = response.ContentType,
// StatusCode = response.StatusCode,
// Duration = 300
// };
// using (var scope = _wca.CreateWorkContextScope()) {
// var appDataFolder = scope.Resolve<IAppDataFolder>();
// var shellSettings = scope.Resolve<ShellSettings>();
// var filename = appDataFolder.Combine("OutputCache", shellSettings.Name, HttpUtility.UrlEncode(url));
// using (var stream = Serialize(cacheItem)) {
// using (var fileStream = appDataFolder.CreateFile(filename)) {
// stream.CopyToAsync(fileStream);
// }
// }
// }
//};
await next.Invoke();
transformRedirect = await TransformRedirectAsync(context, cacheSettings, shellSettings, now, tagCache, cacheStorageProvider);
#region OnResultExecuted
var captureHandlerIsAttached = false;
try {
// This filter is not reentrant (multiple executions within the same request are
// not supported) so child actions are ignored completely.
//if (filterContext.IsChildAction )
// return;
if(!isCachingRequest) {
return;
}
Logger.Debug("Item '{0}' was rendered.", cacheKey);
// Obtain individual route configuration, if any.
//CacheRouteConfig configuration = null;
//var configurations = cacheService.GetRouteConfigs();
//if (configurations.Any()) {
// var route = filterContext.Controller.ControllerContext.RouteData.Route;
// var key = _cacheService.GetRouteDescriptorKey(filterContext.HttpContext, route);
// configuration = configurations.FirstOrDefault(c => c.RouteKey == key);
//}
//if (!ResponseIsCacheable(filterContext, configuration)) {
// filterContext.HttpContext.Response.Cache.SetCacheability(HttpCacheability.NoCache);
// filterContext.HttpContext.Response.Cache.SetNoStore();
// filterContext.HttpContext.Response.Cache.SetMaxAge(new TimeSpan(0));
// return;
//}
// Determine duration and grace time.
//var cacheDuration = configuration != null && configuration.Duration.HasValue ? configuration.Duration.Value : CacheSettings.DefaultCacheDuration;
//var cacheGraceTime = configuration != null && configuration.GraceTime.HasValue ? configuration.GraceTime.Value : CacheSettings.DefaultCacheGraceTime;
// TODO: adapt
var cacheDuration = cacheSettings.DefaultCacheDuration;
var cacheGraceTime = cacheSettings.DefaultCacheGraceTime;
//// Include each content item ID as tags for the cache entry.
//var contentItemIds = _displayedContentItemHandler.GetDisplayed().Select(x => x.ToString(CultureInfo.InvariantCulture)).ToArray();
//// Capture the response output using a custom filter stream.
//var response = filterContext.HttpContext.Response;
var response = httpContext.Response;
var captureStream = new CaptureStream(response.Filter);
response.Filter = captureStream;
captureStream.Captured += (output) => {
try {
// Since this is a callback any call to injected dependencies can result in an Autofac exception: "Instances
// cannot be resolved and nested lifetimes cannot be created from this LifetimeScope as it has already been disposed."
// To prevent access to the original lifetime scope a new work context scope should be created here and dependencies
// should be resolved from it.
using (var scope = _wca.CreateWorkContextScope()) {
var cacheItem = new CacheItem() {
CachedOnUtc = now,
Duration = cacheDuration,
GraceTime = cacheGraceTime,
Output = output,
ContentType = response.ContentType,
QueryString = httpContext.Request.Url.Query,
CacheKey = cacheKey,
InvariantCacheKey = invariantCacheKey,
Url = httpContext.Request.Url.AbsolutePath,
Tenant = scope.Resolve<ShellSettings>().Name,
StatusCode = response.StatusCode,
Tags = new[] { invariantCacheKey } //.Union(contentItemIds).ToArray()
};
// Write the rendered item to the cache.
var localCacheStorageProvider = scope.Resolve<IAsyncOutputCacheStorageProvider>();
cacheStorageProvider.RemoveAsync(cacheKey);
cacheStorageProvider.SetAsync(cacheKey, cacheItem);
Logger.Debug("Item '{0}' was written to cache.", cacheKey);
// Also add the item tags to the tag cache.
var localTagCache = scope.Resolve<IAsyncTagCache>();
foreach (var tag in cacheItem.Tags) {
localTagCache.TagAsync(tag, cacheKey);
}
}
}
finally {
// Always release the cache key lock when the request ends.
ReleaseCacheKeyLock(cacheKey);
}
};
captureHandlerIsAttached = true;
}
finally {
// If the response filter stream capture handler was attached then we'll trust
// it to release the cache key lock at some point in the future when the stream
// is flushed; otherwise we'll make sure we'll release it here.
if (!captureHandlerIsAttached)
ReleaseCacheKeyLock(String.Intern(cacheKey));
}
#endregion
})
}
};
}
protected virtual bool RequestIsCacheable(IOwinContext context, WorkContext workContext, CacheSettings cacheSettings) {
var itemDescriptor = string.Empty;
var httpContext = context.Environment["System.Web.HttpContextBase"] as HttpContextBase;
//if (Logger.IsEnabled(LogLevel.Debug)) {
// var url = filterContext.RequestContext.HttpContext.Request.RawUrl;
// var area = filterContext.RequestContext.RouteData.Values["area"];
// var controller = filterContext.ActionDescriptor.ControllerDescriptor.ControllerName;
// var action = filterContext.ActionDescriptor.ActionName;
// var culture = _workContext.CurrentCulture.ToLowerInvariant();
// var auth = filterContext.HttpContext.User.Identity.IsAuthenticated.ToString().ToLowerInvariant();
// var theme = _themeManager.GetRequestTheme(filterContext.RequestContext).Id.ToLowerInvariant();
// itemDescriptor = string.Format("{0} (Area: {1}, Controller: {2}, Action: {3}, Culture: {4}, Theme: {5}, Auth: {6})", url, area, controller, action, culture, theme, auth);
//}
// Respect OutputCacheAttribute if applied.
//var actionAttributes = filterContext.ActionDescriptor.GetCustomAttributes(typeof(OutputCacheAttribute), true);
//var controllerAttributes = filterContext.ActionDescriptor.ControllerDescriptor.GetCustomAttributes(typeof(OutputCacheAttribute), true);
//var outputCacheAttribute = actionAttributes.Concat(controllerAttributes).Cast<OutputCacheAttribute>().FirstOrDefault();
//if (outputCacheAttribute != null) {
// if (outputCacheAttribute.Duration <= 0 || outputCacheAttribute.NoStore) {
// Logger.Debug("Request for item '{0}' ignored based on OutputCache attribute.", itemDescriptor);
// return false;
// }
//}
// Don't cache POST requests.
if (context.Request.Method.Equals("POST", StringComparison.OrdinalIgnoreCase)) {
Logger.Debug("Request for item '{0}' ignored because HTTP method is POST.", itemDescriptor);
return false;
}
// Don't cache admin section requests.
if (AdminFilter.IsApplied(new RequestContext(httpContext, new RouteData()))) {
Logger.Debug("Request for item '{0}' ignored because it's in admin section.", itemDescriptor);
return false;
}
// Ignore authenticated requests unless the setting to cache them is true.
if (workContext.CurrentUser != null && !cacheSettings.CacheAuthenticatedRequests) {
Logger.Debug("Request for item '{0}' ignored because user is authenticated.", itemDescriptor);
return false;
}
// Don't cache ignored URLs.
if (IsIgnoredUrl(httpContext.Request.AppRelativeCurrentExecutionFilePath, cacheSettings.IgnoredUrls)) {
Logger.Debug("Request for item '{0}' ignored because the URL is configured as ignored.", itemDescriptor);
return false;
}
// Ignore requests with the refresh key on the query string.
foreach (var key in httpContext.Request.QueryString.AllKeys) {
if (String.Equals(_refreshKey, key, StringComparison.OrdinalIgnoreCase)) {
Logger.Debug("Request for item '{0}' ignored because refresh key was found on query string.", itemDescriptor);
return false;
}
}
return true;
}
protected virtual bool ResponseIsCacheable(IOwinContext context, CacheRouteConfig configuration, bool transformRedirect, string cacheKey) {
var httpContext = context.Environment["System.Web.HttpContextBase"] as HttpContextBase;
if (httpContext.Request.Url == null) {
return false;
}
// Don't cache non-200 responses or results of a redirect.
var response = httpContext.Response;
if (response.StatusCode != 200 || transformRedirect) {
return false;
}
// Don't cache in individual route configuration says no.
if (configuration != null && configuration.Duration == 0) {
Logger.Debug("Response for item '{0}' will not be cached because route is configured to not be cached.", cacheKey);
return false;
}
// Don't cache if request created notifications.
//var hasNotifications = !String.IsNullOrEmpty(Convert.ToString(filterContext.Controller.TempData["messages"]));
//if (hasNotifications) {
// Logger.Debug("Response for item '{0}' will not be cached because one or more notifications were created.", _cacheKey);
// return false;
//}
return true;
}
protected virtual IDictionary<string, object> GetCacheKeyParameters(IOwinContext context, IThemeManager themeManager, WorkContext workContext, CacheSettings cacheSettings) {
var result = new Dictionary<string, object>();
var httpContext = context.Environment["System.Web.HttpContextBase"] as HttpContextBase;
// Vary by action parameters.
//foreach (var p in filterContext.ActionParameters)
// result.Add("PARAM:" + p.Key, p.Value);
// Vary by theme.
result.Add("theme", themeManager.GetRequestTheme(httpContext.Request.RequestContext).Id.ToLowerInvariant());
// Vary by configured query string parameters.
var queryString = httpContext.Request.RequestContext.HttpContext.Request.QueryString;
foreach (var key in queryString.AllKeys) {
if (key == null || (cacheSettings.VaryByQueryStringParameters != null && !cacheSettings.VaryByQueryStringParameters.Contains(key)))
continue;
result[key] = queryString[key];
}
// Vary by configured request headers.
var requestHeaders = httpContext.Request.RequestContext.HttpContext.Request.Headers;
foreach (var varyByRequestHeader in cacheSettings.VaryByRequestHeaders) {
if (requestHeaders.AllKeys.Contains(varyByRequestHeader))
result["HEADER:" + varyByRequestHeader] = requestHeaders[varyByRequestHeader];
}
// Vary by request culture if configured.
if (cacheSettings.VaryByCulture) {
result["culture"] = workContext.CurrentCulture.ToLowerInvariant();
}
// Vary by authentication state if configured.
if (cacheSettings.VaryByAuthenticationState) {
result["auth"] = httpContext.User.Identity.IsAuthenticated.ToString().ToLowerInvariant();
}
return result;
}
protected virtual async Task<bool> TransformRedirectAsync(IOwinContext context, CacheSettings cacheSettings, ShellSettings shellSettings, DateTime now, IAsyncTagCache tagCache, IAsyncOutputCacheStorageProvider cacheStorageProvider) {
var httpContext = context.Environment["System.Web.HttpContextBase"] as HttpContextBase;
// Removes the target of the redirection from cache after a POST.
//if (filterContext.Result == null) {
// throw new ArgumentNullException();
//}
if (AdminFilter.IsApplied(new RequestContext(httpContext, new RouteData()))) {
return false;
}
RedirectResult redirectResult = null; // filterContext.Result as RedirectResult;
// status code can't be tested at this point, so test the result type instead
if (redirectResult == null || !httpContext.Request.HttpMethod.Equals("POST", StringComparison.OrdinalIgnoreCase)) {
return false;
}
Logger.Debug("Redirect on POST detected; removing from cache and adding refresh key.");
var redirectUrl = redirectResult.Url;
if (httpContext.Request.IsLocalUrl(redirectUrl)) {
// Remove all cached versions of the same item.
var helper = new UrlHelper(httpContext.Request.RequestContext);
var absolutePath = new Uri(helper.MakeAbsolute(redirectUrl)).AbsolutePath;
var invariantCacheKey = ComputeCacheKey(shellSettings.Name, absolutePath, null);
foreach (var key in await tagCache.GetTaggedItemsAsync(invariantCacheKey)) {
await cacheStorageProvider.RemoveAsync(key);
}
// we no longer need the tag entry as the items have been removed
await tagCache.RemoveTagAsync(invariantCacheKey);
}
// Adding a refresh key so that the redirection doesn't get restored
// from a cached version on a proxy. This can happen when using public
// caching, we want to force the client to get a fresh copy of the
// redirectUrl content.
if (cacheSettings.DefaultMaxAge > 0) {
var epIndex = redirectUrl.IndexOf('?');
var qs = new NameValueCollection();
if (epIndex > 0) {
qs = HttpUtility.ParseQueryString(redirectUrl.Substring(epIndex));
}
// Substract Epoch to get a smaller number.
var refresh = now.Ticks - _epoch;
qs.Remove(_refreshKey);
qs.Add(_refreshKey, refresh.ToString("x"));
var querystring = "?" + string.Join("&", Array.ConvertAll(qs.AllKeys, k => string.Format("{0}={1}", HttpUtility.UrlEncode(k), HttpUtility.UrlEncode(qs[k]))));
if (epIndex > 0) {
redirectUrl = redirectUrl.Substring(0, epIndex) + querystring;
}
else {
redirectUrl = redirectUrl + querystring;
}
}
//filterContext.Result = new RedirectResult(redirectUrl, redirectResult.Permanent);
//filterContext.HttpContext.Response.Cache.SetCacheability(HttpCacheability.NoCache);
return true;
}
private async Task ServeCachedItemAsync(IOwinContext context, CacheSettings cacheSettings, CacheItem cacheItem) {
var httpContext = context.Environment["System.Web.HttpContextBase"] as HttpContextBase;
// Fix for missing charset in response headers
// context.Response.Charset = response.Charset;
// Adds some caching information to the output if requested.
if (cacheSettings.DebugMode) {
//response.AddHeader("X-Cached-On", cacheItem.CachedOnUtc.ToString("r"));
//response.AddHeader("X-Cached-Until", cacheItem.ValidUntilUtc.ToString("r"));
}
context.Response.ContentType = cacheItem.ContentType;
await context.Response.WriteAsync(cacheItem.Output);
context.Response.StatusCode = cacheItem.StatusCode;
ApplyCacheControl(context, cacheSettings);
}
private void BeginRenderItem(IOwinContext context, CacheSettings cacheSettings, out bool isCachingRequest) {
var httpContext = context.Environment["System.Web.HttpContextBase"] as HttpContextBase;
var response = httpContext.Response;
ApplyCacheControl(context, cacheSettings);
// Remember that we should intercept the rendered response output.
isCachingRequest = true;
}
private void ApplyCacheControl(IOwinContext context, CacheSettings cacheSettings) {
if (cacheSettings.DefaultMaxAge > 0) {
var maxAge = TimeSpan.FromSeconds(cacheSettings.DefaultMaxAge); //cacheItem.ValidUntilUtc - _clock.UtcNow;
if (maxAge.TotalMilliseconds < 0) {
maxAge = TimeSpan.Zero;
}
//response.Cache.SetCacheability(HttpCacheability.Public);
//response.Cache.SetMaxAge(maxAge);
}
// Keeping this example for later usage.
// response.DisableUserCache();
// response.DisableKernelCache();
// response.Cache.SetOmitVaryStar(true);
// An ETag is a string that uniquely identifies a specific version of a component.
// We use the cache item to detect if it's a new one.
if (HttpRuntime.UsingIntegratedPipeline) {
//if (response.Headers.Get("ETag") == null) {
// // What is the point of GetHashCode() of a newly generated item? /DanielStolt
// response.Cache.SetETag(new CacheItem().GetHashCode().ToString(CultureInfo.InvariantCulture));
//}
}
//if (cacheSettings.VaryByQueryStringParameters == null) {
// response.Cache.VaryByParams["*"] = true;
//}
//else {
// foreach (var queryStringParam in cacheSettings.VaryByQueryStringParameters) {
// response.Cache.VaryByParams[queryStringParam] = true;
// }
//}
//foreach (var varyRequestHeader in cacheSettings.VaryByRequestHeaders) {
// response.Cache.VaryByHeaders[varyRequestHeader] = true;
//}
}
private void ReleaseCacheKeyLock(string cacheKey) {
if (cacheKey != null) {
if (Monitor.IsEntered(String.Intern(cacheKey))) {
Logger.Debug("Releasing cache key lock for item '{0}'.", cacheKey);
Monitor.Exit(String.Intern(cacheKey));
}
}
}
protected virtual bool IsIgnoredUrl(string url, IEnumerable<string> ignoredUrls) {
if (ignoredUrls == null || !ignoredUrls.Any()) {
return false;
}
url = url.TrimStart(new[] { '~' });
foreach (var ignoredUrl in ignoredUrls) {
var relativePath = ignoredUrl.TrimStart(new[] { '~' }).Trim();
if (String.IsNullOrWhiteSpace(relativePath)) {
continue;
}
// Ignore comments
if (relativePath.StartsWith("#")) {
continue;
}
if (String.Equals(relativePath, url, StringComparison.OrdinalIgnoreCase)) {
return true;
}
}
return false;
}
protected virtual string ComputeCacheKey(IOwinContext context, ShellSettings shellSettings, IEnumerable<KeyValuePair<string, object>> parameters) {
var httpContext = context.Environment["System.Web.HttpContextBase"] as HttpContextBase;
var url = httpContext.Request.Url.AbsolutePath;
return ComputeCacheKey(shellSettings.Name, url, parameters);
}
protected virtual string ComputeCacheKey(string tenant, string absoluteUrl, IEnumerable<KeyValuePair<string, object>> parameters) {
var keyBuilder = new StringBuilder();
keyBuilder.AppendFormat("tenant={0};url={1};", tenant, absoluteUrl.ToLowerInvariant());
if (parameters != null) {
foreach (var pair in parameters) {
keyBuilder.AppendFormat("{0}={1};", pair.Key.ToLowerInvariant(), Convert.ToString(pair.Value).ToLowerInvariant());
}
}
return keyBuilder.ToString();
}
protected virtual async Task<CacheItem> GetCacheItemAsync(string key, IAsyncOutputCacheStorageProvider cacheStorageProvider) {
try {
var cacheItem = await cacheStorageProvider.GetAsync(key);
return cacheItem;
}
catch (Exception e) {
Logger.Error(e, "An unexpected error occured while reading a cache entry");
}
return null;
}
}
}

View File

@@ -25,3 +25,8 @@ Features:
Description: Business data cache using Redis.
Category: Performance
Dependencies: Orchard.Caching, Orchard.Redis
Orchard.Redis.OutputCache2:
Name: Redis Output Cache 2
Description: An output cache storage provider using Redis.
Category: Performance
Dependencies: Orchard.OutputCache2, Orchard.Redis

View File

@@ -111,7 +111,9 @@
<Compile Include="Configuration\IRedisConnectionProvider.cs" />
<Compile Include="Configuration\RedisConnectionProvider.cs" />
<Compile Include="MessageBus\RedisMessageBusBroker.cs" />
<Compile Include="OutputCache\RedisAsyncOutputCacheStorageProvider.cs" />
<Compile Include="OutputCache\RedisOutputCacheStorageProvider.cs" />
<Compile Include="OutputCache\RedisAsyncTagCache.cs" />
<Compile Include="OutputCache\RedisTagCache.cs" />
</ItemGroup>
<PropertyGroup>

View File

@@ -0,0 +1,169 @@
using System;
using System.Collections.Generic;
using System.Linq;
using Orchard.Environment.Configuration;
using Orchard.Environment.Extensions;
using Orchard.Logging;
using Orchard.Redis.Configuration;
using Orchard.OutputCache.Models;
using Orchard.OutputCache.Services;
using Orchard.Redis.Extensions;
using StackExchange.Redis;
using System.Runtime.Serialization.Formatters.Binary;
using System.IO;
using System.IO.Compression;
using System.Threading.Tasks;
namespace Orchard.Redis.OutputCache {
[OrchardFeature("Orchard.Redis.OutputCache2")]
[OrchardSuppressDependency("Orchard.OutputCache.Services.MemoryAsyncOutputCacheStorageProvider")]
public class RedisAsyncOutputCacheStorageProvider : IAsyncOutputCacheStorageProvider {
private readonly ShellSettings _shellSettings;
private readonly IRedisConnectionProvider _redisConnectionProvider;
private HashSet<string> _keysCache;
public const string ConnectionStringKey = "Orchard.Redis.OutputCache";
private readonly string _connectionString;
private readonly ConnectionMultiplexer _connectionMultiplexer;
public RedisAsyncOutputCacheStorageProvider(ShellSettings shellSettings, IRedisConnectionProvider redisConnectionProvider) {
_shellSettings = shellSettings;
_redisConnectionProvider = redisConnectionProvider;
_connectionString = _redisConnectionProvider.GetConnectionString(ConnectionStringKey);
_connectionMultiplexer = _redisConnectionProvider.GetConnection(_connectionString);
Logger = NullLogger.Instance;
}
public ILogger Logger { get; set; }
public IDatabase Database {
get {
return _connectionMultiplexer.GetDatabase();
}
}
public async Task SetAsync(string key, CacheItem cacheItem) {
if(cacheItem == null) {
throw new ArgumentNullException("cacheItem");
}
if (cacheItem.ValidFor <= 0) {
return;
}
using (var decompressedStream = Serialize(cacheItem)) {
using (var compressedStream = await CompressAsync(decompressedStream)) {
await Database.StringSetAsync(GetLocalizedKey(key), compressedStream.ToArray(), TimeSpan.FromSeconds(cacheItem.ValidFor));
}
}
}
public async Task RemoveAsync(string key) {
await Database.KeyDeleteAsync(GetLocalizedKey(key));
}
public async Task RemoveAllAsync() {
Database.KeyDeleteWithPrefix(GetLocalizedKey("*"));
}
public async Task<CacheItem> GetAsync(string key) {
var value = await Database.StringGetAsync(GetLocalizedKey(key));
if (value.IsNullOrEmpty) {
return null;
}
using (var compressedStream = new MemoryStream(value)) {
if(compressedStream.Length == 0) {
return null;
}
using(var decompressedStream = await DecompressAsync(compressedStream)) {
return Deserialize(decompressedStream);
}
}
}
public async Task<IEnumerable<CacheItem>> GetAllAsync(int skip, int count) {
var result = new List<CacheItem>();
foreach (var key in GetAllKeys().Skip(skip).Take(count)) {
var cacheItem = await GetAsync(key);
// the item could have expired in the meantime
if (cacheItem != null) {
result.Add(cacheItem);
}
}
return result;
}
public async Task<int> CountAsync() {
return Database.KeyCount(GetLocalizedKey("*"));
}
/// <summary>
/// Creates a namespaced key to support multiple tenants on top of a single Redis connection.
/// </summary>
/// <param name="key">The key to localized.</param>
/// <returns>A localized key based on the tenant name.</returns>
private string GetLocalizedKey(string key) {
return _shellSettings.Name + ":OC:" + CacheItem.Version + ":" + key;
}
/// <summary>
/// Returns all the keys for the current tenant.
/// </summary>
/// <returns>The keys for the current tenant.</returns>
private IEnumerable<string> GetAllKeys() {
// prevent the same request from computing the list twice (count + list)
if (_keysCache == null) {
_keysCache = new HashSet<string>();
var prefix = GetLocalizedKey("");
foreach (var endPoint in _connectionMultiplexer.GetEndPoints()) {
var server = _connectionMultiplexer.GetServer(endPoint);
foreach (var key in server.Keys(pattern: GetLocalizedKey("*"))) {
_keysCache.Add(key.ToString().Substring(prefix.Length));
}
}
}
return _keysCache;
}
private static MemoryStream Serialize(CacheItem item) {
BinaryFormatter binaryFormatter = new BinaryFormatter();
var memoryStream = new MemoryStream();
binaryFormatter.Serialize(memoryStream, item);
memoryStream.Seek(0, SeekOrigin.Begin);
return memoryStream;
}
private static CacheItem Deserialize(Stream stream) {
BinaryFormatter binaryFormatter = new BinaryFormatter();
var result = (CacheItem)binaryFormatter.Deserialize(stream);
return result;
}
private static async Task<MemoryStream> CompressAsync(Stream stream) {
var compressedStream = new MemoryStream();
using (var compressionStream = new GZipStream(compressedStream, CompressionMode.Compress)) {
await stream.CopyToAsync(compressionStream);
return compressedStream;
}
}
private static async Task<Stream> DecompressAsync(Stream stream) {
var decompressedStream = new MemoryStream();
using (GZipStream decompressionStream = new GZipStream(stream, CompressionMode.Decompress)) {
await decompressionStream.CopyToAsync(decompressedStream);
decompressedStream.Seek(0, SeekOrigin.Begin);
return decompressedStream;
}
}
}
}

View File

@@ -0,0 +1,50 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Orchard.Environment.Configuration;
using Orchard.Environment.Extensions;
using Orchard.OutputCache.Services;
using Orchard.Redis.Configuration;
using StackExchange.Redis;
namespace Orchard.Redis.OutputCache {
[OrchardFeature("Orchard.Redis.OutputCache2")]
[OrchardSuppressDependency("Orchard.OutputCache.Services.MemoryAsyncTagCache")]
public class RedisAsyncTagCache : IAsyncTagCache {
private readonly IRedisConnectionProvider _redisConnectionProvider;
private readonly string _connectionString;
private readonly ConnectionMultiplexer _connectionMultiplexer;
private readonly ShellSettings _shellSettings;
public RedisAsyncTagCache(IRedisConnectionProvider redisConnectionProvider, ShellSettings shellSettings) {
_redisConnectionProvider = redisConnectionProvider;
_connectionString = _redisConnectionProvider.GetConnectionString(RedisOutputCacheStorageProvider.ConnectionStringKey);
_connectionMultiplexer = _redisConnectionProvider.GetConnection(_connectionString);
_shellSettings = shellSettings;
}
private IDatabase Database {
get { return _connectionMultiplexer.GetDatabase(); }
}
public async Task TagAsync(string tag, params string[] keys) {
await Database.SetAddAsync(GetLocalizedKey(tag), Array.ConvertAll(keys, x=> (RedisValue) x));
}
public async Task<IEnumerable<string>> GetTaggedItemsAsync(string tag) {
var values = await Database.SetMembersAsync(GetLocalizedKey(tag));
if (values == null || values.Length == 0)
return Enumerable.Empty<string>();
return Array.ConvertAll(values, x => (string) x);
}
public async Task RemoveTagAsync(string tag) {
await Database.KeyDeleteAsync(GetLocalizedKey(tag));
}
private string GetLocalizedKey(string key) {
return _shellSettings.Name + ":Tag:" + key;
}
}
}

View File

@@ -14,7 +14,9 @@ namespace Orchard.Owin {
if (handler == null) {
throw new ArgumentException("orchard.Handler can't be null");
}
await handler();
});
return app;