U4-9322 - begin implementing scoped repository caches

This commit is contained in:
Stephan
2017-01-19 19:47:09 +01:00
parent 4ab7f768fa
commit 434703f4ec
14 changed files with 384 additions and 35 deletions

View File

@@ -238,7 +238,9 @@ namespace Umbraco.Core.Cache
/// <inheritdoc />
public override void ClearAll()
{
Cache.ClearCacheByKeySearch(GetEntityTypeCacheKey());
// fixme the cache should NOT contain anything else so we can clean all, can't we?
Cache.ClearAllCache();
//Cache.ClearCacheByKeySearch(GetEntityTypeCacheKey());
}
}
}

View File

@@ -0,0 +1,83 @@
using System;
using System.Collections.Generic;
using Umbraco.Core.Models.EntityBase;
using Umbraco.Core.Scoping;
namespace Umbraco.Core.Cache
{
internal class ScopedDefaultRepositoryCachePolicy<TEntity, TId> : IRepositoryCachePolicy<TEntity, TId>
where TEntity : class, IAggregateRoot
{
private readonly DefaultRepositoryCachePolicy<TEntity, TId> _cachePolicy;
private readonly IRuntimeCacheProvider _globalIsolatedCache;
private readonly IScope _scope;
public ScopedDefaultRepositoryCachePolicy(DefaultRepositoryCachePolicy<TEntity, TId> cachePolicy, IRuntimeCacheProvider globalIsolatedCache, IScope scope)
{
_cachePolicy = cachePolicy;
_globalIsolatedCache = globalIsolatedCache;
_scope = scope;
}
// when the scope completes we need to clear the global isolated cache
// for now, we are not doing it selectively at all - just kill everything
private void RegisterDirty(TEntity entity = null)
{
// "name" would be used to de-duplicate?
// fixme - casting!
((Scope) _scope).Register("name", completed => _globalIsolatedCache.ClearAllCache());
}
public TEntity Get(TId id, Func<TId, TEntity> performGet, Func<TId[], IEnumerable<TEntity>> performGetAll)
{
// loads into the local cache only, ok for now
return _cachePolicy.Get(id, performGet, performGetAll);
}
public TEntity GetCached(TId id)
{
// loads into the local cache only, ok for now
return _cachePolicy.GetCached(id);
}
public bool Exists(TId id, Func<TId, bool> performExists, Func<TId[], IEnumerable<TEntity>> performGetAll)
{
// loads into the local cache only, ok for now
return _cachePolicy.Exists(id, performExists, performGetAll);
}
public void Create(TEntity entity, Action<TEntity> persistNew)
{
// writes into the local cache
_cachePolicy.Create(entity, persistNew);
RegisterDirty(entity);
}
public void Update(TEntity entity, Action<TEntity> persistUpdated)
{
// writes into the local cache
_cachePolicy.Update(entity, persistUpdated);
RegisterDirty(entity);
}
public void Delete(TEntity entity, Action<TEntity> persistDeleted)
{
// deletes the local cache
_cachePolicy.Delete(entity, persistDeleted);
RegisterDirty(entity);
}
public TEntity[] GetAll(TId[] ids, Func<TId[], IEnumerable<TEntity>> performGetAll)
{
// loads into the local cache only, ok for now
return _cachePolicy.GetAll(ids, performGetAll);
}
public void ClearAll()
{
// fixme - what's this doing?
_cachePolicy.ClearAll();
RegisterDirty();
}
}
}

View File

@@ -9,6 +9,7 @@ using Umbraco.Core.Models.EntityBase;
using Umbraco.Core.Persistence.Querying;
using Umbraco.Core.Persistence.UnitOfWork;
using Umbraco.Core.Scoping;
namespace Umbraco.Core.Persistence.Repositories
{
@@ -84,7 +85,7 @@ namespace Umbraco.Core.Persistence.Repositories
#region Static Queries
private IQuery<TEntity> _hasIdQuery;
private static IQuery<TEntity> _hasIdQuery;
#endregion
@@ -101,23 +102,56 @@ namespace Umbraco.Core.Persistence.Repositories
get { return RepositoryCache.IsolatedRuntimeCache.GetOrCreateCache<TEntity>(); }
}
private IRepositoryCachePolicy<TEntity, TId> _cachePolicy;
private static RepositoryCachePolicyOptions _defaultOptions;
protected virtual RepositoryCachePolicyOptions DefaultOptions
{
get
{
return _defaultOptions ?? (_defaultOptions
= new RepositoryCachePolicyOptions(() =>
{
// get count of all entities of current type (TEntity) to ensure cached result is correct
// create query once if it is needed (no need for locking here) - query is static!
var query = _hasIdQuery ?? (_hasIdQuery = Query<TEntity>.Builder.Where(x => x.Id != 0));
return PerformCount(query);
}));
}
}
// this would be better for perfs BUT it breaks the tests - l8tr
//
//private static IRepositoryCachePolicy<TEntity, TId> _defaultCachePolicy;
//protected virtual IRepositoryCachePolicy<TEntity, TId> DefaultCachePolicy
//{
// get
// {
// return _defaultCachePolicy ?? (_defaultCachePolicy
// = new DefaultRepositoryCachePolicy<TEntity, TId>(RuntimeCache, DefaultOptions));
// }
//}
private IRepositoryCachePolicy<TEntity, TId> _cachePolicy;
protected virtual IRepositoryCachePolicy<TEntity, TId> CachePolicy
{
get
{
if (_cachePolicy != null) return _cachePolicy;
var options = new RepositoryCachePolicyOptions(() =>
var scope = ((PetaPocoUnitOfWork) UnitOfWork).Scope;
switch (scope.RepositoryCacheMode)
{
//Get count of all entities of current type (TEntity) to ensure cached result is correct
//create query once if it is needed (no need for locking here)
var query = _hasIdQuery ?? (_hasIdQuery = Query<TEntity>.Builder.Where(x => x.Id != 0));
return PerformCount(query);
});
_cachePolicy = new DefaultRepositoryCachePolicy<TEntity, TId>(RuntimeCache, options);
case RepositoryCacheMode.Default:
//_cachePolicy = DefaultCachePolicy;
_cachePolicy = new DefaultRepositoryCachePolicy<TEntity, TId>(RuntimeCache, DefaultOptions);
break;
case RepositoryCacheMode.Scoped:
var scopedCache = scope.IsolatedRuntimeCache.GetOrCreateCache<TEntity>();
var scopedPolicy = new DefaultRepositoryCachePolicy<TEntity, TId>(scopedCache, DefaultOptions);
_cachePolicy = new ScopedDefaultRepositoryCachePolicy<TEntity, TId>(scopedPolicy, RuntimeCache, scope);
break;
default:
throw new Exception();
}
return _cachePolicy;
}

View File

@@ -144,14 +144,14 @@ namespace Umbraco.Core.Persistence.UnitOfWork
get { return _key; }
}
private IScope ThisScope
public IScope Scope
{
get { return _scope ?? (_scope = _scopeProvider.CreateScope(_isolationLevel)); }
}
public UmbracoDatabase Database
{
get { return ThisScope.Database; }
get { return Scope.Database; }
}
#region Operation

View File

@@ -1,5 +1,6 @@
using System;
using System.Collections.Generic;
using Umbraco.Core.Cache;
using Umbraco.Core.Events;
using Umbraco.Core.Persistence;
@@ -20,6 +21,14 @@ namespace Umbraco.Core.Scoping
/// </summary>
IList<EventMessage> Messages { get; }
/// <summary>
/// Gets the repository cache mode.
/// </summary>
RepositoryCacheMode RepositoryCacheMode { get; }
// fixme
IsolatedRuntimeCache IsolatedRuntimeCache { get; }
/// <summary>
/// Completes the scope.
/// </summary>

View File

@@ -17,7 +17,7 @@ namespace Umbraco.Core.Scoping
/// <para>If an ambient scope already exists, it becomes the parent of the created scope.</para>
/// <para>When the created scope is disposed, the parent scope becomes the ambient scope again.</para>
/// </remarks>
IScope CreateScope(IsolationLevel isolationLevel = IsolationLevel.Unspecified);
IScope CreateScope(IsolationLevel isolationLevel = IsolationLevel.Unspecified, RepositoryCacheMode repositoryCacheMode = RepositoryCacheMode.Unspecified);
/// <summary>
/// Creates a detached scope.
@@ -27,7 +27,7 @@ namespace Umbraco.Core.Scoping
/// <para>A detached scope is not ambient and has no parent.</para>
/// <para>It is meant to be attached by <see cref="AttachScope"/>.</para>
/// </remarks>
IScope CreateDetachedScope(IsolationLevel isolationLevel = IsolationLevel.Unspecified);
IScope CreateDetachedScope(IsolationLevel isolationLevel = IsolationLevel.Unspecified, RepositoryCacheMode repositoryCacheMode = RepositoryCacheMode.Unspecified);
/// <summary>
/// Attaches a scope.

View File

@@ -1,5 +1,6 @@
using System;
using System.Collections.Generic;
using Umbraco.Core.Cache;
using Umbraco.Core.Events;
using Umbraco.Core.Persistence;
@@ -29,6 +30,15 @@ namespace Umbraco.Core.Scoping
public Guid InstanceId { get { return _instanceId; } }
#endif
/// <inheritdoc />
public RepositoryCacheMode RepositoryCacheMode
{
get { return RepositoryCacheMode.Default; }
}
/// <inheritdoc />
public IsolatedRuntimeCache IsolatedRuntimeCache { get { throw new NotImplementedException(); } }
/// <inheritdoc />
public UmbracoDatabase Database
{

View File

@@ -0,0 +1,16 @@
namespace Umbraco.Core.Scoping
{
public enum RepositoryCacheMode
{
// ?
Unspecified = 0,
// the default, full L2 cache
Default = 1,
// a scoped cache
// reads from and writes to a local cache
// clears the global cache on completion
Scoped = 2
}
}

View File

@@ -1,6 +1,7 @@
using System;
using System.Collections.Generic;
using System.Data;
using Umbraco.Core.Cache;
using Umbraco.Core.Events;
using Umbraco.Core.Persistence;
@@ -14,9 +15,11 @@ namespace Umbraco.Core.Scoping
{
private readonly ScopeProvider _scopeProvider;
private readonly IsolationLevel _isolationLevel;
private readonly RepositoryCacheMode _repositoryCacheMode;
private bool _disposed;
private bool? _completed;
private IsolatedRuntimeCache _isolatedRuntimeCache;
private UmbracoDatabase _database;
private IList<EventMessage> _messages;
@@ -24,10 +27,11 @@ namespace Umbraco.Core.Scoping
private const IsolationLevel DefaultIsolationLevel = IsolationLevel.ReadCommitted;
// initializes a new scope
public Scope(ScopeProvider scopeProvider, IsolationLevel isolationLevel = IsolationLevel.Unspecified, bool detachable = false)
public Scope(ScopeProvider scopeProvider, IsolationLevel isolationLevel = IsolationLevel.Unspecified, RepositoryCacheMode repositoryCacheMode = RepositoryCacheMode.Unspecified, bool detachable = false)
{
_scopeProvider = scopeProvider;
_isolationLevel = isolationLevel;
_repositoryCacheMode = repositoryCacheMode;
Detachable = detachable;
#if DEBUG_SCOPES
_scopeProvider.Register(this);
@@ -35,15 +39,19 @@ namespace Umbraco.Core.Scoping
}
// initializes a new scope in a nested scopes chain, with its parent
public Scope(ScopeProvider scopeProvider, Scope parent, IsolationLevel isolationLevel = IsolationLevel.Unspecified)
: this(scopeProvider, isolationLevel)
public Scope(ScopeProvider scopeProvider, Scope parent, IsolationLevel isolationLevel = IsolationLevel.Unspecified, RepositoryCacheMode repositoryCacheMode = RepositoryCacheMode.Unspecified)
: this(scopeProvider, isolationLevel, repositoryCacheMode)
{
ParentScope = parent;
// cannot specify a different mode!
if (repositoryCacheMode != RepositoryCacheMode.Unspecified && parent.RepositoryCacheMode != repositoryCacheMode)
throw new ArgumentException("Cannot be different from parent.", "repositoryCacheMode");
}
// initializes a new scope, replacing a NoScope instance
public Scope(ScopeProvider scopeProvider, NoScope noScope, IsolationLevel isolationLevel = IsolationLevel.Unspecified)
: this(scopeProvider, isolationLevel)
public Scope(ScopeProvider scopeProvider, NoScope noScope, IsolationLevel isolationLevel = IsolationLevel.Unspecified, RepositoryCacheMode repositoryCacheMode = RepositoryCacheMode.Unspecified)
: this(scopeProvider, isolationLevel, repositoryCacheMode)
{
// steal everything from NoScope
_database = noScope.DatabaseOrNull;
@@ -59,6 +67,29 @@ namespace Umbraco.Core.Scoping
public Guid InstanceId { get { return _instanceId; } }
#endif
/// <inheritdoc />
public RepositoryCacheMode RepositoryCacheMode
{
get
{
if (_repositoryCacheMode != RepositoryCacheMode.Unspecified) return _repositoryCacheMode;
if (ParentScope != null) return ParentScope.RepositoryCacheMode;
return RepositoryCacheMode.Default;
}
}
/// <inheritdoc />
public IsolatedRuntimeCache IsolatedRuntimeCache
{
get
{
if (ParentScope != null) return ParentScope.IsolatedRuntimeCache;
return _isolatedRuntimeCache ?? (_isolatedRuntimeCache
= new IsolatedRuntimeCache(type => new DeepCloneRuntimeCacheProvider(new ObjectCacheRuntimeCacheProvider())));
}
}
// a value indicating whether the scope is detachable
// ie whether it was created by CreateDetachedScope
public bool Detachable { get; private set; }
@@ -208,20 +239,54 @@ namespace Umbraco.Core.Scoping
// at the moment we are totally not filtering the messages based on completion
// status, so whether the scope is committed or rolled back makes no difference
if (_database == null) return;
var completed = _completed.HasValue && _completed.Value;
try
if (_database != null)
{
if (_completed.HasValue && _completed.Value)
_database.CompleteTransaction();
else
_database.AbortTransaction();
try
{
if (completed)
_database.CompleteTransaction();
else
_database.AbortTransaction();
}
finally
{
_database.Dispose();
_database = null;
}
}
finally
// run everything we need to run when completing
foreach (var action in Actions.Values)
action(completed); // fixme try catch and everything
}
// fixme - wip
private IDictionary<string, Action<bool>> _actions;
private IDictionary<string, Action<bool>> Actions
{
get
{
_database.Dispose();
_database = null;
if (ParentScope != null) return ParentScope.Actions;
return _actions ?? (_actions
= new Dictionary<string, Action<bool>>());
}
}
public void Register(string name, Action action)
{
Actions[name] = completed =>
{
if (completed) action();
};
}
public void Register(string name, Action<bool> action)
{
Actions[name] = action;
}
}
}

View File

@@ -114,9 +114,9 @@ namespace Umbraco.Core.Scoping
}
/// <inheritdoc />
public IScope CreateDetachedScope(IsolationLevel isolationLevel = IsolationLevel.Unspecified)
public IScope CreateDetachedScope(IsolationLevel isolationLevel = IsolationLevel.Unspecified, RepositoryCacheMode repositoryCacheMode = RepositoryCacheMode.Unspecified)
{
return new Scope(this, isolationLevel, true);
return new Scope(this, isolationLevel, repositoryCacheMode, true);
}
/// <inheritdoc />
@@ -157,11 +157,11 @@ namespace Umbraco.Core.Scoping
}
/// <inheritdoc />
public IScope CreateScope(IsolationLevel isolationLevel = IsolationLevel.Unspecified)
public IScope CreateScope(IsolationLevel isolationLevel = IsolationLevel.Unspecified, RepositoryCacheMode repositoryCacheMode = RepositoryCacheMode.Unspecified)
{
var ambient = AmbientScope;
if (ambient == null)
return AmbientScope = new Scope(this, isolationLevel);
return AmbientScope = new Scope(this, isolationLevel, repositoryCacheMode);
// replace noScope with a real one
var noScope = ambient as NoScope;
@@ -174,13 +174,13 @@ namespace Umbraco.Core.Scoping
var database = noScope.DatabaseOrNull;
if (database != null && database.InTransaction)
throw new Exception("NoScope is in a transaction.");
return AmbientScope = new Scope(this, noScope, isolationLevel);
return AmbientScope = new Scope(this, noScope, isolationLevel, repositoryCacheMode);
}
var scope = ambient as Scope;
if (scope == null) throw new Exception("Ambient scope is not a Scope instance.");
return AmbientScope = new Scope(this, scope, isolationLevel);
return AmbientScope = new Scope(this, scope, isolationLevel, repositoryCacheMode);
}
/// <inheritdoc />

View File

@@ -156,6 +156,7 @@
<Compile Include="Cache\PayloadCacheRefresherBase.cs" />
<Compile Include="Cache\RepositoryCachePolicyOptions.cs" />
<Compile Include="Cache\StaticCacheProvider.cs" />
<Compile Include="Cache\ScopedDefaultRepositoryCachePolicy.cs" />
<Compile Include="Cache\TypedCacheRefresherBase.cs" />
<Compile Include="Cache\DeepCloneRuntimeCacheProvider.cs" />
<Compile Include="CodeAnnotations\FriendlyNameAttribute.cs" />
@@ -515,6 +516,7 @@
<Compile Include="Scoping\Scope.cs" />
<Compile Include="Scoping\ScopeProvider.cs" />
<Compile Include="Scoping\ScopeReference.cs" />
<Compile Include="Scoping\RepositoryCacheMode.cs" />
<Compile Include="Security\ActiveDirectoryBackOfficeUserPasswordChecker.cs" />
<Compile Include="Security\BackOfficeClaimsIdentityFactory.cs" />
<Compile Include="Security\BackOfficeCookieAuthenticationProvider.cs" />

View File

@@ -418,5 +418,25 @@ namespace Umbraco.Tests.Scoping
var db = nested.Database;
});
}
[TestCase(true)]
[TestCase(false)]
public void ScopeAction(bool complete)
{
var scopeProvider = DatabaseContext.ScopeProvider;
bool? completed = null;
Assert.IsNull(scopeProvider.AmbientScope);
using (var scope = scopeProvider.CreateScope())
{
((Scope) scope).Register("name", x => completed = x);
if (complete)
scope.Complete();
}
Assert.IsNull(scopeProvider.AmbientScope);
Assert.IsNotNull(completed);
Assert.AreEqual(complete, completed.Value);
}
}
}

View File

@@ -0,0 +1,107 @@
using System;
using NUnit.Framework;
using Umbraco.Core;
using Umbraco.Core.Cache;
using Umbraco.Core.Models.Membership;
using Umbraco.Core.Scoping;
using Umbraco.Tests.TestHelpers;
namespace Umbraco.Tests.Scoping
{
[TestFixture]
[DatabaseTestBehavior(DatabaseBehavior.NewDbFileAndSchemaPerTest)]
public class ScopedRepositoryTests : BaseDatabaseFactoryTest
{
// setup
public override void Initialize()
{
base.Initialize();
Assert.IsNull(DatabaseContext.ScopeProvider.AmbientScope); // gone
}
protected override CacheHelper CreateCacheHelper()
{
//return CacheHelper.CreateDisabledCacheHelper();
return new CacheHelper(
new ObjectCacheRuntimeCacheProvider(),
new StaticCacheProvider(),
new NullCacheProvider(),
new IsolatedRuntimeCache(type => new ObjectCacheRuntimeCacheProvider()));
}
[TestCase(true)]
[TestCase(false)]
public void Test(bool complete)
{
var scopeProvider = DatabaseContext.ScopeProvider;
var userService = ApplicationContext.Services.UserService;
var globalCache = ApplicationContext.ApplicationCache.IsolatedRuntimeCache.GetOrCreateCache(typeof(IUser));
var userType = userService.GetUserTypeByAlias("admin");
var user = (IUser) new User("name", "email", "username", "rawPassword", userType);
userService.Save(user);
// global cache contains the user entity
var globalCached = (IUser) globalCache.GetCacheItem(GetCacheIdKey<IUser>(user.Id), () => null);
Assert.IsNotNull(globalCached);
Assert.AreEqual(user.Id, globalCached.Id);
Assert.AreEqual("name", globalCached.Name);
Assert.IsNull(scopeProvider.AmbientScope);
using (var scope = scopeProvider.CreateScope(repositoryCacheMode: RepositoryCacheMode.Scoped))
{
Assert.IsInstanceOf<Scope>(scope);
Assert.IsNotNull(scopeProvider.AmbientScope);
Assert.AreSame(scope, scopeProvider.AmbientScope);
// scope has its own isolated cache
var scopedCache = scope.IsolatedRuntimeCache.GetOrCreateCache(typeof (IUser));
Assert.AreNotSame(globalCache, scopedCache);
user.Name = "changed";
ApplicationContext.Services.UserService.Save(user);
// scoped cache contains the "new" user entity
var scopeCached = (IUser) scopedCache.GetCacheItem(GetCacheIdKey<IUser>(user.Id), () => null);
Assert.IsNotNull(scopeCached);
Assert.AreEqual(user.Id, scopeCached.Id);
Assert.AreEqual("changed", scopeCached.Name);
// global cache is unchanged
globalCached = (IUser) globalCache.GetCacheItem(GetCacheIdKey<IUser>(user.Id), () => null);
Assert.IsNotNull(globalCached);
Assert.AreEqual(user.Id, globalCached.Id);
Assert.AreEqual("name", globalCached.Name);
if (complete)
scope.Complete();
}
Assert.IsNull(scopeProvider.AmbientScope);
// global cache has been cleared
globalCached = (IUser) globalCache.GetCacheItem(GetCacheIdKey<IUser>(user.Id), () => null);
Assert.IsNull(globalCached);
// get again, updated if completed
user = userService.GetUserById(user.Id);
Assert.AreEqual(complete ? "changed" : "name", user.Name);
// global cache contains the entity again
globalCached = (IUser) globalCache.GetCacheItem(GetCacheIdKey<IUser>(user.Id), () => null);
Assert.IsNotNull(globalCached);
Assert.AreEqual(user.Id, globalCached.Id);
Assert.AreEqual(complete ? "changed" : "name", globalCached.Name);
}
public static string GetCacheIdKey<T>(object id)
{
return string.Format("{0}{1}", GetCacheTypeKey<T>(), id);
}
public static string GetCacheTypeKey<T>()
{
return string.Format("uRepo_{0}_", typeof(T).Name);
}
}
}

View File

@@ -170,6 +170,7 @@
<Compile Include="Scheduling\DeployTest.cs" />
<Compile Include="Routing\NiceUrlRoutesTests.cs" />
<Compile Include="Scoping\LeakTests.cs" />
<Compile Include="Scoping\ScopedRepositoryTests.cs" />
<Compile Include="Scoping\ScopeTests.cs" />
<Compile Include="TestHelpers\Entities\MockedPropertyTypes.cs" />
<Compile Include="TryConvertToTests.cs" />