refactors scheduled publishing logic - splits into 2x scopes/2x trans, only take a write lock when necessary

This commit is contained in:
Shannon
2020-07-08 18:16:12 +10:00
parent e1757178b9
commit df61f30393
3 changed files with 126 additions and 68 deletions

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

@@ -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
@@ -1359,17 +1359,88 @@ namespace Umbraco.Core.Services.Implement
}
/// <inheritdoc />
public IEnumerable<PublishResult> PerformScheduledPublish(DateTime date)
public IEnumerable<PublishResult> PerformScheduledPublish(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 continuing 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))
{
@@ -1382,13 +1453,13 @@ 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 continuing 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;
continue; // this document is canceled move next
}
var publishing = true;
@@ -1401,14 +1472,14 @@ 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; // no point continuing
}
PublishResult result;
@@ -1418,7 +1489,7 @@ namespace Umbraco.Core.Services.Implement
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);
@@ -1441,60 +1512,11 @@ namespace Umbraco.Core.Services.Implement
}
}
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();
_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)))
{
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, 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);
scope.Complete();
}
return results;
scope.Complete();
}
// utility 'PublishCultures' func used by SaveAndPublishBranch
@@ -2627,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);
@@ -3140,6 +3162,6 @@ namespace Umbraco.Core.Services.Implement
#endregion
}
}