Files
Umbraco-CMS/src/Umbraco.Core/Scoping/Scope.cs

495 lines
18 KiB
C#

using System;
using System.Data;
using Umbraco.Core.Cache;
using Umbraco.Core.Composing;
using Umbraco.Core.Events;
using Umbraco.Core.IO;
using Umbraco.Core.Logging;
using Umbraco.Core.Persistence;
namespace Umbraco.Core.Scoping
{
/// <summary>
/// Implements <see cref="IScope"/>.
/// </summary>
/// <remarks>Not thread-safe obviously.</remarks>
internal class Scope : IScope
{
private readonly ScopeProvider _scopeProvider;
private readonly ILogger _logger;
private readonly IsolationLevel _isolationLevel;
private readonly RepositoryCacheMode _repositoryCacheMode;
private readonly bool? _scopeFileSystem;
private readonly bool _autoComplete;
private bool _callContext;
private bool _disposed;
private bool? _completed;
private IsolatedCaches _isolatedCaches;
private IUmbracoDatabase _database;
private EventMessages _messages;
private ICompletable _fscope;
private IEventDispatcher _eventDispatcher;
// initializes a new scope
private Scope(ScopeProvider scopeProvider,
ILogger logger, FileSystems fileSystems, Scope parent, ScopeContext scopeContext, bool detachable,
IsolationLevel isolationLevel = IsolationLevel.Unspecified,
RepositoryCacheMode repositoryCacheMode = RepositoryCacheMode.Unspecified,
IEventDispatcher eventDispatcher = null,
bool? scopeFileSystems = null,
bool callContext = false,
bool autoComplete = false)
{
_scopeProvider = scopeProvider;
_logger = logger;
Context = scopeContext;
_isolationLevel = isolationLevel;
_repositoryCacheMode = repositoryCacheMode;
_eventDispatcher = eventDispatcher;
_scopeFileSystem = scopeFileSystems;
_callContext = callContext;
_autoComplete = autoComplete;
Detachable = detachable;
#if DEBUG_SCOPES
_scopeProvider.RegisterScope(this);
Console.WriteLine("create " + InstanceId.ToString("N").Substring(0, 8));
#endif
if (detachable)
{
if (parent != null) throw new ArgumentException("Cannot set parent on detachable scope.", nameof(parent));
if (scopeContext != null) throw new ArgumentException("Cannot set context on detachable scope.", nameof(scopeContext));
if (autoComplete) throw new ArgumentException("Cannot auto-complete a detachable scope.", nameof(autoComplete));
// detachable creates its own scope context
Context = new ScopeContext();
// see note below
if (scopeFileSystems == true)
_fscope = fileSystems.Shadow();
return;
}
if (parent != null)
{
ParentScope = parent;
// cannot specify a different mode!
// TODO: means that it's OK to go from L2 to None for reading purposes, but writing would be BAD!
// this is for XmlStore that wants to bypass caches when rebuilding XML (same for NuCache)
if (repositoryCacheMode != RepositoryCacheMode.Unspecified && parent.RepositoryCacheMode > repositoryCacheMode)
throw new ArgumentException($"Value '{repositoryCacheMode}' cannot be lower than parent value '{parent.RepositoryCacheMode}'.", nameof(repositoryCacheMode));
// cannot specify a dispatcher!
if (_eventDispatcher != null)
throw new ArgumentException("Value cannot be specified on nested scope.", nameof(eventDispatcher));
// cannot specify a different fs scope!
// can be 'true' only on outer scope (and false does not make much sense)
if (scopeFileSystems != null && parent._scopeFileSystem != scopeFileSystems)
throw new ArgumentException($"Value '{scopeFileSystems.Value}' be different from parent value '{parent._scopeFileSystem}'.", nameof(scopeFileSystems));
}
else
{
// the FS scope cannot be "on demand" like the rest, because we would need to hook into
// every scoped FS to trigger the creation of shadow FS "on demand", and that would be
// pretty pointless since if scopeFileSystems is true, we *know* we want to shadow
if (scopeFileSystems == true)
_fscope = fileSystems.Shadow();
}
}
// initializes a new scope
public Scope(ScopeProvider scopeProvider,
ILogger logger, FileSystems fileSystems, bool detachable, ScopeContext scopeContext,
IsolationLevel isolationLevel = IsolationLevel.Unspecified,
RepositoryCacheMode repositoryCacheMode = RepositoryCacheMode.Unspecified,
IEventDispatcher eventDispatcher = null,
bool? scopeFileSystems = null,
bool callContext = false,
bool autoComplete = false)
: this(scopeProvider, logger, fileSystems, null, scopeContext, detachable, isolationLevel, repositoryCacheMode, eventDispatcher, scopeFileSystems, callContext, autoComplete)
{ }
// initializes a new scope in a nested scopes chain, with its parent
public Scope(ScopeProvider scopeProvider,
ILogger logger, FileSystems fileSystems, Scope parent,
IsolationLevel isolationLevel = IsolationLevel.Unspecified,
RepositoryCacheMode repositoryCacheMode = RepositoryCacheMode.Unspecified,
IEventDispatcher eventDispatcher = null,
bool? scopeFileSystems = null,
bool callContext = false,
bool autoComplete = false)
: this(scopeProvider, logger, fileSystems, parent, null, false, isolationLevel, repositoryCacheMode, eventDispatcher, scopeFileSystems, callContext, autoComplete)
{ }
public Guid InstanceId { get; } = Guid.NewGuid();
public ISqlContext SqlContext => _scopeProvider.SqlContext;
// a value indicating whether to force call-context
public bool CallContext
{
get
{
if (_callContext) return true;
if (ParentScope != null) return ParentScope.CallContext;
return false;
}
set => _callContext = value;
}
public bool ScopedFileSystems
{
get
{
if (ParentScope != null) return ParentScope.ScopedFileSystems;
return _fscope != null;
}
}
/// <inheritdoc />
public RepositoryCacheMode RepositoryCacheMode
{
get
{
if (_repositoryCacheMode != RepositoryCacheMode.Unspecified) return _repositoryCacheMode;
if (ParentScope != null) return ParentScope.RepositoryCacheMode;
return RepositoryCacheMode.Default;
}
}
/// <inheritdoc />
public IsolatedCaches IsolatedCaches
{
get
{
if (ParentScope != null) return ParentScope.IsolatedCaches;
return _isolatedCaches ?? (_isolatedCaches
= new IsolatedCaches(type => new DeepCloneAppCache(new ObjectCacheAppCache())));
}
}
// a value indicating whether the scope is detachable
// ie whether it was created by CreateDetachedScope
public bool Detachable { get; }
// the parent scope (in a nested scopes chain)
public Scope ParentScope { get; set; }
public bool Attached { get; set; }
// the original scope (when attaching a detachable scope)
public Scope OrigScope { get; set; }
// the original context (when attaching a detachable scope)
public ScopeContext OrigContext { get; set; }
// the context (for attaching & detaching only)
public ScopeContext Context { get; }
public IsolationLevel IsolationLevel
{
get
{
if (_isolationLevel != IsolationLevel.Unspecified) return _isolationLevel;
if (ParentScope != null) return ParentScope.IsolationLevel;
return Database.SqlContext.SqlSyntax.DefaultIsolationLevel;
}
}
/// <inheritdoc />
public IUmbracoDatabase Database
{
get
{
EnsureNotDisposed();
if (_database != null)
return _database;
if (ParentScope != null)
{
var database = ParentScope.Database;
var currentLevel = database.GetCurrentTransactionIsolationLevel();
if (_isolationLevel > IsolationLevel.Unspecified && currentLevel < _isolationLevel)
throw new Exception("Scope requires isolation level " + _isolationLevel + ", but got " + currentLevel + " from parent.");
return _database = database;
}
// create a new database
_database = _scopeProvider.DatabaseFactory.CreateDatabase();
// enter a transaction, as a scope implies a transaction, always
try
{
_database.BeginTransaction(IsolationLevel);
return _database;
}
catch
{
_database.Dispose();
_database = null;
throw;
}
}
}
public IUmbracoDatabase DatabaseOrNull
{
get
{
EnsureNotDisposed();
return ParentScope == null ? _database : ParentScope.DatabaseOrNull;
}
}
/// <inheritdoc />
public EventMessages Messages
{
get
{
EnsureNotDisposed();
if (ParentScope != null) return ParentScope.Messages;
return _messages ?? (_messages = new EventMessages());
// TODO: event messages?
// this may be a problem: the messages collection will be cleared at the end of the scope
// how shall we process it in controllers etc? if we don't want the global factory from v7?
// it'd need to be captured by the controller
//
// + rename // EventMessages = ServiceMessages or something
}
}
public EventMessages MessagesOrNull
{
get
{
EnsureNotDisposed();
return ParentScope == null ? _messages : ParentScope.MessagesOrNull;
}
}
/// <inheritdoc />
public IEventDispatcher Events
{
get
{
EnsureNotDisposed();
if (ParentScope != null) return ParentScope.Events;
return _eventDispatcher ?? (_eventDispatcher = new QueuingEventDispatcher());
}
}
/// <inheritdoc />
public bool Complete()
{
if (_completed.HasValue == false)
_completed = true;
return _completed.Value;
}
public void Reset()
{
_completed = null;
}
public void ChildCompleted(bool? completed)
{
// if child did not complete we cannot complete
if (completed.HasValue == false || completed.Value == false)
{
if (LogUncompletedScopes)
_logger.Debug<Scope>("Uncompleted Child Scope at\r\n {StackTrace}", Environment.StackTrace);
_completed = false;
}
}
private void EnsureNotDisposed()
{
if (_disposed)
throw new ObjectDisposedException(GetType().FullName);
// TODO: safer?
//if (Interlocked.CompareExchange(ref _disposed, 1, 0) != 0)
// throw new ObjectDisposedException(GetType().FullName);
}
public void Dispose()
{
EnsureNotDisposed();
if (this != _scopeProvider.AmbientScope)
{
#if DEBUG_SCOPES
var ambient = _scopeProvider.AmbientScope;
_logger.Debug<Scope>("Dispose error (" + (ambient == null ? "no" : "other") + " ambient)");
if (ambient == null)
throw new InvalidOperationException("Not the ambient scope (no ambient scope).");
var ambientInfos = _scopeProvider.GetScopeInfo(ambient);
var disposeInfos = _scopeProvider.GetScopeInfo(this);
throw new InvalidOperationException("Not the ambient scope (see ctor stack traces).\r\n"
+ "- ambient ctor ->\r\n" + ambientInfos.CtorStack + "\r\n"
+ "- dispose ctor ->\r\n" + disposeInfos.CtorStack + "\r\n");
#else
throw new InvalidOperationException("Not the ambient scope.");
#endif
}
var parent = ParentScope;
_scopeProvider.AmbientScope = parent; // might be null = this is how scopes are removed from context objects
#if DEBUG_SCOPES
_scopeProvider.Disposed(this);
#endif
if (_autoComplete && _completed == null)
_completed = true;
if (parent != null)
parent.ChildCompleted(_completed);
else
DisposeLastScope();
_disposed = true;
GC.SuppressFinalize(this);
}
private void DisposeLastScope()
{
// figure out completed
var completed = _completed.HasValue && _completed.Value;
// deal with database
var databaseException = false;
if (_database != null)
{
try
{
if (completed)
_database.CompleteTransaction();
else
_database.AbortTransaction();
}
catch
{
databaseException = true;
throw;
}
finally
{
_database.Dispose();
_database = null;
if (databaseException)
RobustExit(false, true);
}
}
RobustExit(completed, false);
}
// this chains some try/finally blocks to
// - complete and dispose the scoped filesystems
// - deal with events if appropriate
// - remove the scope context if it belongs to this scope
// - deal with detachable scopes
// here,
// - completed indicates whether the scope has been completed
// can be true or false, but in both cases the scope is exiting
// in a normal way
// - onException indicates whether completing/aborting the database
// transaction threw an exception, in which case 'completed' has
// to be false + events don't trigger and we just to some cleanup
// to ensure we don't leave a scope around, etc
private void RobustExit(bool completed, bool onException)
{
if (onException) completed = false;
TryFinally(() =>
{
if (_scopeFileSystem == true)
{
if (completed)
_fscope.Complete();
_fscope.Dispose();
_fscope = null;
}
}, () =>
{
// deal with events
if (onException == false)
_eventDispatcher?.ScopeExit(completed);
}, () =>
{
// if *we* created it, then get rid of it
if (_scopeProvider.AmbientContext == Context)
{
try
{
_scopeProvider.AmbientContext.ScopeExit(completed);
}
finally
{
// removes the ambient context (ambient scope already gone)
_scopeProvider.SetAmbient(null);
}
}
}, () =>
{
if (Detachable)
{
// get out of the way, restore original
_scopeProvider.SetAmbient(OrigScope, OrigContext);
Attached = false;
OrigScope = null;
OrigContext = null;
}
});
}
private static void TryFinally(params Action[] actions)
{
TryFinally(0, actions);
}
private static void TryFinally(int index, Action[] actions)
{
if (index == actions.Length) return;
try
{
actions[index]();
}
finally
{
TryFinally(index + 1, actions);
}
}
// backing field for LogUncompletedScopes
private static bool? _logUncompletedScopes;
// caching config
// true if Umbraco.CoreDebug.LogUncompletedScope appSetting is set to "true"
private static bool LogUncompletedScopes => (_logUncompletedScopes
?? (_logUncompletedScopes = Current.Configs.CoreDebug().LogUncompletedScopes)).Value;
/// <inheritdoc />
public void ReadLock(params int[] lockIds) => Database.SqlContext.SqlSyntax.ReadLock(Database, lockIds);
/// <inheritdoc />
public void WriteLock(params int[] lockIds) => Database.SqlContext.SqlSyntax.WriteLock(Database, lockIds);
}
}