Merge pull request #8398 from umbraco/v8/bugfix/sqlmaindom-updates

Adjustments for SqlMainDomLock and others to make azure operations more resilient
This commit is contained in:
Shannon Deminick
2020-07-09 17:13:05 +10:00
committed by GitHub
14 changed files with 729 additions and 280 deletions

View File

@@ -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.
/// <summary>
/// Provides the transient error detection logic for transient faults that are specific to SQL Azure.
/// </summary>
@@ -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.
/// </summary>
/// <param name="ex">The exception object to be verified.</param>
/// <returns>True if the specified exception is considered as transient, otherwise false.</returns>
/// <returns>true if the specified exception is considered as transient; otherwise, false.</returns>
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.

View File

@@ -12,6 +12,11 @@ namespace Umbraco.Core.Persistence.Repositories
/// </summary>
void ClearSchedule(DateTime date);
void ClearSchedule(DateTime date, ContentScheduleAction action);
bool HasContentForExpiration(DateTime date);
bool HasContentForRelease(DateTime date);
/// <summary>
/// Gets <see cref="IContent"/> objects having an expiration date before (lower than, or equal to) a specified date.
/// </summary>

View File

@@ -1000,6 +1000,37 @@ namespace Umbraco.Core.Persistence.Repositories.Implement
Database.Execute(sql);
}
/// <inheritdoc />
public void ClearSchedule(DateTime date, ContentScheduleAction action)
{
var a = action.ToString();
var sql = Sql().Delete<ContentScheduleDto>().Where<ContentScheduleDto>(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<ContentScheduleDto>()
.Where<ContentScheduleDto>(x => x.Action == SqlTemplate.Arg<string>("action") && x.Date <= SqlTemplate.Arg<DateTime>("date")));
var sql = template.Sql(action.ToString(), date);
return sql;
}
public bool HasContentForExpiration(DateTime date)
{
var sql = GetSqlForHasScheduling(ContentScheduleAction.Expire, date);
return Database.ExecuteScalar<int>(sql) > 0;
}
public bool HasContentForRelease(DateTime date)
{
var sql = GetSqlForHasScheduling(ContentScheduleAction.Release, date);
return Database.ExecuteScalar<int>(sql) > 0;
}
/// <inheritdoc />
public IEnumerable<IContent> GetContentForRelease(DateTime date)
{

View File

@@ -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<SqlMainDomLock>("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<SqlMainDomLock>(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<SqlMainDomLock>("Acquired with ID {LockId}", _lockId);
return true;
}
@@ -100,23 +94,23 @@ namespace Umbraco.Core.Runtime
}
catch (Exception ex)
{
ResetDatabase();
// unexpected
_logger.Error<SqlMainDomLock>(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<SqlMainDomLock>("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<SqlMainDomLock>(ex, "Unexpected error, listening is canceled.");
_hasError = true;
return;
_logger.Error<SqlMainDomLock>(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;
}
/// <summary>
/// Wait for any existing MainDom to release so we can continue booting
/// </summary>
@@ -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<KeyValueDto>("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<SqlMainDomLock>("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<SqlMainDomLock>("Cannot acquire, another booting application detected.");
return false;
}
}
catch (Exception ex)
{
ResetDatabase();
if (IsLockTimeoutException(ex))
{
_logger.Error<SqlMainDomLock>(ex, "Sql timeout occurred, waiting for existing MainDom is canceled.");
_hasError = true;
return false;
}
// unexpected
_logger.Error<SqlMainDomLock>(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<SqlMainDomLock>("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<SqlMainDomLock>("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<SqlMainDomLock>(ex, "Sql timeout occurred, could not forcibly acquire MainDom.");
_hasError = true;
return false;
}
_logger.Error<SqlMainDomLock>(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<KeyValueDto>("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<SqlMainDomLock>("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<SqlMainDomLock>("Cannot acquire, another booting application detected.");
return false;
}
}
catch (Exception ex)
{
if (IsLockTimeoutException(ex as SqlException))
{
_logger.Error<SqlMainDomLock>(ex, "Sql timeout occurred, waiting for existing MainDom is canceled.");
_errorDuringAcquiring = true;
return false;
}
// unexpected
_logger.Error<SqlMainDomLock>(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<SqlMainDomLock>("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<SqlMainDomLock>("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<SqlMainDomLock>(ex, "Sql timeout occurred, could not forcibly acquire MainDom.");
_errorDuringAcquiring = true;
return false;
}
_logger.Error<SqlMainDomLock>(ex, "Unexpected error, could not forcibly acquire MainDom.");
_errorDuringAcquiring = true;
return false;
}
finally
{
transaction.Complete();
}
}
/// <summary>
/// Inserts or updates the key/value row
/// </summary>
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
/// </summary>
/// <returns></returns>
private bool IsMainDomValue(string val)
private bool IsMainDomValue(string val, IUmbracoDatabase db)
{
var db = GetDatabase();
return db.ExecuteScalar<int>("SELECT COUNT(*) FROM umbracoKeyValue WHERE [key] = @key AND [value] = @val",
new { key = MainDomKey, val = val }) == 1;
}
@@ -366,7 +349,7 @@ namespace Umbraco.Core.Runtime
/// </summary>
/// <param name="exception"></param>
/// <returns></returns>
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<SqlMainDomLock>("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<SqlMainDomLock>("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<SqlMainDomLock>(ex, "Unexpected error during dipsose.");
_hasError = true;
}
finally
{
db?.CompleteTransaction();
ResetDatabase();
transaction.Complete();
}
}
}

View File

@@ -30,7 +30,7 @@ namespace Umbraco.Core.Services.Implement
private IQuery<IContent> _queryNotTrashed;
//TODO: The non-lazy object should be injected
private readonly Lazy<PropertyValidationService> _propertyValidationService = new Lazy<PropertyValidationService>(() => 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
/// <inheritdoc />
public IEnumerable<PublishResult> 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<PublishResult> PerformScheduledPublishInternal(DateTime date)
{
var allLangs = new Lazy<List<ILanguage>>(() => _languageRepository.GetMany().ToList());
var evtMsgs = EventMessagesFactory.Get();
var results = new List<PublishResult>();
using (var scope = ScopeProvider.CreateScope())
PerformScheduledPublishingRelease(date, results, evtMsgs, allLangs);
PerformScheduledPublishingExpiration(date, results, evtMsgs, allLangs);
return results;
}
private void PerformScheduledPublishingExpiration(DateTime date, List<PublishResult> results, EventMessages evtMsgs, Lazy<List<ILanguage>> 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<ContentService>(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<ContentService>(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<PublishResult> results, EventMessages evtMsgs, Lazy<List<ILanguage>> 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<ContentService>("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<ContentService>(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<ContentService>(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<ContentService>(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<ContentService>(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
}
}

View File

@@ -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
/// </summary>
public class IndexRebuilder
{
private readonly IProfilingLogger _logger;
private readonly IEnumerable<IIndexPopulator> _populators;
public IExamineManager ExamineManager { get; }
[Obsolete("Use constructor with all dependencies")]
public IndexRebuilder(IExamineManager examineManager, IEnumerable<IIndexPopulator> populators)
: this(Current.ProfilingLogger, examineManager, populators)
{
}
public IndexRebuilder(IProfilingLogger logger, IExamineManager examineManager, IEnumerable<IIndexPopulator> 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<IndexRebuilder>(e, "Index populating failed for populator {Populator}", populator.GetType());
}
}
}

View File

@@ -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 = @"<html>
<head>
<title>LoadTest</title>
<style>
body { font-family: arial; }
a,a:visited { color: blue; }
h1 { margin: 0; padding: 0; font-size: 120%; font-weight: bold; }
h1 a { text-decoration: none; }
div.block { margin: 20px 0; }
ul { margin:0; }
div.ver { font-size: 80%; }
div.head { padding:0 0 10px 0; margin: 0 0 20px 0; border-bottom: 1px solid #cccccc; }
</style>
</head>
<body>
<div class=""head"">
<h1><a href=""/LoadTest"">LoadTest</a></h1>
<div class=""ver"">" + System.Configuration.ConfigurationManager.AppSettings["umbracoConfigurationStatus"] + @"</div>
</div>
";
private const string FootHtml = @"</body>
</html>";
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<string>(""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 + @"
<div class=""block"">
<span @Html.Raw(missing ? ""style=\""color:red;\"""" : """")>@contents.Length items</span>
<ul>
@foreach (var group in groups)
{
<li>@group.Key: @group.Count()</li>
}
</ul></div>
<div class=""block"">
@foreach (var content in contents)
{
while (content.Id > id)
{
<div style=""color:red;"">@id :: MISSING</div>
id++;
}
if (wurl)
{
<div>@content.Id :: @content.Name :: @content.Url</div>
}
else
{
<div>@content.Id :: @content.Name</div>
} id++;
}
</div>
" + 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:
<ul>
<li><a href=""/LoadTestContainer"">List existing contents</a> (u:url)</li>
<li><a href=""/LoadTest/Create?o=browser"">Create a content</a> (o:origin, r:restart, n:number)</li>
<li><a href=""/LoadTest/Clear"">Clear all contents</a></li>
<li><a href=""/LoadTest/Domains"">List the current domains in w3wp.exe</a></li>
<li><a href=""/LoadTest/Restart"">Restart the current AppDomain</a></li>
<li><a href=""/LoadTest/Recycle"">Recycle the AppPool</a></li>
<li><a href=""/LoadTest/Die"">Cause w3wp.exe to die</a></li>
</ul>
";
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 <a href=\"/LoadTest/Install\">install</a>.");
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<int>(() => 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("<div class=\"block\">Process ID: " + Process.GetCurrentProcess().Id + "</div>");
text.Append("<div class=\"block\">");
text.Append("<div>IIS Site: " + HostingEnvironment.ApplicationHost.GetSiteName() + "</div>");
text.Append("<div>App ID: " + currentName + "</div>");
//text.Append("<div>AppPool: " + Zbu.WebManagement.AppPoolHelper.GetCurrentApplicationPoolName() + "</div>");
text.Append("</div>");
text.Append("<div class=\"block\">Domains:<ul>");
text.Append("<li>Not implemented.</li>");
/*
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("<li style=\""
+ (name != currentName ? "color: #cccccc;" : "")
//+ (domain.Id == currentDomain.Id ? "" : "")
+ "\">"
+"[" + domain.Id + "] " + name
+ (domain.IsDefaultAppDomain() ? " (default)" : "")
+ (domain.Id == currentDomain.Id ? " (current)" : "")
+ "</li>");
}
*/
text.Append("</ul></div>");
return ContentHtml(text.ToString());
}
public ActionResult Recycle()
{
return ContentHtml("Not implemented&mdash;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<TestComponent>, IUserComposer
{
public override void Compose(Composition composition)
{
base.Compose(composition);
if (ConfigurationManager.AppSettings["Umbraco.TestData.Enabled"] != "true")
return;
composition.Register(typeof(LoadTestController), Lifetime.Request);
}
}
}

View File

@@ -41,6 +41,7 @@
<Reference Include="System.Xml" />
</ItemGroup>
<ItemGroup>
<Compile Include="LoadTestController.cs" />
<Compile Include="SegmentTestController.cs" />
<Compile Include="UmbracoTestDataController.cs" />
<Compile Include="Properties\AssemblyInfo.cs" />

View File

@@ -126,6 +126,10 @@
<Project>{52ac0ba8-a60e-4e36-897b-e8b97a54ed1c}</Project>
<Name>Umbraco.ModelsBuilder.Embedded</Name>
</ProjectReference>
<ProjectReference Include="..\Umbraco.TestData\Umbraco.TestData.csproj">
<Project>{fb5676ed-7a69-492c-b802-e7b24144c0fc}</Project>
<Name>Umbraco.TestData</Name>
</ProjectReference>
<ProjectReference Include="..\Umbraco.Web\Umbraco.Web.csproj">
<Project>{651e1350-91b6-44b7-bd60-7207006d7003}</Project>
<Name>Umbraco.Web</Name>

View File

@@ -218,7 +218,7 @@ namespace Umbraco.Web.Compose
}
catch (Exception e)
{
_logger.Error<InstructionProcessTask>("Failed (will repeat).", e);
_logger.Error<InstructionProcessTask>(e, "Failed (will repeat).");
}
return true; // repeat
}

View File

@@ -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<RecurringTaskBase> 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<HealthCheckNotifier>("Health checks executing", "Health checks complete"))
{
var healthCheckConfig = Current.Configs.HealthChecks();

View File

@@ -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<LogScrubber>("Log scrubbing executing", "Log scrubbing complete"))
{

View File

@@ -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<ScheduledPublishing>("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<ScheduledPublishing>("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)

View File

@@ -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;
}