Skip to content

Move publish with descendants to a background task with polling #18497

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 13 commits into from
Apr 4, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
using Umbraco.Cms.Api.Management.Controllers.Content;
Expand Down Expand Up @@ -140,6 +140,10 @@
.WithDetail(
"An unspecified error occurred while (un)publishing. Please check the logs for additional information.")
.Build()),
ContentPublishingOperationStatus.TaskResultNotFound => NotFound(problemDetailsBuilder
.WithTitle("The result of the submitted task could not be found")
.Build()),

Check warning on line 146 in src/Umbraco.Cms.Api.Management/Controllers/Document/DocumentControllerBase.cs

View check run for this annotation

CodeScene Delta Analysis / CodeScene Cloud Delta Analysis (v15/dev)

❌ Getting worse: Large Method

DocumentPublishingOperationStatusResult increases from 112 to 115 lines of code, threshold = 70. Large functions with many lines of code are generally harder to understand and lower the code health. Avoid adding more lines to this function.
_ => StatusCode(StatusCodes.Status500InternalServerError, "Unknown content operation status."),
});

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@ public PublishDocumentWithDescendantsController(

[HttpPut("{id:guid}/publish-with-descendants")]
[MapToApiVersion("1.0")]
[ProducesResponseType(StatusCodes.Status200OK)]
[ProducesResponseType(typeof(PublishWithDescendantsResultModel), StatusCodes.Status200OK)]
[ProducesResponseType(typeof(ProblemDetails), StatusCodes.Status400BadRequest)]
[ProducesResponseType(typeof(ProblemDetails), StatusCodes.Status404NotFound)]
public async Task<IActionResult> PublishWithDescendants(CancellationToken cancellationToken, Guid id, PublishDocumentWithDescendantsRequestModel requestModel)
Expand All @@ -54,10 +54,15 @@ public async Task<IActionResult> PublishWithDescendants(CancellationToken cancel
id,
requestModel.Cultures,
BuildPublishBranchFilter(requestModel),
CurrentUserKey(_backOfficeSecurityAccessor));
CurrentUserKey(_backOfficeSecurityAccessor),
true);

return attempt.Success
? Ok()
return attempt.Success && attempt.Result.AcceptedTaskId.HasValue
? Ok(new PublishWithDescendantsResultModel
{
TaskId = attempt.Result.AcceptedTaskId.Value,
IsComplete = false
})
: DocumentPublishingOperationStatusResult(attempt.Status, failedBranchItems: attempt.Result.FailedItems);
}

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
using Asp.Versioning;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
using Umbraco.Cms.Api.Management.ViewModels.Document;
using Umbraco.Cms.Core;
using Umbraco.Cms.Core.Actions;
using Umbraco.Cms.Core.Models.ContentPublishing;
using Umbraco.Cms.Core.Security.Authorization;
using Umbraco.Cms.Core.Services;
using Umbraco.Cms.Core.Services.OperationStatus;
using Umbraco.Cms.Web.Common.Authorization;
using Umbraco.Extensions;

namespace Umbraco.Cms.Api.Management.Controllers.Document;

[ApiVersion("1.0")]
public class PublishDocumentWithDescendantsResultController : DocumentControllerBase
{
private readonly IAuthorizationService _authorizationService;
private readonly IContentPublishingService _contentPublishingService;

public PublishDocumentWithDescendantsResultController(
IAuthorizationService authorizationService,
IContentPublishingService contentPublishingService)
{
_authorizationService = authorizationService;
_contentPublishingService = contentPublishingService;
}

[HttpGet("{id:guid}/publish-with-descendants/result/{taskId:guid}")]
[MapToApiVersion("1.0")]
[ProducesResponseType(typeof(PublishWithDescendantsResultModel), StatusCodes.Status200OK)]
[ProducesResponseType(typeof(ProblemDetails), StatusCodes.Status400BadRequest)]
[ProducesResponseType(typeof(ProblemDetails), StatusCodes.Status404NotFound)]
public async Task<IActionResult> PublishWithDescendantsResult(CancellationToken cancellationToken, Guid id, Guid taskId)
{
AuthorizationResult authorizationResult = await _authorizationService.AuthorizeResourceAsync(
User,
ContentPermissionResource.Branch(ActionPublish.ActionLetter, id),
AuthorizationPolicies.ContentPermissionByResource);

if (!authorizationResult.Succeeded)
{
return Forbidden();
}

// Check if the publishing task has completed, if not, return the status.
var isPublishing = await _contentPublishingService.IsPublishingBranchAsync(taskId);
if (isPublishing)
{
return Ok(new PublishWithDescendantsResultModel
{
TaskId = taskId,
IsComplete = false
});
};

// If completed, get the result and return the status.
Attempt<ContentPublishingBranchResult, ContentPublishingOperationStatus> attempt = await _contentPublishingService.GetPublishBranchResultAsync(taskId);
return attempt.Success
? Ok(new PublishWithDescendantsResultModel
{
TaskId = taskId,
IsComplete = true
})
: DocumentPublishingOperationStatusResult(attempt.Status, failedBranchItems: attempt.Result.FailedItems);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ public async Task<IActionResult> Rebuild(CancellationToken cancellationToken)
{
var problemDetails = new ProblemDetails
{
Title = "Database cache can not be rebuilt",
Title = "Database cache cannot be rebuilt",
Detail = $"The database cache is in the process of rebuilding.",
Status = StatusCodes.Status400BadRequest,
Type = "Error",
Expand Down
113 changes: 112 additions & 1 deletion src/Umbraco.Cms.Api.Management/OpenApi.json
Original file line number Diff line number Diff line change
Expand Up @@ -8902,6 +8902,17 @@
"nullable": true
}
}
},
"content": {
"application/json": {
"schema": {
"oneOf": [
{
"$ref": "#/components/schemas/PublishWithDescendantsResultModel"
}
]
}
}
}
},
"400": {
Expand Down Expand Up @@ -8982,6 +8993,89 @@
]
}
},
"/umbraco/management/api/v1/document/{id}/publish-with-descendants/result/{taskId}": {
"get": {
"tags": [
"Document"
],
"operationId": "GetDocumentByIdPublishWithDescendantsResultByTaskId",
"parameters": [
{
"name": "id",
"in": "path",
"required": true,
"schema": {
"type": "string",
"format": "uuid"
}
},
{
"name": "taskId",
"in": "path",
"required": true,
"schema": {
"type": "string",
"format": "uuid"
}
}
],
"responses": {
"200": {
"description": "OK",
"content": {
"application/json": {
"schema": {
"oneOf": [
{
"$ref": "#/components/schemas/PublishWithDescendantsResultModel"
}
]
}
}
}
},
"400": {
"description": "Bad Request",
"content": {
"application/json": {
"schema": {
"oneOf": [
{
"$ref": "#/components/schemas/ProblemDetails"
}
]
}
}
}
},
"404": {
"description": "Not Found",
"content": {
"application/json": {
"schema": {
"oneOf": [
{
"$ref": "#/components/schemas/ProblemDetails"
}
]
}
}
}
},
"401": {
"description": "The resource is protected and requires an authentication token"
},
"403": {
"description": "The authenticated user does not have access to this resource"
}
},
"security": [
{
"Backoffice User": [ ]
}
]
}
},
"/umbraco/management/api/v1/document/{id}/published": {
"get": {
"tags": [
Expand Down Expand Up @@ -43154,6 +43248,23 @@
},
"additionalProperties": false
},
"PublishWithDescendantsResultModel": {
"required": [
"isComplete",
"taskId"
],
"type": "object",
"properties": {
"taskId": {
"type": "string",
"format": "uuid"
},
"isComplete": {
"type": "boolean"
}
},
"additionalProperties": false
},
"PublishedDocumentResponseModel": {
"required": [
"documentType",
Expand Down Expand Up @@ -46815,4 +46926,4 @@
}
}
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
namespace Umbraco.Cms.Api.Management.ViewModels.Document;

public class PublishWithDescendantsResultModel
{
public Guid TaskId { get; set; }

public bool IsComplete { get; set; }
}
20 changes: 20 additions & 0 deletions src/Umbraco.Core/HostedServices/IBackgroundTaskQueue.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
namespace Umbraco.Cms.Core.HostedServices;

/// <summary>
/// A Background Task Queue, to enqueue tasks for executing in the background.
/// </summary>
/// <remarks>
/// Borrowed from https://docs.microsoft.com/en-us/aspnet/core/fundamentals/host/hosted-services?view=aspnetcore-5.0
/// </remarks>
public interface IBackgroundTaskQueue
{
/// <summary>
/// Enqueue a work item to be executed on in the background.
/// </summary>
void QueueBackgroundWorkItem(Func<CancellationToken, Task> workItem);

/// <summary>
/// Dequeue the first item on the queue.
/// </summary>
Task<Func<CancellationToken, Task>?> DequeueAsync(CancellationToken cancellationToken);
}
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
namespace Umbraco.Cms.Core.Models.ContentPublishing;
namespace Umbraco.Cms.Core.Models.ContentPublishing;

public sealed class ContentPublishingBranchResult
{
Expand All @@ -7,4 +7,6 @@ public sealed class ContentPublishingBranchResult
public IEnumerable<ContentPublishingBranchItemResult> SucceededItems { get; set; } = [];

public IEnumerable<ContentPublishingBranchItemResult> FailedItems { get; set; } = [];

public Guid? AcceptedTaskId { get; init; }
}
Loading
Loading