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/Services/Implement/ContentService.cs b/src/Umbraco.Core/Services/Implement/ContentService.cs index 93e7f0a5df..2a23a1adad 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 @@ -1359,17 +1359,88 @@ namespace Umbraco.Core.Services.Implement } /// - public IEnumerable PerformScheduledPublish(DateTime date) + public IEnumerable PerformScheduledPublish(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 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(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)) { @@ -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("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(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(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); - - 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 - + } }