diff --git a/src/Umbraco.Core/Persistence/FaultHandling/Strategies/SqlAzureTransientErrorDetectionStrategy.cs b/src/Umbraco.Core/Persistence/FaultHandling/Strategies/SqlAzureTransientErrorDetectionStrategy.cs index 849fd35fad..f763594616 100644 --- a/src/Umbraco.Core/Persistence/FaultHandling/Strategies/SqlAzureTransientErrorDetectionStrategy.cs +++ b/src/Umbraco.Core/Persistence/FaultHandling/Strategies/SqlAzureTransientErrorDetectionStrategy.cs @@ -4,6 +4,10 @@ using System.Data.SqlClient; namespace Umbraco.Core.Persistence.FaultHandling.Strategies { + // See https://docs.microsoft.com/en-us/azure/azure-sql/database/troubleshoot-common-connectivity-issues + // Also we could just use the nuget package instead https://www.nuget.org/packages/EnterpriseLibrary.TransientFaultHandling/ ? + // but i guess that's not netcore so we'll just leave it. + /// /// Provides the transient error detection logic for transient faults that are specific to SQL Azure. /// @@ -71,7 +75,7 @@ namespace Umbraco.Core.Persistence.FaultHandling.Strategies /// Determines whether the specified exception represents a transient failure that can be compensated by a retry. /// /// The exception object to be verified. - /// True if the specified exception is considered as transient, otherwise false. + /// true if the specified exception is considered as transient; otherwise, false. public bool IsTransient(Exception ex) { if (ex != null) @@ -97,40 +101,50 @@ namespace Umbraco.Core.Persistence.FaultHandling.Strategies return true; - // SQL Error Code: 40197 - // The service has encountered an error processing your request. Please try again. - case 40197: + // SQL Error Code: 10928 + // Resource ID: %d. The %s limit for the database is %d and has been reached. + case 10928: + // SQL Error Code: 10929 + // Resource ID: %d. The %s minimum guarantee is %d, maximum limit is %d and the current usage for the database is %d. + // However, the server is currently too busy to support requests greater than %d for this database. + case 10929: // SQL Error Code: 10053 // A transport-level error has occurred when receiving results from the server. // An established connection was aborted by the software in your host machine. case 10053: // SQL Error Code: 10054 - // A transport-level error has occurred when sending the request to the server. + // A transport-level error has occurred when sending the request to the server. // (provider: TCP Provider, error: 0 - An existing connection was forcibly closed by the remote host.) case 10054: // SQL Error Code: 10060 - // A network-related or instance-specific error occurred while establishing a connection to SQL Server. - // The server was not found or was not accessible. Verify that the instance name is correct and that SQL Server - // is configured to allow remote connections. (provider: TCP Provider, error: 0 - A connection attempt failed - // because the connected party did not properly respond after a period of time, or established connection failed + // A network-related or instance-specific error occurred while establishing a connection to SQL Server. + // The server was not found or was not accessible. Verify that the instance name is correct and that SQL Server + // is configured to allow remote connections. (provider: TCP Provider, error: 0 - A connection attempt failed + // because the connected party did not properly respond after a period of time, or established connection failed // because connected host has failed to respond.)"} case 10060: + // SQL Error Code: 40197 + // The service has encountered an error processing your request. Please try again. + case 40197: + // SQL Error Code: 40540 + // The service has encountered an error processing your request. Please try again. + case 40540: // SQL Error Code: 40613 - // Database XXXX on server YYYY is not currently available. Please retry the connection later. If the problem persists, contact customer + // Database XXXX on server YYYY is not currently available. Please retry the connection later. If the problem persists, contact customer // support, and provide them the session tracing ID of ZZZZZ. case 40613: // SQL Error Code: 40143 // The service has encountered an error processing your request. Please try again. case 40143: // SQL Error Code: 233 - // The client was unable to establish a connection because of an error during connection initialization process before login. - // Possible causes include the following: the client tried to connect to an unsupported version of SQL Server; the server was too busy - // to accept new connections; or there was a resource limitation (insufficient memory or maximum allowed connections) on the server. + // The client was unable to establish a connection because of an error during connection initialization process before login. + // Possible causes include the following: the client tried to connect to an unsupported version of SQL Server; the server was too busy + // to accept new connections; or there was a resource limitation (insufficient memory or maximum allowed connections) on the server. // (provider: TCP Provider, error: 0 - An existing connection was forcibly closed by the remote host.) case 233: // SQL Error Code: 64 - // A connection was successfully established with the server, but then an error occurred during the login process. - // (provider: TCP Provider, error: 0 - The specified network name is no longer available.) + // A connection was successfully established with the server, but then an error occurred during the login process. + // (provider: TCP Provider, error: 0 - The specified network name is no longer available.) case 64: // DBNETLIB Error Code: 20 // The instance of SQL Server you attempted to connect to does not support encryption. diff --git a/src/Umbraco.Core/Persistence/Repositories/IDocumentRepository.cs b/src/Umbraco.Core/Persistence/Repositories/IDocumentRepository.cs index fc5382499f..0971b2047a 100644 --- a/src/Umbraco.Core/Persistence/Repositories/IDocumentRepository.cs +++ b/src/Umbraco.Core/Persistence/Repositories/IDocumentRepository.cs @@ -12,6 +12,11 @@ namespace Umbraco.Core.Persistence.Repositories /// void ClearSchedule(DateTime date); + void ClearSchedule(DateTime date, ContentScheduleAction action); + + bool HasContentForExpiration(DateTime date); + bool HasContentForRelease(DateTime date); + /// /// Gets objects having an expiration date before (lower than, or equal to) a specified date. /// diff --git a/src/Umbraco.Core/Persistence/Repositories/Implement/DocumentRepository.cs b/src/Umbraco.Core/Persistence/Repositories/Implement/DocumentRepository.cs index ccfa8209fb..a34aadd70f 100644 --- a/src/Umbraco.Core/Persistence/Repositories/Implement/DocumentRepository.cs +++ b/src/Umbraco.Core/Persistence/Repositories/Implement/DocumentRepository.cs @@ -1000,6 +1000,37 @@ namespace Umbraco.Core.Persistence.Repositories.Implement Database.Execute(sql); } + /// + public void ClearSchedule(DateTime date, ContentScheduleAction action) + { + var a = action.ToString(); + var sql = Sql().Delete().Where(x => x.Date <= date && x.Action == a); + Database.Execute(sql); + } + + private Sql GetSqlForHasScheduling(ContentScheduleAction action, DateTime date) + { + var template = SqlContext.Templates.Get("Umbraco.Core.DocumentRepository.GetSqlForHasScheduling", tsql => tsql + .SelectCount() + .From() + .Where(x => x.Action == SqlTemplate.Arg("action") && x.Date <= SqlTemplate.Arg("date"))); + + var sql = template.Sql(action.ToString(), date); + return sql; + } + + public bool HasContentForExpiration(DateTime date) + { + var sql = GetSqlForHasScheduling(ContentScheduleAction.Expire, date); + return Database.ExecuteScalar(sql) > 0; + } + + public bool HasContentForRelease(DateTime date) + { + var sql = GetSqlForHasScheduling(ContentScheduleAction.Release, date); + return Database.ExecuteScalar(sql) > 0; + } + /// public IEnumerable GetContentForRelease(DateTime date) { diff --git a/src/Umbraco.Core/Runtime/SqlMainDomLock.cs b/src/Umbraco.Core/Runtime/SqlMainDomLock.cs index 5f5d0d607f..209b99aef1 100644 --- a/src/Umbraco.Core/Runtime/SqlMainDomLock.cs +++ b/src/Umbraco.Core/Runtime/SqlMainDomLock.cs @@ -6,7 +6,6 @@ using System.Linq; using System.Security.Cryptography; using System.Threading; using System.Threading.Tasks; -using System.Web; using Umbraco.Core.Logging; using Umbraco.Core.Persistence; using Umbraco.Core.Persistence.Dtos; @@ -21,12 +20,11 @@ namespace Umbraco.Core.Runtime private const string MainDomKeyPrefix = "Umbraco.Core.Runtime.SqlMainDom"; private const string UpdatedSuffix = "_updated"; private readonly ILogger _logger; - private IUmbracoDatabase _db; private CancellationTokenSource _cancellationTokenSource = new CancellationTokenSource(); private SqlServerSyntaxProvider _sqlServerSyntax = new SqlServerSyntaxProvider(); private bool _mainDomChanging = false; private readonly UmbracoDatabaseFactory _dbFactory; - private bool _hasError; + private bool _errorDuringAcquiring; private object _locker = new object(); public SqlMainDomLock(ILogger logger) @@ -56,25 +54,24 @@ namespace Umbraco.Core.Runtime _logger.Debug("Acquiring lock..."); - var db = GetDatabase(); - var tempId = Guid.NewGuid().ToString(); + using var db = _dbFactory.CreateDatabase(); + using var transaction = db.GetTransaction(IsolationLevel.ReadCommitted); + try { - db.BeginTransaction(IsolationLevel.ReadCommitted); - try { // wait to get a write lock _sqlServerSyntax.WriteLock(db, TimeSpan.FromMilliseconds(millisecondsTimeout), Constants.Locks.MainDom); } - catch (Exception ex) + catch(SqlException ex) { if (IsLockTimeoutException(ex)) { _logger.Error(ex, "Sql timeout occurred, could not acquire MainDom."); - _hasError = true; + _errorDuringAcquiring = true; return false; } @@ -82,15 +79,12 @@ namespace Umbraco.Core.Runtime throw; } - var result = InsertLockRecord(tempId); //we change the row to a random Id to signal other MainDom to shutdown + var result = InsertLockRecord(tempId, db); //we change the row to a random Id to signal other MainDom to shutdown if (result == RecordPersistenceType.Insert) { // if we've inserted, then there was no MainDom so we can instantly acquire - // TODO: see the other TODO, could we just delete the row and that would indicate that we - // are MainDom? then we don't leave any orphan rows behind. - - InsertLockRecord(_lockId); // so update with our appdomain id + InsertLockRecord(_lockId, db); // so update with our appdomain id _logger.Debug("Acquired with ID {LockId}", _lockId); return true; } @@ -100,23 +94,23 @@ namespace Umbraco.Core.Runtime } catch (Exception ex) { - ResetDatabase(); // unexpected _logger.Error(ex, "Unexpected error, cannot acquire MainDom"); - _hasError = true; + _errorDuringAcquiring = true; return false; } finally { - db?.CompleteTransaction(); + transaction.Complete(); } + return await WaitForExistingAsync(tempId, millisecondsTimeout); } public Task ListenAsync() { - if (_hasError) + if (_errorDuringAcquiring) { _logger.Warn("Could not acquire MainDom, listening is canceled."); return Task.CompletedTask; @@ -142,8 +136,9 @@ namespace Umbraco.Core.Runtime { while (true) { - // poll every 1 second - Thread.Sleep(1000); + // poll every couple of seconds + // local testing shows the actual query to be executed from client/server is approx 300ms but would change depending on environment/IO + Thread.Sleep(2000); if (!_dbFactory.Configured) { @@ -160,20 +155,14 @@ namespace Umbraco.Core.Runtime if (_cancellationTokenSource.IsCancellationRequested) return; - var db = GetDatabase(); - + using var db = _dbFactory.CreateDatabase(); + using var transaction = db.GetTransaction(IsolationLevel.ReadCommitted); try { - db.BeginTransaction(IsolationLevel.ReadCommitted); - // get a read lock _sqlServerSyntax.ReadLock(db, Constants.Locks.MainDom); - // TODO: We could in theory just check if the main dom row doesn't exist, that could indicate that - // we are still the maindom. An empty value might be better because then we won't have any orphan rows - // if the app is terminated. Could that work? - - if (!IsMainDomValue(_lockId)) + if (!IsMainDomValue(_lockId, db)) { // we are no longer main dom, another one has come online, exit _mainDomChanging = true; @@ -183,38 +172,23 @@ namespace Umbraco.Core.Runtime } catch (Exception ex) { - ResetDatabase(); - // unexpected - _logger.Error(ex, "Unexpected error, listening is canceled."); - _hasError = true; - return; + _logger.Error(ex, "Unexpected error during listening."); + + // We need to keep on listening unless we've been notified by our own AppDomain to shutdown since + // we don't want to shutdown resources controlled by MainDom inadvertently. We'll just keep listening otherwise. + if (_cancellationTokenSource.IsCancellationRequested) + return; + } finally { - db?.CompleteTransaction(); + transaction.Complete(); } } } } - private void ResetDatabase() - { - if (_db.InTransaction) - _db.AbortTransaction(); - _db.Dispose(); - _db = null; - } - - private IUmbracoDatabase GetDatabase() - { - if (_db != null) - return _db; - - _db = _dbFactory.CreateDatabase(); - return _db; - } - /// /// Wait for any existing MainDom to release so we can continue booting /// @@ -227,121 +201,131 @@ namespace Umbraco.Core.Runtime return Task.Run(() => { - var db = GetDatabase(); + using var db = _dbFactory.CreateDatabase(); + var watch = new Stopwatch(); watch.Start(); - while(true) + while (true) { // poll very often, we need to take over as fast as we can - Thread.Sleep(100); + // local testing shows the actual query to be executed from client/server is approx 300ms but would change depending on environment/IO + Thread.Sleep(1000); - try - { - db.BeginTransaction(IsolationLevel.ReadCommitted); - - // get a read lock - _sqlServerSyntax.ReadLock(db, Constants.Locks.MainDom); - - // the row - var mainDomRows = db.Fetch("SELECT * FROM umbracoKeyValue WHERE [key] = @key", new { key = MainDomKey }); - - if (mainDomRows.Count == 0 || mainDomRows[0].Value == updatedTempId) - { - // the other main dom has updated our record - // Or the other maindom shutdown super fast and just deleted the record - // which indicates that we - // can acquire it and it has shutdown. - - _sqlServerSyntax.WriteLock(db, Constants.Locks.MainDom); - - // so now we update the row with our appdomain id - InsertLockRecord(_lockId); - _logger.Debug("Acquired with ID {LockId}", _lockId); - return true; - } - else if (mainDomRows.Count == 1 && !mainDomRows[0].Value.StartsWith(tempId)) - { - // in this case, the prefixed ID is different which means - // another new AppDomain has come online and is wanting to take over. In that case, we will not - // acquire. - - _logger.Debug("Cannot acquire, another booting application detected."); - - return false; - } - } - catch (Exception ex) - { - ResetDatabase(); - - if (IsLockTimeoutException(ex)) - { - _logger.Error(ex, "Sql timeout occurred, waiting for existing MainDom is canceled."); - _hasError = true; - return false; - } - // unexpected - _logger.Error(ex, "Unexpected error, waiting for existing MainDom is canceled."); - _hasError = true; - return false; - } - finally - { - db?.CompleteTransaction(); - } + var acquired = TryAcquire(db, tempId, updatedTempId); + if (acquired.HasValue) + return acquired.Value; if (watch.ElapsedMilliseconds >= millisecondsTimeout) { - // if the timeout has elapsed, it either means that the other main dom is taking too long to shutdown, - // or it could mean that the previous appdomain was terminated and didn't clear out the main dom SQL row - // and it's just been left as an orphan row. - // There's really know way of knowing unless we are constantly updating the row for the current maindom - // which isn't ideal. - // So... we're going to 'just' take over, if the writelock works then we'll assume we're ok - - _logger.Debug("Timeout elapsed, assuming orphan row, acquiring MainDom."); - - try - { - db.BeginTransaction(IsolationLevel.ReadCommitted); - - _sqlServerSyntax.WriteLock(db, Constants.Locks.MainDom); - - // so now we update the row with our appdomain id - InsertLockRecord(_lockId); - _logger.Debug("Acquired with ID {LockId}", _lockId); - return true; - } - catch (Exception ex) - { - ResetDatabase(); - - if (IsLockTimeoutException(ex)) - { - // something is wrong, we cannot acquire, not much we can do - _logger.Error(ex, "Sql timeout occurred, could not forcibly acquire MainDom."); - _hasError = true; - return false; - } - _logger.Error(ex, "Unexpected error, could not forcibly acquire MainDom."); - _hasError = true; - return false; - } - finally - { - db?.CompleteTransaction(); - } + return AcquireWhenMaxWaitTimeElapsed(db); } } }, _cancellationTokenSource.Token); } + private bool? TryAcquire(IUmbracoDatabase db, string tempId, string updatedTempId) + { + using var transaction = db.GetTransaction(IsolationLevel.ReadCommitted); + + try + { + // get a read lock + _sqlServerSyntax.ReadLock(db, Constants.Locks.MainDom); + + // the row + var mainDomRows = db.Fetch("SELECT * FROM umbracoKeyValue WHERE [key] = @key", new { key = MainDomKey }); + + if (mainDomRows.Count == 0 || mainDomRows[0].Value == updatedTempId) + { + // the other main dom has updated our record + // Or the other maindom shutdown super fast and just deleted the record + // which indicates that we + // can acquire it and it has shutdown. + + _sqlServerSyntax.WriteLock(db, Constants.Locks.MainDom); + + // so now we update the row with our appdomain id + InsertLockRecord(_lockId, db); + _logger.Debug("Acquired with ID {LockId}", _lockId); + return true; + } + else if (mainDomRows.Count == 1 && !mainDomRows[0].Value.StartsWith(tempId)) + { + // in this case, the prefixed ID is different which means + // another new AppDomain has come online and is wanting to take over. In that case, we will not + // acquire. + + _logger.Debug("Cannot acquire, another booting application detected."); + return false; + } + } + catch (Exception ex) + { + if (IsLockTimeoutException(ex as SqlException)) + { + _logger.Error(ex, "Sql timeout occurred, waiting for existing MainDom is canceled."); + _errorDuringAcquiring = true; + return false; + } + // unexpected + _logger.Error(ex, "Unexpected error, waiting for existing MainDom is canceled."); + _errorDuringAcquiring = true; + return false; + } + finally + { + transaction.Complete(); + } + + return null; // continue + } + + private bool AcquireWhenMaxWaitTimeElapsed(IUmbracoDatabase db) + { + // if the timeout has elapsed, it either means that the other main dom is taking too long to shutdown, + // or it could mean that the previous appdomain was terminated and didn't clear out the main dom SQL row + // and it's just been left as an orphan row. + // There's really know way of knowing unless we are constantly updating the row for the current maindom + // which isn't ideal. + // So... we're going to 'just' take over, if the writelock works then we'll assume we're ok + + _logger.Debug("Timeout elapsed, assuming orphan row, acquiring MainDom."); + + using var transaction = db.GetTransaction(IsolationLevel.ReadCommitted); + + try + { + _sqlServerSyntax.WriteLock(db, Constants.Locks.MainDom); + + // so now we update the row with our appdomain id + InsertLockRecord(_lockId, db); + _logger.Debug("Acquired with ID {LockId}", _lockId); + return true; + } + catch (Exception ex) + { + if (IsLockTimeoutException(ex as SqlException)) + { + // something is wrong, we cannot acquire, not much we can do + _logger.Error(ex, "Sql timeout occurred, could not forcibly acquire MainDom."); + _errorDuringAcquiring = true; + return false; + } + _logger.Error(ex, "Unexpected error, could not forcibly acquire MainDom."); + _errorDuringAcquiring = true; + return false; + } + finally + { + transaction.Complete(); + } + } + /// /// Inserts or updates the key/value row /// - private RecordPersistenceType InsertLockRecord(string id) + private RecordPersistenceType InsertLockRecord(string id, IUmbracoDatabase db) { - var db = GetDatabase(); return db.InsertOrUpdate(new KeyValueDto { Key = MainDomKey, @@ -354,9 +338,8 @@ namespace Umbraco.Core.Runtime /// Checks if the DB row value is equals the value /// /// - private bool IsMainDomValue(string val) + private bool IsMainDomValue(string val, IUmbracoDatabase db) { - var db = GetDatabase(); return db.ExecuteScalar("SELECT COUNT(*) FROM umbracoKeyValue WHERE [key] = @key AND [value] = @val", new { key = MainDomKey, val = val }) == 1; } @@ -366,7 +349,7 @@ namespace Umbraco.Core.Runtime /// /// /// - private bool IsLockTimeoutException(Exception exception) => exception is SqlException sqlException && sqlException.Number == 1222; + private bool IsLockTimeoutException(SqlException sqlException) => sqlException?.Number == 1222; #region IDisposable Support private bool _disposedValue = false; // To detect redundant calls @@ -385,11 +368,11 @@ namespace Umbraco.Core.Runtime if (_dbFactory.Configured) { - var db = GetDatabase(); + using var db = _dbFactory.CreateDatabase(); + using var transaction = db.GetTransaction(IsolationLevel.ReadCommitted); + try { - db.BeginTransaction(IsolationLevel.ReadCommitted); - // get a write lock _sqlServerSyntax.WriteLock(db, Constants.Locks.MainDom); @@ -402,24 +385,21 @@ namespace Umbraco.Core.Runtime if (_mainDomChanging) { _logger.Debug("Releasing MainDom, updating row, new application is booting."); - db.Execute($"UPDATE umbracoKeyValue SET [value] = [value] + '{UpdatedSuffix}' WHERE [key] = @key", new { key = MainDomKey }); + var count = db.Execute($"UPDATE umbracoKeyValue SET [value] = [value] + '{UpdatedSuffix}' WHERE [key] = @key", new { key = MainDomKey }); } else { _logger.Debug("Releasing MainDom, deleting row, application is shutting down."); - db.Execute("DELETE FROM umbracoKeyValue WHERE [key] = @key", new { key = MainDomKey }); + var count = db.Execute("DELETE FROM umbracoKeyValue WHERE [key] = @key", new { key = MainDomKey }); } } catch (Exception ex) { - ResetDatabase(); _logger.Error(ex, "Unexpected error during dipsose."); - _hasError = true; } finally { - db?.CompleteTransaction(); - ResetDatabase(); + transaction.Complete(); } } } diff --git a/src/Umbraco.Core/Services/Implement/ContentService.cs b/src/Umbraco.Core/Services/Implement/ContentService.cs index 1558b0170b..f8b63a2c40 100644 --- a/src/Umbraco.Core/Services/Implement/ContentService.cs +++ b/src/Umbraco.Core/Services/Implement/ContentService.cs @@ -30,7 +30,7 @@ namespace Umbraco.Core.Services.Implement private IQuery _queryNotTrashed; //TODO: The non-lazy object should be injected private readonly Lazy _propertyValidationService = new Lazy(() => new PropertyValidationService()); - + #region Constructors @@ -875,7 +875,7 @@ namespace Umbraco.Core.Services.Implement throw new NotSupportedException($"Culture \"{culture}\" is not supported by invariant content types."); } - if(content.Name != null && content.Name.Length > 255) + if (content.Name != null && content.Name.Length > 255) { throw new InvalidOperationException("Name cannot be more than 255 characters in length."); } @@ -1243,7 +1243,7 @@ namespace Umbraco.Core.Services.Implement if (culturesUnpublishing != null) { // This will mean that that we unpublished a mandatory culture or we unpublished the last culture. - + var langs = string.Join(", ", allLangs .Where(x => culturesUnpublishing.InvariantContains(x.IsoCode)) .Select(x => x.CultureName)); @@ -1252,7 +1252,7 @@ namespace Umbraco.Core.Services.Implement if (publishResult == null) throw new PanicException("publishResult == null - should not happen"); - switch(publishResult.Result) + switch (publishResult.Result) { case PublishResultType.FailedPublishMandatoryCultureMissing: //occurs when a mandatory culture was unpublished (which means we tried publishing the document without a mandatory culture) @@ -1266,7 +1266,7 @@ namespace Umbraco.Core.Services.Implement Audit(AuditType.Unpublish, userId, content.Id, "Unpublished (last language unpublished)"); return new PublishResult(PublishResultType.SuccessUnpublishLastCulture, evtMsgs, content); } - + } Audit(AuditType.Unpublish, userId, content.Id); @@ -1286,7 +1286,7 @@ namespace Umbraco.Core.Services.Implement changeType = TreeChangeTypes.RefreshBranch; // whole branch else if (isNew == false && previouslyPublished) changeType = TreeChangeTypes.RefreshNode; // single node - + // invalidate the node/branch if (!branchOne) // for branches, handled by SaveAndPublishBranch @@ -1360,24 +1360,90 @@ namespace Umbraco.Core.Services.Implement /// public IEnumerable PerformScheduledPublish(DateTime date) - => PerformScheduledPublishInternal(date).ToList(); - - // beware! this method yields results, so the returned IEnumerable *must* be - // enumerated for anything to happen - dangerous, so private + exposed via - // the public method above, which forces ToList(). - private IEnumerable PerformScheduledPublishInternal(DateTime date) { + var allLangs = new Lazy>(() => _languageRepository.GetMany().ToList()); var evtMsgs = EventMessagesFactory.Get(); + var results = new List(); - using (var scope = ScopeProvider.CreateScope()) + PerformScheduledPublishingRelease(date, results, evtMsgs, allLangs); + PerformScheduledPublishingExpiration(date, results, evtMsgs, allLangs); + + return results; + } + + private void PerformScheduledPublishingExpiration(DateTime date, List results, EventMessages evtMsgs, Lazy> allLangs) + { + using var scope = ScopeProvider.CreateScope(); + + // do a fast read without any locks since this executes often to see if we even need to proceed + if (_documentRepository.HasContentForExpiration(date)) { + // now take a write lock since we'll be updating scope.WriteLock(Constants.Locks.ContentTree); - var allLangs = _languageRepository.GetMany().ToList(); + foreach (var d in _documentRepository.GetContentForExpiration(date)) + { + if (d.ContentType.VariesByCulture()) + { + //find which cultures have pending schedules + var pendingCultures = d.ContentSchedule.GetPending(ContentScheduleAction.Expire, date) + .Select(x => x.Culture) + .Distinct() + .ToList(); + + if (pendingCultures.Count == 0) + continue; //shouldn't happen but no point in processing this document if there's nothing there + + var saveEventArgs = new ContentSavingEventArgs(d, evtMsgs); + if (scope.Events.DispatchCancelable(Saving, this, saveEventArgs, nameof(Saving))) + { + results.Add(new PublishResult(PublishResultType.FailedPublishCancelledByEvent, evtMsgs, d)); + continue; + } + + foreach (var c in pendingCultures) + { + //Clear this schedule for this culture + d.ContentSchedule.Clear(c, ContentScheduleAction.Expire, date); + //set the culture to be published + d.UnpublishCulture(c); + } + + var result = CommitDocumentChangesInternal(scope, d, saveEventArgs, allLangs.Value, d.WriterId); + if (result.Success == false) + Logger.Error(null, "Failed to publish document id={DocumentId}, reason={Reason}.", d.Id, result.Result); + results.Add(result); + + } + else + { + //Clear this schedule + d.ContentSchedule.Clear(ContentScheduleAction.Expire, date); + var result = Unpublish(d, userId: d.WriterId); + if (result.Success == false) + Logger.Error(null, "Failed to unpublish document id={DocumentId}, reason={Reason}.", d.Id, result.Result); + results.Add(result); + } + } + + _documentRepository.ClearSchedule(date, ContentScheduleAction.Expire); + } + + scope.Complete(); + } + + private void PerformScheduledPublishingRelease(DateTime date, List results, EventMessages evtMsgs, Lazy> allLangs) + { + using var scope = ScopeProvider.CreateScope(); + + // do a fast read without any locks since this executes often to see if we even need to proceed + if (_documentRepository.HasContentForRelease(date)) + { + // now take a write lock since we'll be updating + scope.WriteLock(Constants.Locks.ContentTree); foreach (var d in _documentRepository.GetContentForRelease(date)) { - PublishResult result; if (d.ContentType.VariesByCulture()) { //find which cultures have pending schedules @@ -1387,11 +1453,14 @@ namespace Umbraco.Core.Services.Implement .ToList(); if (pendingCultures.Count == 0) - break; //shouldn't happen but no point in continuing if there's nothing there + continue; //shouldn't happen but no point in processing this document if there's nothing there var saveEventArgs = new ContentSavingEventArgs(d, evtMsgs); if (scope.Events.DispatchCancelable(Saving, this, saveEventArgs, nameof(Saving))) - yield return new PublishResult(PublishResultType.FailedPublishCancelledByEvent, evtMsgs, d); + { + results.Add(new PublishResult(PublishResultType.FailedPublishCancelledByEvent, evtMsgs, d)); + continue; // this document is canceled move next + } var publishing = true; foreach (var culture in pendingCultures) @@ -1403,94 +1472,51 @@ namespace Umbraco.Core.Services.Implement //publish the culture values and validate the property values, if validation fails, log the invalid properties so the develeper has an idea of what has failed Property[] invalidProperties = null; - var impact = CultureImpact.Explicit(culture, IsDefaultCulture(allLangs, culture)); + var impact = CultureImpact.Explicit(culture, IsDefaultCulture(allLangs.Value, culture)); var tryPublish = d.PublishCulture(impact) && _propertyValidationService.Value.IsPropertyDataValid(d, out invalidProperties, impact); if (invalidProperties != null && invalidProperties.Length > 0) Logger.Warn("Scheduled publishing will fail for document {DocumentId} and culture {Culture} because of invalid properties {InvalidProperties}", d.Id, culture, string.Join(",", invalidProperties.Select(x => x.Alias))); publishing &= tryPublish; //set the culture to be published - if (!publishing) break; // no point continuing + if (!publishing) continue; // move to next document } + PublishResult result; + if (d.Trashed) result = new PublishResult(PublishResultType.FailedPublishIsTrashed, evtMsgs, d); else if (!publishing) result = new PublishResult(PublishResultType.FailedPublishContentInvalid, evtMsgs, d); else - result = CommitDocumentChangesInternal(scope, d, saveEventArgs, allLangs, d.WriterId); - + result = CommitDocumentChangesInternal(scope, d, saveEventArgs, allLangs.Value, d.WriterId); if (result.Success == false) Logger.Error(null, "Failed to publish document id={DocumentId}, reason={Reason}.", d.Id, result.Result); - yield return result; + results.Add(result); } else { //Clear this schedule d.ContentSchedule.Clear(ContentScheduleAction.Release, date); - result = d.Trashed + var result = d.Trashed ? new PublishResult(PublishResultType.FailedPublishIsTrashed, evtMsgs, d) : SaveAndPublish(d, userId: d.WriterId); if (result.Success == false) Logger.Error(null, "Failed to publish document id={DocumentId}, reason={Reason}.", d.Id, result.Result); - yield return result; + results.Add(result); } } - foreach (var d in _documentRepository.GetContentForExpiration(date)) - { - PublishResult result; - if (d.ContentType.VariesByCulture()) - { - //find which cultures have pending schedules - var pendingCultures = d.ContentSchedule.GetPending(ContentScheduleAction.Expire, date) - .Select(x => x.Culture) - .Distinct() - .ToList(); + _documentRepository.ClearSchedule(date, ContentScheduleAction.Release); - if (pendingCultures.Count == 0) - break; //shouldn't happen but no point in continuing if there's nothing there - - var saveEventArgs = new ContentSavingEventArgs(d, evtMsgs); - if (scope.Events.DispatchCancelable(Saving, this, saveEventArgs, nameof(Saving))) - yield return new PublishResult(PublishResultType.FailedPublishCancelledByEvent, evtMsgs, d); - - foreach (var c in pendingCultures) - { - //Clear this schedule for this culture - d.ContentSchedule.Clear(c, ContentScheduleAction.Expire, date); - //set the culture to be published - d.UnpublishCulture(c); - } - - result = CommitDocumentChangesInternal(scope, d, saveEventArgs, allLangs, d.WriterId); - if (result.Success == false) - Logger.Error(null, "Failed to publish document id={DocumentId}, reason={Reason}.", d.Id, result.Result); - yield return result; - - } - else - { - //Clear this schedule - d.ContentSchedule.Clear(ContentScheduleAction.Expire, date); - result = Unpublish(d, userId: d.WriterId); - if (result.Success == false) - Logger.Error(null, "Failed to unpublish document id={DocumentId}, reason={Reason}.", d.Id, result.Result); - yield return result; - } - - - } - - _documentRepository.ClearSchedule(date); - - scope.Complete(); } + + scope.Complete(); } // utility 'PublishCultures' func used by SaveAndPublishBranch @@ -2623,7 +2649,7 @@ namespace Umbraco.Core.Services.Implement // there will be nothing to publish/unpublish. return new PublishResult(PublishResultType.FailedPublishNothingToPublish, evtMsgs, content); } - + // missing mandatory culture = cannot be published var mandatoryCultures = allLangs.Where(x => x.IsMandatory).Select(x => x.IsoCode); @@ -3136,6 +3162,6 @@ namespace Umbraco.Core.Services.Implement #endregion - + } } diff --git a/src/Umbraco.Examine/IndexRebuilder.cs b/src/Umbraco.Examine/IndexRebuilder.cs index 02fb4fc2da..b14ff25c57 100644 --- a/src/Umbraco.Examine/IndexRebuilder.cs +++ b/src/Umbraco.Examine/IndexRebuilder.cs @@ -3,6 +3,8 @@ using System.Collections.Generic; using System.Linq; using System.Threading.Tasks; using Examine; +using Umbraco.Core.Composing; +using Umbraco.Core.Logging; namespace Umbraco.Examine { @@ -12,12 +14,20 @@ namespace Umbraco.Examine /// public class IndexRebuilder { + private readonly IProfilingLogger _logger; private readonly IEnumerable _populators; public IExamineManager ExamineManager { get; } + [Obsolete("Use constructor with all dependencies")] public IndexRebuilder(IExamineManager examineManager, IEnumerable populators) + : this(Current.ProfilingLogger, examineManager, populators) + { + } + + public IndexRebuilder(IProfilingLogger logger, IExamineManager examineManager, IEnumerable populators) { _populators = populators; + _logger = logger; ExamineManager = examineManager; } @@ -53,7 +63,14 @@ namespace Umbraco.Examine // run each populator over the indexes foreach(var populator in _populators) { - populator.Populate(indexes); + try + { + populator.Populate(indexes); + } + catch (Exception e) + { + _logger.Error(e, "Index populating failed for populator {Populator}", populator.GetType()); + } } } diff --git a/src/Umbraco.TestData/LoadTestController.cs b/src/Umbraco.TestData/LoadTestController.cs new file mode 100644 index 0000000000..97665dd084 --- /dev/null +++ b/src/Umbraco.TestData/LoadTestController.cs @@ -0,0 +1,371 @@ +using System; +using System.Threading; +using System.Linq; +using System.Web.Mvc; +using Umbraco.Core.Services; +using Umbraco.Core.Models; +using System.Web; +using System.Web.Hosting; +using System.Web.Routing; +using System.Diagnostics; +using Umbraco.Core.Composing; +using System.Configuration; + +// see https://github.com/Shazwazza/UmbracoScripts/tree/master/src/LoadTesting + +namespace Umbraco.TestData +{ + public class LoadTestController : Controller + { + public LoadTestController(ServiceContext serviceContext) + { + _serviceContext = serviceContext; + } + + private static readonly Random _random = new Random(); + private static readonly object _locko = new object(); + + private static volatile int _containerId = -1; + + private const string _containerAlias = "LoadTestContainer"; + private const string _contentAlias = "LoadTestContent"; + private const int _textboxDefinitionId = -88; + private const int _maxCreate = 1000; + + private static readonly string HeadHtml = @" + + LoadTest + + + +
+

LoadTest

+
" + System.Configuration.ConfigurationManager.AppSettings["umbracoConfigurationStatus"] + @"
+
+"; + + private const string FootHtml = @" +"; + + private static readonly string _containerTemplateText = @" +@inherits Umbraco.Web.Mvc.UmbracoViewPage +@{ + Layout = null; + var container = Umbraco.ContentAtRoot().OfTypes(""" + _containerAlias + @""").FirstOrDefault(); + var contents = container.Children().ToArray(); + var groups = contents.GroupBy(x => x.Value(""origin"")); + var id = contents.Length > 0 ? contents[0].Id : -1; + var wurl = Request.QueryString[""u""] == ""1""; + var missing = contents.Length > 0 && contents[contents.Length - 1].Id - contents[0].Id >= contents.Length; +} +" + HeadHtml + @" +
+@contents.Length items +
    +@foreach (var group in groups) +{ +
  • @group.Key: @group.Count()
  • +} +
+
+@foreach (var content in contents) +{ + while (content.Id > id) + { +
@id :: MISSING
+ id++; + } + if (wurl) + { +
@content.Id :: @content.Name :: @content.Url
+ } + else + { +
@content.Id :: @content.Name
+ } id++; +} +
+" + FootHtml; + private readonly ServiceContext _serviceContext; + + private ActionResult ContentHtml(string s) + { + return Content(HeadHtml + s + FootHtml); + } + + public ActionResult Index() + { + var res = EnsureInitialize(); + if (res != null) return res; + + var html = @"Welcome. You can: + +"; + + return ContentHtml(html); + } + + private ActionResult EnsureInitialize() + { + if (_containerId > 0) return null; + + lock (_locko) + { + if (_containerId > 0) return null; + + var contentTypeService = _serviceContext.ContentTypeService; + var contentType = contentTypeService.Get(_contentAlias); + if (contentType == null) + return ContentHtml("Not installed, first you must install."); + + var containerType = contentTypeService.Get(_containerAlias); + if (containerType == null) + return ContentHtml("Panic! Container type is missing."); + + var contentService = _serviceContext.ContentService; + var container = contentService.GetPagedOfType(containerType.Id, 0, 100, out _, null).FirstOrDefault(); + if (container == null) + return ContentHtml("Panic! Container is missing."); + + _containerId = container.Id; + return null; + } + } + + public ActionResult Install() + { + var dataTypeService = _serviceContext.DataTypeService; + + //var dataType = dataTypeService.GetAll(Constants.DataTypes.DefaultContentListView); + + + //if (!dict.ContainsKey("pageSize")) dict["pageSize"] = new PreValue("10"); + //dict["pageSize"].Value = "200"; + //dataTypeService.SavePreValues(dataType, dict); + + var contentTypeService = _serviceContext.ContentTypeService; + + var contentType = new ContentType(-1) + { + Alias = _contentAlias, + Name = "LoadTest Content", + Description = "Content for LoadTest", + Icon = "icon-document" + }; + var def = _serviceContext.DataTypeService.GetDataType(_textboxDefinitionId); + contentType.AddPropertyType(new PropertyType(def) + { + Name = "Origin", + Alias = "origin", + Description = "The origin of the content.", + }); + contentTypeService.Save(contentType); + + var containerTemplate = ImportTemplate(_serviceContext, + "LoadTestContainer", "LoadTestContainer", _containerTemplateText); + + var containerType = new ContentType(-1) + { + Alias = _containerAlias, + Name = "LoadTest Container", + Description = "Container for LoadTest content", + Icon = "icon-document", + AllowedAsRoot = true, + IsContainer = true + }; + containerType.AllowedContentTypes = containerType.AllowedContentTypes.Union(new[] + { + new ContentTypeSort(new Lazy(() => contentType.Id), 0, contentType.Alias), + }); + containerType.AllowedTemplates = containerType.AllowedTemplates.Union(new[] { containerTemplate }); + containerType.SetDefaultTemplate(containerTemplate); + contentTypeService.Save(containerType); + + var contentService = _serviceContext.ContentService; + var content = contentService.Create("LoadTestContainer", -1, _containerAlias); + contentService.SaveAndPublish(content); + + return ContentHtml("Installed."); + } + + public ActionResult Create(int n = 1, int r = 0, string o = null) + { + var res = EnsureInitialize(); + if (res != null) return res; + + if (r < 0) r = 0; + if (r > 100) r = 100; + var restart = GetRandom(0, 100) > (100 - r); + + var contentService = _serviceContext.ContentService; + + if (n < 1) n = 1; + if (n > _maxCreate) n = _maxCreate; + for (int i = 0; i < n; i++) + { + var name = Guid.NewGuid().ToString("N").ToUpper() + "-" + (restart ? "R" : "X") + "-" + o; + var content = contentService.Create(name, _containerId, _contentAlias); + content.SetValue("origin", o); + contentService.SaveAndPublish(content); + } + + if (restart) + DoRestart(); + + return ContentHtml("Created " + n + " content" + + (restart ? ", and restarted" : "") + + "."); + } + + private int GetRandom(int minValue, int maxValue) + { + lock (_locko) + { + return _random.Next(minValue, maxValue); + } + } + + public ActionResult Clear() + { + var res = EnsureInitialize(); + if (res != null) return res; + + var contentType = _serviceContext.ContentTypeService.Get(_contentAlias); + _serviceContext.ContentService.DeleteOfType(contentType.Id); + + return ContentHtml("Cleared."); + } + + private void DoRestart() + { + HttpContext.User = null; + System.Web.HttpContext.Current.User = null; + Thread.CurrentPrincipal = null; + HttpRuntime.UnloadAppDomain(); + } + + public ActionResult Restart() + { + DoRestart(); + + return ContentHtml("Restarted."); + } + + public ActionResult Die() + { + var timer = new System.Threading.Timer(_ => + { + throw new Exception("die!"); + }); + timer.Change(100, 0); + + return ContentHtml("Dying."); + } + + public ActionResult Domains() + { + var currentDomain = AppDomain.CurrentDomain; + var currentName = currentDomain.FriendlyName; + var pos = currentName.IndexOf('-'); + if (pos > 0) currentName = currentName.Substring(0, pos); + + var text = new System.Text.StringBuilder(); + text.Append("
Process ID: " + Process.GetCurrentProcess().Id + "
"); + text.Append("
"); + text.Append("
IIS Site: " + HostingEnvironment.ApplicationHost.GetSiteName() + "
"); + text.Append("
App ID: " + currentName + "
"); + //text.Append("
AppPool: " + Zbu.WebManagement.AppPoolHelper.GetCurrentApplicationPoolName() + "
"); + text.Append("
"); + + text.Append("
Domains:
    "); + text.Append("
  • Not implemented.
  • "); + /* + foreach (var domain in Zbu.WebManagement.AppDomainHelper.GetAppDomains().OrderBy(x => x.Id)) + { + var name = domain.FriendlyName; + pos = name.IndexOf('-'); + if (pos > 0) name = name.Substring(0, pos); + text.Append("
  • " + +"[" + domain.Id + "] " + name + + (domain.IsDefaultAppDomain() ? " (default)" : "") + + (domain.Id == currentDomain.Id ? " (current)" : "") + + "
  • "); + } + */ + text.Append("
"); + + return ContentHtml(text.ToString()); + } + + public ActionResult Recycle() + { + return ContentHtml("Not implemented—please use IIS console."); + } + + private static Template ImportTemplate(ServiceContext svces, string name, string alias, string text, ITemplate master = null) + { + var t = new Template(name, alias) { Content = text }; + if (master != null) + t.SetMasterTemplate(master); + svces.FileService.SaveTemplate(t); + return t; + } + } + + public class TestComponent : IComponent + { + public void Initialize() + { + if (ConfigurationManager.AppSettings["Umbraco.TestData.Enabled"] != "true") + return; + + RouteTable.Routes.MapRoute( + name: "LoadTest", + url: "LoadTest/{action}", + defaults: new + { + controller = "LoadTest", + action = "Index" + }, + namespaces: new[] { "Umbraco.TestData" } + ); + } + + public void Terminate() + { + } + } + + public class TestComposer : ComponentComposer, IUserComposer + { + public override void Compose(Composition composition) + { + base.Compose(composition); + + if (ConfigurationManager.AppSettings["Umbraco.TestData.Enabled"] != "true") + return; + + composition.Register(typeof(LoadTestController), Lifetime.Request); + } + } +} diff --git a/src/Umbraco.TestData/Umbraco.TestData.csproj b/src/Umbraco.TestData/Umbraco.TestData.csproj index d61321ebb8..a3753cc17b 100644 --- a/src/Umbraco.TestData/Umbraco.TestData.csproj +++ b/src/Umbraco.TestData/Umbraco.TestData.csproj @@ -41,6 +41,7 @@ + diff --git a/src/Umbraco.Web.UI/Umbraco.Web.UI.csproj b/src/Umbraco.Web.UI/Umbraco.Web.UI.csproj index 0de757531a..7b3a24512f 100644 --- a/src/Umbraco.Web.UI/Umbraco.Web.UI.csproj +++ b/src/Umbraco.Web.UI/Umbraco.Web.UI.csproj @@ -126,6 +126,10 @@ {52ac0ba8-a60e-4e36-897b-e8b97a54ed1c} Umbraco.ModelsBuilder.Embedded + + {fb5676ed-7a69-492c-b802-e7b24144c0fc} + Umbraco.TestData + {651e1350-91b6-44b7-bd60-7207006d7003} Umbraco.Web diff --git a/src/Umbraco.Web/Compose/DatabaseServerRegistrarAndMessengerComponent.cs b/src/Umbraco.Web/Compose/DatabaseServerRegistrarAndMessengerComponent.cs index 6dfad8c45b..2fa9d80779 100644 --- a/src/Umbraco.Web/Compose/DatabaseServerRegistrarAndMessengerComponent.cs +++ b/src/Umbraco.Web/Compose/DatabaseServerRegistrarAndMessengerComponent.cs @@ -218,7 +218,7 @@ namespace Umbraco.Web.Compose } catch (Exception e) { - _logger.Error("Failed (will repeat).", e); + _logger.Error(e, "Failed (will repeat)."); } return true; // repeat } diff --git a/src/Umbraco.Web/Scheduling/HealthCheckNotifier.cs b/src/Umbraco.Web/Scheduling/HealthCheckNotifier.cs index 746bc61e34..9a4a4f0e2c 100644 --- a/src/Umbraco.Web/Scheduling/HealthCheckNotifier.cs +++ b/src/Umbraco.Web/Scheduling/HealthCheckNotifier.cs @@ -5,6 +5,7 @@ using Umbraco.Core; using Umbraco.Core.Composing; using Umbraco.Core.Configuration; using Umbraco.Core.Logging; +using Umbraco.Core.Scoping; using Umbraco.Core.Sync; using Umbraco.Web.HealthCheck; @@ -15,16 +16,17 @@ namespace Umbraco.Web.Scheduling private readonly IRuntimeState _runtimeState; private readonly HealthCheckCollection _healthChecks; private readonly HealthCheckNotificationMethodCollection _notifications; + private readonly IScopeProvider _scopeProvider; private readonly IProfilingLogger _logger; public HealthCheckNotifier(IBackgroundTaskRunner runner, int delayMilliseconds, int periodMilliseconds, HealthCheckCollection healthChecks, HealthCheckNotificationMethodCollection notifications, - IRuntimeState runtimeState, - IProfilingLogger logger) + IScopeProvider scopeProvider, IRuntimeState runtimeState, IProfilingLogger logger) : base(runner, delayMilliseconds, periodMilliseconds) { _healthChecks = healthChecks; _notifications = notifications; + _scopeProvider = scopeProvider; _runtimeState = runtimeState; _logger = logger; } @@ -51,6 +53,10 @@ namespace Umbraco.Web.Scheduling return false; // do NOT repeat, going down } + // Ensure we use an explicit scope since we are running on a background thread and plugin health + // checks can be making service/database calls so we want to ensure the CallContext/Ambient scope + // isn't used since that can be problematic. + using (var scope = _scopeProvider.CreateScope()) using (_logger.DebugDuration("Health checks executing", "Health checks complete")) { var healthCheckConfig = Current.Configs.HealthChecks(); diff --git a/src/Umbraco.Web/Scheduling/LogScrubber.cs b/src/Umbraco.Web/Scheduling/LogScrubber.cs index db13a80f9b..ffdb584c7a 100644 --- a/src/Umbraco.Web/Scheduling/LogScrubber.cs +++ b/src/Umbraco.Web/Scheduling/LogScrubber.cs @@ -70,8 +70,7 @@ namespace Umbraco.Web.Scheduling return false; // do NOT repeat, going down } - // running on a background task, and Log.CleanLogs uses the old SqlHelper, - // better wrap in a scope and ensure it's all cleaned up and nothing leaks + // Ensure we use an explicit scope since we are running on a background thread. using (var scope = _scopeProvider.CreateScope()) using (_logger.DebugDuration("Log scrubbing executing", "Log scrubbing complete")) { diff --git a/src/Umbraco.Web/Scheduling/ScheduledPublishing.cs b/src/Umbraco.Web/Scheduling/ScheduledPublishing.cs index 2e79e40d7a..97afe25e22 100644 --- a/src/Umbraco.Web/Scheduling/ScheduledPublishing.cs +++ b/src/Umbraco.Web/Scheduling/ScheduledPublishing.cs @@ -2,6 +2,7 @@ using System.Linq; using Umbraco.Core; using Umbraco.Core.Logging; +using Umbraco.Core.Scoping; using Umbraco.Core.Services; using Umbraco.Core.Sync; @@ -55,30 +56,24 @@ namespace Umbraco.Web.Scheduling try { - // ensure we run with an UmbracoContext, because this may run in a background task, - // yet developers may be using the 'current' UmbracoContext in the event handlers - // - // TODO: or maybe not, CacheRefresherComponent already ensures a context when handling events - // - UmbracoContext 'current' needs to be refactored and cleaned up - // - batched messenger should not depend on a current HttpContext - // but then what should be its "scope"? could we attach it to scopes? - // - and we should definitively *not* have to flush it here (should be auto) - // - using (var contextReference = _umbracoContextFactory.EnsureUmbracoContext()) + // We don't need an explicit scope here because PerformScheduledPublish creates it's own scope + // so it's safe as it will create it's own ambient scope. + // Ensure we run with an UmbracoContext, because this will run in a background task, + // and developers may be using the UmbracoContext in the event handlers. + + using var contextReference = _umbracoContextFactory.EnsureUmbracoContext(); + try { - try - { - // run - var result = _contentService.PerformScheduledPublish(DateTime.Now); - foreach (var grouped in result.GroupBy(x => x.Result)) - _logger.Info("Scheduled publishing result: '{StatusCount}' items with status {Status}", grouped.Count(), grouped.Key); - } - finally - { - // if running on a temp context, we have to flush the messenger - if (contextReference.IsRoot && Composing.Current.ServerMessenger is BatchedDatabaseServerMessenger m) - m.FlushBatch(); - } + // run + var result = _contentService.PerformScheduledPublish(DateTime.Now); + foreach (var grouped in result.GroupBy(x => x.Result)) + _logger.Info("Scheduled publishing result: '{StatusCount}' items with status {Status}", grouped.Count(), grouped.Key); + } + finally + { + // if running on a temp context, we have to flush the messenger + if (contextReference.IsRoot && Composing.Current.ServerMessenger is BatchedDatabaseServerMessenger m) + m.FlushBatch(); } } catch (Exception ex) diff --git a/src/Umbraco.Web/Scheduling/SchedulerComponent.cs b/src/Umbraco.Web/Scheduling/SchedulerComponent.cs index a08289186f..f6ce11f939 100644 --- a/src/Umbraco.Web/Scheduling/SchedulerComponent.cs +++ b/src/Umbraco.Web/Scheduling/SchedulerComponent.cs @@ -155,7 +155,7 @@ namespace Umbraco.Web.Scheduling } var periodInMilliseconds = healthCheckConfig.NotificationSettings.PeriodInHours * 60 * 60 * 1000; - var task = new HealthCheckNotifier(_healthCheckRunner, delayInMilliseconds, periodInMilliseconds, healthChecks, notifications, _runtime, logger); + var task = new HealthCheckNotifier(_healthCheckRunner, delayInMilliseconds, periodInMilliseconds, healthChecks, notifications, _scopeProvider, _runtime, logger); _healthCheckRunner.TryAdd(task); return task; }