Initial work on workflow instance cancellation
This commit is contained in:
parent
94b086f5f0
commit
64636a362b
|
|
@ -6,7 +6,13 @@ using MediatR;
|
|||
|
||||
namespace Elsa.Activities.Temporal.Common.Handlers
|
||||
{
|
||||
public class RemoveScheduledTriggers : INotificationHandler<BlockingActivityRemoved>, INotificationHandler<WorkflowDefinitionPublished>, INotificationHandler<WorkflowDefinitionRetracted>, INotificationHandler<WorkflowDefinitionDeleted>
|
||||
public class RemoveScheduledTriggers :
|
||||
INotificationHandler<BlockingActivityRemoved>,
|
||||
INotificationHandler<WorkflowDefinitionPublished>,
|
||||
INotificationHandler<WorkflowDefinitionRetracted>,
|
||||
INotificationHandler<WorkflowDefinitionDeleted>,
|
||||
INotificationHandler<WorkflowCancelled>,
|
||||
INotificationHandler<WorkflowInstanceCancelled>
|
||||
{
|
||||
private readonly IWorkflowDefinitionScheduler _workflowDefinitionScheduler;
|
||||
private readonly IWorkflowInstanceScheduler _workflowInstanceScheduler;
|
||||
|
|
@ -20,14 +26,16 @@ namespace Elsa.Activities.Temporal.Common.Handlers
|
|||
public async Task Handle(BlockingActivityRemoved notification, CancellationToken cancellationToken)
|
||||
{
|
||||
// TODO: Consider introducing a "stereotype" field for activities to exit early in case they are not stereotyped as "temporal".
|
||||
|
||||
|
||||
await _workflowInstanceScheduler.UnscheduleAsync(
|
||||
notification.WorkflowExecutionContext.WorkflowInstance.Id,
|
||||
notification.BlockingActivity.ActivityId,
|
||||
cancellationToken);
|
||||
}
|
||||
|
||||
public Task Handle(WorkflowDefinitionPublished notification, CancellationToken cancellationToken) => _workflowDefinitionScheduler.UnscheduleAsync(notification.WorkflowDefinition.DefinitionId , cancellationToken);
|
||||
public async Task Handle(WorkflowCancelled notification, CancellationToken cancellationToken) => await _workflowInstanceScheduler.UnscheduleAsync(notification.WorkflowExecutionContext.WorkflowInstance.Id, cancellationToken);
|
||||
public async Task Handle(WorkflowInstanceCancelled notification, CancellationToken cancellationToken) => await _workflowInstanceScheduler.UnscheduleAsync(notification.WorkflowInstance.Id, cancellationToken);
|
||||
public Task Handle(WorkflowDefinitionPublished notification, CancellationToken cancellationToken) => _workflowDefinitionScheduler.UnscheduleAsync(notification.WorkflowDefinition.DefinitionId, cancellationToken);
|
||||
public Task Handle(WorkflowDefinitionRetracted notification, CancellationToken cancellationToken) => _workflowDefinitionScheduler.UnscheduleAsync(notification.WorkflowDefinition.DefinitionId, cancellationToken);
|
||||
public Task Handle(WorkflowDefinitionDeleted notification, CancellationToken cancellationToken) => _workflowDefinitionScheduler.UnscheduleAsync(notification.WorkflowDefinition.DefinitionId, cancellationToken);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,19 @@
|
|||
using Elsa.Models;
|
||||
using Elsa.Services.Models;
|
||||
using MediatR;
|
||||
|
||||
namespace Elsa.Events
|
||||
{
|
||||
/// <summary>
|
||||
/// Published when a workflow instance was cancelled
|
||||
/// </summary>
|
||||
public class WorkflowInstanceCancelled : INotification
|
||||
{
|
||||
public WorkflowInstanceCancelled(WorkflowInstance workflowInstance)
|
||||
{
|
||||
WorkflowInstance = workflowInstance;
|
||||
}
|
||||
|
||||
public WorkflowInstance WorkflowInstance { get; }
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,20 @@
|
|||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using Elsa.Models;
|
||||
|
||||
namespace Elsa.Services
|
||||
{
|
||||
public interface IWorkflowInstanceCanceller
|
||||
{
|
||||
Task<CancelWorkflowInstanceResult> CancelAsync(string workflowInstanceId, CancellationToken cancellationToken = default);
|
||||
}
|
||||
|
||||
public record CancelWorkflowInstanceResult(CancelWorkflowInstanceResultStatus Status, WorkflowInstance? WorkflowInstance);
|
||||
|
||||
public enum CancelWorkflowInstanceResultStatus
|
||||
{
|
||||
Ok,
|
||||
NotFound,
|
||||
InvalidStatus
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,45 @@
|
|||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using Elsa.Exceptions;
|
||||
using Elsa.Options;
|
||||
using Elsa.Services;
|
||||
using Elsa.Services.Models;
|
||||
|
||||
namespace Elsa.Decorators
|
||||
{
|
||||
public class LockingWorkflowInstanceCanceller : IWorkflowInstanceCanceller
|
||||
{
|
||||
private readonly IWorkflowInstanceCanceller _workflowInstanceCanceller;
|
||||
private readonly IDistributedLockProvider _distributedLockProvider;
|
||||
private readonly ElsaOptions _elsaOptions;
|
||||
|
||||
public LockingWorkflowInstanceCanceller(IWorkflowInstanceCanceller workflowInstanceCanceller, IDistributedLockProvider distributedLockProvider, ElsaOptions elsaOptions)
|
||||
{
|
||||
_workflowInstanceCanceller = workflowInstanceCanceller;
|
||||
_distributedLockProvider = distributedLockProvider;
|
||||
_elsaOptions = elsaOptions;
|
||||
}
|
||||
|
||||
public async Task<CancelWorkflowInstanceResult> CancelAsync(string workflowInstanceId, CancellationToken cancellationToken = default)
|
||||
{
|
||||
var workflowInstanceLockKey = $"workflow-instance:{workflowInstanceId}";
|
||||
var currentWorkflowInstanceLockHandle = AmbientLockContext.GetCurrentWorkflowInstanceLock(workflowInstanceId);
|
||||
var workflowInstanceLockHandle = currentWorkflowInstanceLockHandle ?? await _distributedLockProvider.AcquireLockAsync(workflowInstanceLockKey, _elsaOptions.DistributedLockTimeout, cancellationToken);
|
||||
|
||||
if (workflowInstanceLockHandle == null)
|
||||
throw new LockAcquisitionException("Could not acquire a lock within the configured amount of time");
|
||||
|
||||
try
|
||||
{
|
||||
AmbientLockContext.SetCurrentWorkflowInstanceLock(workflowInstanceId, workflowInstanceLockHandle);
|
||||
|
||||
return await _workflowInstanceCanceller.CancelAsync(workflowInstanceId, cancellationToken);
|
||||
}
|
||||
finally
|
||||
{
|
||||
AmbientLockContext.DeleteCurrentWorkflowInstanceLock(workflowInstanceId);
|
||||
await workflowInstanceLockHandle.DisposeAsync();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -95,6 +95,7 @@ namespace Microsoft.Extensions.DependencyInjection
|
|||
services.Decorate<IWorkflowDefinitionStore, EventPublishingWorkflowDefinitionStore>();
|
||||
services.Decorate<IWorkflowInstanceStore, EventPublishingWorkflowInstanceStore>();
|
||||
services.Decorate<IWorkflowInstanceExecutor, LockingWorkflowInstanceExecutor>();
|
||||
services.Decorate<IWorkflowInstanceCanceller, LockingWorkflowInstanceCanceller>();
|
||||
|
||||
//TenantId default source
|
||||
services.TryAddScoped<ITenantAccessor, DefaultTenantAccessor>();
|
||||
|
|
@ -174,6 +175,7 @@ namespace Microsoft.Extensions.DependencyInjection
|
|||
.AddScoped<IWorkflowInstanceExecutor, WorkflowInstanceExecutor>()
|
||||
.AddScoped<IWorkflowTriggerInterruptor, WorkflowTriggerInterruptor>()
|
||||
.AddScoped<IWorkflowReviver, WorkflowReviver>()
|
||||
.AddScoped<IWorkflowInstanceCanceller, WorkflowInstanceCanceller>()
|
||||
.AddSingleton<IWorkflowFactory, WorkflowFactory>()
|
||||
.AddTransient<IWorkflowBlueprintMaterializer, WorkflowBlueprintMaterializer>()
|
||||
.AddSingleton<IWorkflowBlueprintReflector, WorkflowBlueprintReflector>()
|
||||
|
|
|
|||
|
|
@ -0,0 +1,43 @@
|
|||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using Elsa.Events;
|
||||
using Elsa.Models;
|
||||
using Elsa.Persistence;
|
||||
using MediatR;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using NodaTime;
|
||||
|
||||
namespace Elsa.Services.Workflows
|
||||
{
|
||||
public class WorkflowInstanceCanceller : IWorkflowInstanceCanceller
|
||||
{
|
||||
private readonly IWorkflowInstanceStore _workflowInstanceStore;
|
||||
private readonly IClock _clock;
|
||||
private readonly IMediator _mediator;
|
||||
|
||||
public WorkflowInstanceCanceller(IWorkflowInstanceStore workflowInstanceStore, IClock clock, IMediator mediator)
|
||||
{
|
||||
_workflowInstanceStore = workflowInstanceStore;
|
||||
_clock = clock;
|
||||
_mediator = mediator;
|
||||
}
|
||||
|
||||
public async Task<CancelWorkflowInstanceResult> CancelAsync(string workflowInstanceId, CancellationToken cancellationToken = default)
|
||||
{
|
||||
var workflowInstance = await _workflowInstanceStore.FindByIdAsync(workflowInstanceId, cancellationToken);
|
||||
|
||||
if (workflowInstance == null)
|
||||
return new CancelWorkflowInstanceResult(CancelWorkflowInstanceResultStatus.NotFound, null);
|
||||
|
||||
if (workflowInstance.WorkflowStatus != WorkflowStatus.Idle && workflowInstance.WorkflowStatus != WorkflowStatus.Running && workflowInstance.WorkflowStatus != WorkflowStatus.Suspended)
|
||||
return new CancelWorkflowInstanceResult(CancelWorkflowInstanceResultStatus.InvalidStatus, workflowInstance);
|
||||
|
||||
workflowInstance.WorkflowStatus = WorkflowStatus.Cancelled;
|
||||
workflowInstance.CancelledAt = _clock.GetCurrentInstant();
|
||||
|
||||
await _workflowInstanceStore.SaveAsync(workflowInstance, cancellationToken);
|
||||
|
||||
return new CancelWorkflowInstanceResult(CancelWorkflowInstanceResultStatus.Ok, workflowInstance);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -128,12 +128,12 @@ namespace Elsa.Services.Workflows
|
|||
default:
|
||||
throw new ArgumentOutOfRangeException();
|
||||
}
|
||||
|
||||
|
||||
await _mediator.Publish(new WorkflowExecuted(workflowExecutionContext), cancellationToken);
|
||||
|
||||
var statusEvent = workflowExecutionContext.Status switch
|
||||
{
|
||||
WorkflowStatus.Cancelled => new WorkflowCancelled(workflowExecutionContext),
|
||||
WorkflowStatus.Cancelled => new WorkflowCancelled(workflowExecutionContext), // TODO: Publish WorkflowInstanceCancelled event also
|
||||
WorkflowStatus.Finished => new WorkflowCompleted(workflowExecutionContext),
|
||||
WorkflowStatus.Faulted => new WorkflowFaulted(workflowExecutionContext),
|
||||
WorkflowStatus.Suspended => new WorkflowSuspended(workflowExecutionContext),
|
||||
|
|
|
|||
|
|
@ -11,10 +11,15 @@
|
|||
<script src="/build/assets/js/monaco-editor/min/vs/loader.js"></script>
|
||||
<script type="module" src="/build/elsa-workflows-studio.esm.js"></script>
|
||||
<script nomodule src="/build/elsa-workflows-studio.js"></script>
|
||||
<style>
|
||||
elsa-studio-dashboard nav.elsa-bg-gray-800 {
|
||||
background-color: teal;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
|
||||
<elsa-studio-root server-url="https://localhost:15265" monaco-lib-path="build/assets/js/monaco-editor/min" culture="en-US">
|
||||
<elsa-studio-root server-url="https://localhost:11000" monaco-lib-path="build/assets/js/monaco-editor/min" culture="en-US">
|
||||
<!-- The root dashboard component -->
|
||||
<elsa-studio-dashboard></elsa-studio-dashboard>
|
||||
|
||||
|
|
|
|||
|
|
@ -0,0 +1,47 @@
|
|||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using Elsa.Models;
|
||||
using Elsa.Persistence;
|
||||
using Elsa.Services;
|
||||
using Microsoft.AspNetCore.Http;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Swashbuckle.AspNetCore.Annotations;
|
||||
|
||||
namespace Elsa.Server.Api.Endpoints.WorkflowInstances
|
||||
{
|
||||
[ApiController]
|
||||
[ApiVersion("1")]
|
||||
[Route("v{apiVersion:apiVersion}/workflow-instances/{id}/cancel")]
|
||||
[Produces("application/json")]
|
||||
public class Cancel : Controller
|
||||
{
|
||||
private readonly IWorkflowInstanceCanceller _canceller;
|
||||
|
||||
public Cancel(IWorkflowInstanceCanceller canceller)
|
||||
{
|
||||
_canceller = canceller;
|
||||
}
|
||||
|
||||
[HttpPost]
|
||||
[ProducesResponseType(StatusCodes.Status200OK)]
|
||||
[ProducesResponseType(StatusCodes.Status404NotFound)]
|
||||
[ProducesResponseType(StatusCodes.Status400BadRequest)]
|
||||
[SwaggerOperation(
|
||||
Summary = "Cancels a workflow instance.",
|
||||
Description = "Retries a workflow instance.",
|
||||
OperationId = "WorkflowInstances.Retry",
|
||||
Tags = new[] { "WorkflowInstances" })
|
||||
]
|
||||
public async Task<IActionResult> Handle(string id, CancellationToken cancellationToken = default)
|
||||
{
|
||||
var result = await _canceller.CancelAsync(id, cancellationToken);
|
||||
|
||||
return result.Status switch
|
||||
{
|
||||
CancelWorkflowInstanceResultStatus.NotFound => NotFound(),
|
||||
CancelWorkflowInstanceResultStatus.InvalidStatus => BadRequest($"Cannot cancel a workflow instance with status {result.WorkflowInstance!.WorkflowStatus}"),
|
||||
_ => Ok()
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
Loading…
Reference in a new issue