Skip to content

Commit ad7053a

Browse files
Move publish with descendants to a background task with polling (#18497)
* Use background queue for database cache rebuild and track rebuilding status. * Updated OpenApi.json and client-side types. * Updated client to poll for completion of database rebuild. * Move IBackgroundTaskQueue to core and prepare publish branch to run as background task. * Endpoints for retrieval of status and result from branch publish operations. * Poll and retrieve result for publish with descendants. * Handled issues from testing. * Rework to single controller for status and result. * Updated client side sdk. * OpenApi post dev merge gen --------- Co-authored-by: Migaroez <[email protected]>
1 parent 6247f54 commit ad7053a

26 files changed

+486
-82
lines changed

src/Umbraco.Cms.Api.Management/Controllers/Document/DocumentControllerBase.cs

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
using Microsoft.AspNetCore.Authorization;
1+
using Microsoft.AspNetCore.Authorization;
22
using Microsoft.AspNetCore.Http;
33
using Microsoft.AspNetCore.Mvc;
44
using Umbraco.Cms.Api.Management.Controllers.Content;
@@ -140,6 +140,10 @@ protected IActionResult DocumentPublishingOperationStatusResult(
140140
.WithDetail(
141141
"An unspecified error occurred while (un)publishing. Please check the logs for additional information.")
142142
.Build()),
143+
ContentPublishingOperationStatus.TaskResultNotFound => NotFound(problemDetailsBuilder
144+
.WithTitle("The result of the submitted task could not be found")
145+
.Build()),
146+
143147
_ => StatusCode(StatusCodes.Status500InternalServerError, "Unknown content operation status."),
144148
});
145149

src/Umbraco.Cms.Api.Management/Controllers/Document/PublishDocumentWithDescendantsController.cs

Lines changed: 9 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,7 @@ public PublishDocumentWithDescendantsController(
3535

3636
[HttpPut("{id:guid}/publish-with-descendants")]
3737
[MapToApiVersion("1.0")]
38-
[ProducesResponseType(StatusCodes.Status200OK)]
38+
[ProducesResponseType(typeof(PublishWithDescendantsResultModel), StatusCodes.Status200OK)]
3939
[ProducesResponseType(typeof(ProblemDetails), StatusCodes.Status400BadRequest)]
4040
[ProducesResponseType(typeof(ProblemDetails), StatusCodes.Status404NotFound)]
4141
public async Task<IActionResult> PublishWithDescendants(CancellationToken cancellationToken, Guid id, PublishDocumentWithDescendantsRequestModel requestModel)
@@ -54,10 +54,15 @@ public async Task<IActionResult> PublishWithDescendants(CancellationToken cancel
5454
id,
5555
requestModel.Cultures,
5656
BuildPublishBranchFilter(requestModel),
57-
CurrentUserKey(_backOfficeSecurityAccessor));
57+
CurrentUserKey(_backOfficeSecurityAccessor),
58+
true);
5859

59-
return attempt.Success
60-
? Ok()
60+
return attempt.Success && attempt.Result.AcceptedTaskId.HasValue
61+
? Ok(new PublishWithDescendantsResultModel
62+
{
63+
TaskId = attempt.Result.AcceptedTaskId.Value,
64+
IsComplete = false
65+
})
6166
: DocumentPublishingOperationStatusResult(attempt.Status, failedBranchItems: attempt.Result.FailedItems);
6267
}
6368

Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,69 @@
1+
using Asp.Versioning;
2+
using Microsoft.AspNetCore.Authorization;
3+
using Microsoft.AspNetCore.Http;
4+
using Microsoft.AspNetCore.Mvc;
5+
using Umbraco.Cms.Api.Management.ViewModels.Document;
6+
using Umbraco.Cms.Core;
7+
using Umbraco.Cms.Core.Actions;
8+
using Umbraco.Cms.Core.Models.ContentPublishing;
9+
using Umbraco.Cms.Core.Security.Authorization;
10+
using Umbraco.Cms.Core.Services;
11+
using Umbraco.Cms.Core.Services.OperationStatus;
12+
using Umbraco.Cms.Web.Common.Authorization;
13+
using Umbraco.Extensions;
14+
15+
namespace Umbraco.Cms.Api.Management.Controllers.Document;
16+
17+
[ApiVersion("1.0")]
18+
public class PublishDocumentWithDescendantsResultController : DocumentControllerBase
19+
{
20+
private readonly IAuthorizationService _authorizationService;
21+
private readonly IContentPublishingService _contentPublishingService;
22+
23+
public PublishDocumentWithDescendantsResultController(
24+
IAuthorizationService authorizationService,
25+
IContentPublishingService contentPublishingService)
26+
{
27+
_authorizationService = authorizationService;
28+
_contentPublishingService = contentPublishingService;
29+
}
30+
31+
[HttpGet("{id:guid}/publish-with-descendants/result/{taskId:guid}")]
32+
[MapToApiVersion("1.0")]
33+
[ProducesResponseType(typeof(PublishWithDescendantsResultModel), StatusCodes.Status200OK)]
34+
[ProducesResponseType(typeof(ProblemDetails), StatusCodes.Status400BadRequest)]
35+
[ProducesResponseType(typeof(ProblemDetails), StatusCodes.Status404NotFound)]
36+
public async Task<IActionResult> PublishWithDescendantsResult(CancellationToken cancellationToken, Guid id, Guid taskId)
37+
{
38+
AuthorizationResult authorizationResult = await _authorizationService.AuthorizeResourceAsync(
39+
User,
40+
ContentPermissionResource.Branch(ActionPublish.ActionLetter, id),
41+
AuthorizationPolicies.ContentPermissionByResource);
42+
43+
if (!authorizationResult.Succeeded)
44+
{
45+
return Forbidden();
46+
}
47+
48+
// Check if the publishing task has completed, if not, return the status.
49+
var isPublishing = await _contentPublishingService.IsPublishingBranchAsync(taskId);
50+
if (isPublishing)
51+
{
52+
return Ok(new PublishWithDescendantsResultModel
53+
{
54+
TaskId = taskId,
55+
IsComplete = false
56+
});
57+
};
58+
59+
// If completed, get the result and return the status.
60+
Attempt<ContentPublishingBranchResult, ContentPublishingOperationStatus> attempt = await _contentPublishingService.GetPublishBranchResultAsync(taskId);
61+
return attempt.Success
62+
? Ok(new PublishWithDescendantsResultModel
63+
{
64+
TaskId = taskId,
65+
IsComplete = true
66+
})
67+
: DocumentPublishingOperationStatusResult(attempt.Status, failedBranchItems: attempt.Result.FailedItems);
68+
}
69+
}

src/Umbraco.Cms.Api.Management/Controllers/PublishedCache/RebuildPublishedCacheController.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@ public async Task<IActionResult> Rebuild(CancellationToken cancellationToken)
2121
{
2222
var problemDetails = new ProblemDetails
2323
{
24-
Title = "Database cache can not be rebuilt",
24+
Title = "Database cache cannot be rebuilt",
2525
Detail = $"The database cache is in the process of rebuilding.",
2626
Status = StatusCodes.Status400BadRequest,
2727
Type = "Error",

src/Umbraco.Cms.Api.Management/OpenApi.json

Lines changed: 112 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8902,6 +8902,17 @@
89028902
"nullable": true
89038903
}
89048904
}
8905+
},
8906+
"content": {
8907+
"application/json": {
8908+
"schema": {
8909+
"oneOf": [
8910+
{
8911+
"$ref": "#/components/schemas/PublishWithDescendantsResultModel"
8912+
}
8913+
]
8914+
}
8915+
}
89058916
}
89068917
},
89078918
"400": {
@@ -8982,6 +8993,89 @@
89828993
]
89838994
}
89848995
},
8996+
"/umbraco/management/api/v1/document/{id}/publish-with-descendants/result/{taskId}": {
8997+
"get": {
8998+
"tags": [
8999+
"Document"
9000+
],
9001+
"operationId": "GetDocumentByIdPublishWithDescendantsResultByTaskId",
9002+
"parameters": [
9003+
{
9004+
"name": "id",
9005+
"in": "path",
9006+
"required": true,
9007+
"schema": {
9008+
"type": "string",
9009+
"format": "uuid"
9010+
}
9011+
},
9012+
{
9013+
"name": "taskId",
9014+
"in": "path",
9015+
"required": true,
9016+
"schema": {
9017+
"type": "string",
9018+
"format": "uuid"
9019+
}
9020+
}
9021+
],
9022+
"responses": {
9023+
"200": {
9024+
"description": "OK",
9025+
"content": {
9026+
"application/json": {
9027+
"schema": {
9028+
"oneOf": [
9029+
{
9030+
"$ref": "#/components/schemas/PublishWithDescendantsResultModel"
9031+
}
9032+
]
9033+
}
9034+
}
9035+
}
9036+
},
9037+
"400": {
9038+
"description": "Bad Request",
9039+
"content": {
9040+
"application/json": {
9041+
"schema": {
9042+
"oneOf": [
9043+
{
9044+
"$ref": "#/components/schemas/ProblemDetails"
9045+
}
9046+
]
9047+
}
9048+
}
9049+
}
9050+
},
9051+
"404": {
9052+
"description": "Not Found",
9053+
"content": {
9054+
"application/json": {
9055+
"schema": {
9056+
"oneOf": [
9057+
{
9058+
"$ref": "#/components/schemas/ProblemDetails"
9059+
}
9060+
]
9061+
}
9062+
}
9063+
}
9064+
},
9065+
"401": {
9066+
"description": "The resource is protected and requires an authentication token"
9067+
},
9068+
"403": {
9069+
"description": "The authenticated user does not have access to this resource"
9070+
}
9071+
},
9072+
"security": [
9073+
{
9074+
"Backoffice User": [ ]
9075+
}
9076+
]
9077+
}
9078+
},
89859079
"/umbraco/management/api/v1/document/{id}/published": {
89869080
"get": {
89879081
"tags": [
@@ -43154,6 +43248,23 @@
4315443248
},
4315543249
"additionalProperties": false
4315643250
},
43251+
"PublishWithDescendantsResultModel": {
43252+
"required": [
43253+
"isComplete",
43254+
"taskId"
43255+
],
43256+
"type": "object",
43257+
"properties": {
43258+
"taskId": {
43259+
"type": "string",
43260+
"format": "uuid"
43261+
},
43262+
"isComplete": {
43263+
"type": "boolean"
43264+
}
43265+
},
43266+
"additionalProperties": false
43267+
},
4315743268
"PublishedDocumentResponseModel": {
4315843269
"required": [
4315943270
"documentType",
@@ -46815,4 +46926,4 @@
4681546926
}
4681646927
}
4681746928
}
46818-
}
46929+
}
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
namespace Umbraco.Cms.Api.Management.ViewModels.Document;
2+
3+
public class PublishWithDescendantsResultModel
4+
{
5+
public Guid TaskId { get; set; }
6+
7+
public bool IsComplete { get; set; }
8+
}
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
namespace Umbraco.Cms.Core.HostedServices;
2+
3+
/// <summary>
4+
/// A Background Task Queue, to enqueue tasks for executing in the background.
5+
/// </summary>
6+
/// <remarks>
7+
/// Borrowed from https://docs.microsoft.com/en-us/aspnet/core/fundamentals/host/hosted-services?view=aspnetcore-5.0
8+
/// </remarks>
9+
public interface IBackgroundTaskQueue
10+
{
11+
/// <summary>
12+
/// Enqueue a work item to be executed on in the background.
13+
/// </summary>
14+
void QueueBackgroundWorkItem(Func<CancellationToken, Task> workItem);
15+
16+
/// <summary>
17+
/// Dequeue the first item on the queue.
18+
/// </summary>
19+
Task<Func<CancellationToken, Task>?> DequeueAsync(CancellationToken cancellationToken);
20+
}
Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
namespace Umbraco.Cms.Core.Models.ContentPublishing;
1+
namespace Umbraco.Cms.Core.Models.ContentPublishing;
22

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

99
public IEnumerable<ContentPublishingBranchItemResult> FailedItems { get; set; } = [];
10+
11+
public Guid? AcceptedTaskId { get; init; }
1012
}

0 commit comments

Comments
 (0)