Skip to content

Commit 86a437a

Browse files
authored
Merge pull request #441 from serverlessworkflow/feat-suspend-resume-cancel
Added means to suspend, resume and cancel workflow instances
2 parents 71d9adb + 803f66d commit 86a437a

23 files changed

+585
-129
lines changed
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
1+
// Copyright © 2024-Present The Synapse Authors
2+
//
3+
// Licensed under the Apache License, Version 2.0 (the "License"),
4+
// you may not use this file except in compliance with the License.
5+
// You may obtain a copy of the License at
6+
// http://www.apache.org/licenses/LICENSE-2.0
7+
//
8+
// Unless required by applicable law or agreed to in writing, software
9+
// distributed under the License is distributed on an "AS IS" BASIS,
10+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
11+
// See the License for the specific language governing permissions and
12+
// limitations under the License.
13+
14+
using Neuroglia.Data.Infrastructure.ResourceOriented.Properties;
15+
using Synapse.Resources;
16+
17+
namespace Synapse.Api.Application.Commands.WorkflowInstances;
18+
19+
/// <summary>
20+
/// Represents the <see cref="Command"/> used to cancel the execution of a <see cref="WorkflowInstance"/>
21+
/// </summary>
22+
/// <param name="name">The name of the <see cref="WorkflowInstance"/> to cancel the execution of</param>
23+
/// <param name="namespace">The namespace the <see cref="WorkflowInstance"/> to cancel the execution of belongs to</param>
24+
public class CancelWorkflowInstanceCommand(string name, string @namespace)
25+
: Command
26+
{
27+
28+
/// <summary>
29+
/// Gets the name of the <see cref="WorkflowInstance"/> to cancel the execution of
30+
/// </summary>
31+
public string Name { get; } = name;
32+
33+
/// <summary>
34+
/// Gets the namespace the <see cref="WorkflowInstance"/> to cancel the execution of belongs to
35+
/// </summary>
36+
public string Namespace { get; } = @namespace;
37+
38+
}
39+
40+
/// <summary>
41+
/// Represents the service used to handle <see cref="CancelWorkflowInstanceCommand"/>s
42+
/// </summary>
43+
/// <param name="resources">The service used to manage <see cref="IResource"/>s</param>
44+
public class CancelWorkflowInstanceCommandHandler(IResourceRepository resources)
45+
: ICommandHandler<CancelWorkflowInstanceCommand>
46+
{
47+
48+
/// <inheritdoc/>
49+
public virtual async Task<IOperationResult> HandleAsync(CancelWorkflowInstanceCommand command, CancellationToken cancellationToken = default)
50+
{
51+
var workflowInstanceReference = new ResourceReference<WorkflowInstance>(command.Name, command.Namespace);
52+
var original = await resources.GetAsync<WorkflowInstance>(command.Name, command.Namespace, cancellationToken).ConfigureAwait(false)
53+
?? throw new ProblemDetailsException(new(ProblemTypes.NotFound, ProblemTitles.NotFound, (int)HttpStatusCode.NotFound, ProblemDescriptions.ResourceNotFound.Format(workflowInstanceReference.ToString())));
54+
if (!string.IsNullOrWhiteSpace(original.Status?.Phase) && original.Status?.Phase != WorkflowInstanceStatusPhase.Pending && original.Status?.Phase != WorkflowInstanceStatusPhase.Running && original.Status?.Phase != WorkflowInstanceStatusPhase.Waiting)
55+
throw new ProblemDetailsException(new(ProblemTypes.AdmissionFailed, ProblemTitles.AdmissionFailed, (int)HttpStatusCode.BadRequest, $"The workflow instance '{workflowInstanceReference}' is in an expected phase '{original.Status?.Phase}'"));
56+
var updated = original.Clone()!;
57+
updated.Status ??= new();
58+
updated.Status.Phase = WorkflowInstanceStatusPhase.Cancelled;
59+
var jsonPatch = JsonPatchUtility.CreateJsonPatchFromDiff(original, updated);
60+
await resources.PatchStatusAsync<WorkflowInstance>(new(PatchType.JsonPatch, jsonPatch), command.Name, command.Namespace, cancellationToken: cancellationToken).ConfigureAwait(false);
61+
return this.Ok();
62+
}
63+
64+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,63 @@
1+
// Copyright © 2024-Present The Synapse Authors
2+
//
3+
// Licensed under the Apache License, Version 2.0 (the "License"),
4+
// you may not use this file except in compliance with the License.
5+
// You may obtain a copy of the License at
6+
// http://www.apache.org/licenses/LICENSE-2.0
7+
//
8+
// Unless required by applicable law or agreed to in writing, software
9+
// distributed under the License is distributed on an "AS IS" BASIS,
10+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
11+
// See the License for the specific language governing permissions and
12+
// limitations under the License.
13+
14+
using Neuroglia.Data.Infrastructure.ResourceOriented.Properties;
15+
using Synapse.Resources;
16+
17+
namespace Synapse.Api.Application.Commands.WorkflowInstances;
18+
19+
/// <summary>
20+
/// Represents the <see cref="Command"/> used to resume the execution of a <see cref="WorkflowInstance"/>
21+
/// </summary>
22+
/// <param name="name">The name of the <see cref="WorkflowInstance"/> to resume the execution of</param>
23+
/// <param name="namespace">The namespace the <see cref="WorkflowInstance"/> to resume the execution of belongs to</param>
24+
public class ResumeWorkflowInstanceCommand(string name, string @namespace)
25+
: Command
26+
{
27+
28+
/// <summary>
29+
/// Gets the name of the <see cref="WorkflowInstance"/> to resume the execution of
30+
/// </summary>
31+
public string Name { get; } = name;
32+
33+
/// <summary>
34+
/// Gets the namespace the <see cref="WorkflowInstance"/> to resume the execution of belongs to
35+
/// </summary>
36+
public string Namespace { get; } = @namespace;
37+
38+
}
39+
40+
/// <summary>
41+
/// Represents the service used to handle <see cref="ResumeWorkflowInstanceCommand"/>s
42+
/// </summary>
43+
/// <param name="resources">The service used to manage <see cref="IResource"/>s</param>
44+
public class ResumeWorkflowInstanceCommandHandler(IResourceRepository resources)
45+
: ICommandHandler<ResumeWorkflowInstanceCommand>
46+
{
47+
48+
/// <inheritdoc/>
49+
public virtual async Task<IOperationResult> HandleAsync(ResumeWorkflowInstanceCommand command, CancellationToken cancellationToken = default)
50+
{
51+
var workflowInstanceReference = new ResourceReference<WorkflowInstance>(command.Name, command.Namespace);
52+
var original = await resources.GetAsync<WorkflowInstance>(command.Name, command.Namespace, cancellationToken).ConfigureAwait(false)
53+
?? throw new ProblemDetailsException(new(ProblemTypes.NotFound, ProblemTitles.NotFound, (int)HttpStatusCode.NotFound, ProblemDescriptions.ResourceNotFound.Format(workflowInstanceReference.ToString())));
54+
if (!string.IsNullOrWhiteSpace(original.Status?.Phase) && original.Status?.Phase != WorkflowInstanceStatusPhase.Waiting) throw new ProblemDetailsException(new(ProblemTypes.AdmissionFailed, ProblemTitles.AdmissionFailed, (int)HttpStatusCode.BadRequest, $"The workflow instance '{workflowInstanceReference}' is in an expected phase '{original.Status?.Phase}'"));
55+
var updated = original.Clone()!;
56+
updated.Status ??= new();
57+
updated.Status.Phase = WorkflowInstanceStatusPhase.Running;
58+
var jsonPatch = JsonPatchUtility.CreateJsonPatchFromDiff(original, updated);
59+
await resources.PatchStatusAsync<WorkflowInstance>(new(PatchType.JsonPatch, jsonPatch), command.Name, command.Namespace, cancellationToken: cancellationToken).ConfigureAwait(false);
60+
return this.Ok();
61+
}
62+
63+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,63 @@
1+
// Copyright © 2024-Present The Synapse Authors
2+
//
3+
// Licensed under the Apache License, Version 2.0 (the "License"),
4+
// you may not use this file except in compliance with the License.
5+
// You may obtain a copy of the License at
6+
// http://www.apache.org/licenses/LICENSE-2.0
7+
//
8+
// Unless required by applicable law or agreed to in writing, software
9+
// distributed under the License is distributed on an "AS IS" BASIS,
10+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
11+
// See the License for the specific language governing permissions and
12+
// limitations under the License.
13+
14+
using Neuroglia.Data.Infrastructure.ResourceOriented.Properties;
15+
using Synapse.Resources;
16+
17+
namespace Synapse.Api.Application.Commands.WorkflowInstances;
18+
19+
/// <summary>
20+
/// Represents the <see cref="Command"/> used to suspend the execution of a <see cref="WorkflowInstance"/>
21+
/// </summary>
22+
/// <param name="name">The name of the <see cref="WorkflowInstance"/> to suspend the execution of</param>
23+
/// <param name="namespace">The namespace the <see cref="WorkflowInstance"/> to suspend the execution of belongs to</param>
24+
public class SuspendWorkflowInstanceCommand(string name, string @namespace)
25+
: Command
26+
{
27+
28+
/// <summary>
29+
/// Gets the name of the <see cref="WorkflowInstance"/> to suspend the execution of
30+
/// </summary>
31+
public string Name { get; } = name;
32+
33+
/// <summary>
34+
/// Gets the namespace the <see cref="WorkflowInstance"/> to suspend the execution of belongs to
35+
/// </summary>
36+
public string Namespace { get; } = @namespace;
37+
38+
}
39+
40+
/// <summary>
41+
/// Represents the service used to handle <see cref="SuspendWorkflowInstanceCommand"/>s
42+
/// </summary>
43+
/// <param name="resources">The service used to manage <see cref="IResource"/>s</param>
44+
public class SuspendWorkflowInstanceCommandHandler(IResourceRepository resources)
45+
: ICommandHandler<SuspendWorkflowInstanceCommand>
46+
{
47+
48+
/// <inheritdoc/>
49+
public virtual async Task<IOperationResult> HandleAsync(SuspendWorkflowInstanceCommand command, CancellationToken cancellationToken = default)
50+
{
51+
var workflowInstanceReference = new ResourceReference<WorkflowInstance>(command.Name, command.Namespace);
52+
var original = await resources.GetAsync<WorkflowInstance>(command.Name, command.Namespace, cancellationToken).ConfigureAwait(false)
53+
?? throw new ProblemDetailsException(new(ProblemTypes.NotFound, ProblemTitles.NotFound, (int)HttpStatusCode.NotFound, ProblemDescriptions.ResourceNotFound.Format(workflowInstanceReference.ToString())));
54+
if (original.Status?.Phase != WorkflowInstanceStatusPhase.Running) throw new ProblemDetailsException(new(ProblemTypes.AdmissionFailed, ProblemTitles.AdmissionFailed, (int)HttpStatusCode.BadRequest, $"The workflow instance '{workflowInstanceReference}' is in an expected phase '{original.Status?.Phase}'"));
55+
var updated = original.Clone()!;
56+
updated.Status ??= new();
57+
updated.Status.Phase = WorkflowInstanceStatusPhase.Waiting;
58+
var jsonPatch = JsonPatchUtility.CreateJsonPatchFromDiff(original, updated);
59+
await resources.PatchStatusAsync<WorkflowInstance>(new(PatchType.JsonPatch, jsonPatch), command.Name, command.Namespace, cancellationToken: cancellationToken).ConfigureAwait(false);
60+
return this.Ok();
61+
}
62+
63+
}

src/api/Synapse.Api.Application/Extensions/IServiceCollectionExtensions.cs

+4
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@
1818
using Synapse.Api.Application.Commands.Documents;
1919
using Synapse.Api.Application.Commands.Events;
2020
using Synapse.Api.Application.Commands.Resources.Generic;
21+
using Synapse.Api.Application.Commands.WorkflowInstances;
2122
using Synapse.Api.Application.Queries.Documents;
2223
using Synapse.Api.Application.Queries.Resources.Generic;
2324
using Synapse.Api.Application.Queries.Users;
@@ -159,6 +160,9 @@ public static IServiceCollection AddApiCommands(this IServiceCollection services
159160
services.Add(new ServiceDescriptor(handlerServiceType, handlerImplementationType, serviceLifetime));
160161

161162
}
163+
services.AddScoped<IRequestHandler<SuspendWorkflowInstanceCommand, IOperationResult>, SuspendWorkflowInstanceCommandHandler>();
164+
services.AddScoped<IRequestHandler<ResumeWorkflowInstanceCommand, IOperationResult>, ResumeWorkflowInstanceCommandHandler>();
165+
services.AddScoped<IRequestHandler<CancelWorkflowInstanceCommand, IOperationResult>, CancelWorkflowInstanceCommandHandler>();
162166
services.AddScoped<IRequestHandler<CreateDocumentCommand, IOperationResult<Document>>, CreateDocumentCommandHandler>();
163167
services.AddScoped<IRequestHandler<UpdateDocumentCommand, IOperationResult<Document>>, UpdateDocumentCommandHandler>();
164168
return services;

src/api/Synapse.Api.Application/Queries/WorkflowInstances/ReadWorkflowInstanceLogsQuery.cs

+6-16
Original file line numberDiff line numberDiff line change
@@ -19,31 +19,21 @@ namespace Synapse.Api.Application.Queries.WorkflowInstances;
1919
/// <summary>
2020
/// Represents the query used to read the logs of the specified workflow instance
2121
/// </summary>
22-
public class ReadWorkflowInstanceLogsQuery
23-
: Query<string>
22+
/// <param name="name">The name of the <see cref="WorkflowInstance"/> to read the logs of</param>
23+
/// <param name="namespace">The namespace the <see cref="WorkflowInstance"/> to read the logs of belongs to</param>
24+
public class ReadWorkflowInstanceLogsQuery(string name, string @namespace)
25+
: Query<string>
2426
{
2527

26-
/// <summary>
27-
/// Initializes a new <see cref="ReadWorkflowInstanceLogsQuery"/>
28-
/// </summary>
29-
/// <param name="name">The name of the <see cref="WorkflowInstance"/> to read the logs of</param>
30-
/// <param name="namespace">The namespace the <see cref="WorkflowInstance"/> to read the logs of belongs to</param>
31-
public ReadWorkflowInstanceLogsQuery(string name, string? @namespace)
32-
{
33-
if (string.IsNullOrWhiteSpace(name)) throw new ArgumentNullException(nameof(name));
34-
this.Name = name;
35-
this.Namespace = @namespace;
36-
}
37-
3828
/// <summary>
3929
/// Gets the name of the <see cref="WorkflowInstance"/> to read the logs of
4030
/// </summary>
41-
public string Name { get; }
31+
public string Name { get; } = name;
4232

4333
/// <summary>
4434
/// Gets the namespace the <see cref="WorkflowInstance"/> to read the logs of belongs to
4535
/// </summary>
46-
public string? Namespace { get; }
36+
public string Namespace { get; } = @namespace;
4737

4838
}
4939

src/api/Synapse.Api.Application/Queries/WorkflowInstances/WatchWorkflowInstanceLogsQuery.cs

+6-16
Original file line numberDiff line numberDiff line change
@@ -20,31 +20,21 @@ namespace Synapse.Api.Application.Queries.WorkflowInstances;
2020
/// <summary>
2121
/// Represents the query used to watch the logs of a specified <see cref="WorkflowInstance"/>
2222
/// </summary>
23-
public class WatchWorkflowInstanceLogsQuery
24-
: Query<IAsyncEnumerable<ITextDocumentWatchEvent>>
23+
/// <param name="name">The name of the <see cref="WorkflowInstance"/> to watch the logs of</param>
24+
/// <param name="namespace">The namespace the <see cref="WorkflowInstance"/> to watch the logs of belongs to</param>
25+
public class WatchWorkflowInstanceLogsQuery(string name, string @namespace)
26+
: Query<IAsyncEnumerable<ITextDocumentWatchEvent>>
2527
{
2628

27-
/// <summary>
28-
/// Initializes a new <see cref="WatchWorkflowInstanceLogsQuery"/>
29-
/// </summary>
30-
/// <param name="name">The name of the <see cref="WorkflowInstance"/> to watch the logs of</param>
31-
/// <param name="namespace">The namespace the <see cref="WorkflowInstance"/> to watch the logs of belongs to</param>
32-
public WatchWorkflowInstanceLogsQuery(string name, string? @namespace)
33-
{
34-
if (string.IsNullOrWhiteSpace(name)) throw new ArgumentNullException(nameof(name));
35-
this.Name = name;
36-
this.Namespace = @namespace;
37-
}
38-
3929
/// <summary>
4030
/// Gets the name of the <see cref="WorkflowInstance"/> to watch the logs of
4131
/// </summary>
42-
public string Name { get; }
32+
public string Name { get; } = name;
4333

4434
/// <summary>
4535
/// Gets the namespace the <see cref="WorkflowInstance"/> to watch the logs of belongs to
4636
/// </summary>
47-
public string? Namespace { get; }
37+
public string? Namespace { get; } = @namespace;
4838

4939
}
5040

src/api/Synapse.Api.Client.Core/Services/IWorkflowInstanceApiClient.cs

+27
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,33 @@ public interface IWorkflowInstanceApiClient
2222
: INamespacedResourceApiClient<WorkflowInstance>
2323
{
2424

25+
/// <summary>
26+
/// Suspends the execution of the specified workflow instance
27+
/// </summary>
28+
/// <param name="name">The name of the workflow instance to suspend the execution of</param>
29+
/// <param name="namespace">The namespace the workflow instance to suspend the execution of belongs to</param>
30+
/// <param name="cancellationToken">A <see cref="CancellationToken"/></param>
31+
/// <returns>A new awaitable <see cref="Task"/></returns>
32+
Task SuspendAsync(string name, string @namespace, CancellationToken cancellationToken = default);
33+
34+
/// <summary>
35+
/// Resumes the execution of the specified workflow instance
36+
/// </summary>
37+
/// <param name="name">The name of the workflow instance to resume the execution of</param>
38+
/// <param name="namespace">The namespace the workflow instance to resume the execution of belongs to</param>
39+
/// <param name="cancellationToken">A <see cref="CancellationToken"/></param>
40+
/// <returns>A new awaitable <see cref="Task"/></returns>
41+
Task ResumeAsync(string name, string @namespace, CancellationToken cancellationToken = default);
42+
43+
/// <summary>
44+
/// Cancels the execution of the specified workflow instance
45+
/// </summary>
46+
/// <param name="name">The name of the workflow instance to cancel the execution of</param>
47+
/// <param name="namespace">The namespace the workflow instance to cancel the execution of belongs to</param>
48+
/// <param name="cancellationToken">A <see cref="CancellationToken"/></param>
49+
/// <returns>A new awaitable <see cref="Task"/></returns>
50+
Task CancelAsync(string name, string @namespace, CancellationToken cancellationToken = default);
51+
2552
/// <summary>
2653
/// Reads the logs of the specified <see cref="WorkflowInstance"/>
2754
/// </summary>

0 commit comments

Comments
 (0)