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.BlockingActivities = new HashSet <BlockingActivity>(); workflowInstance.CurrentActivity = null; workflowInstance.WorkflowStatus = WorkflowStatus.Cancelled; workflowInstance.CancelledAt = _clock.GetCurrentInstant(); await _workflowInstanceStore.SaveAsync(workflowInstance, cancellationToken); await _mediator.Publish(new WorkflowInstanceCancelled(workflowInstance), cancellationToken); return(new CancelWorkflowInstanceResult(CancelWorkflowInstanceResultStatus.Ok, workflowInstance)); }
public async Task ExecuteAsync(CancellationToken cancellationToken = default) { await using var handle = await _distributedLockProvider.AcquireLockAsync(GetType ().Name, _elsaOptions.DistributedLockTimeout, cancellationToken); if (handle == null) { return; } var instances = await _workflowInstanceStore.FindManyAsync(new WorkflowStatusSpecification(WorkflowStatus.Running), cancellationToken : cancellationToken).ToList(); if (instances.Any()) { _logger.LogInformation("Found {WorkflowInstanceCount} workflows with status 'Running'. Resuming each one of them", instances.Count); } else { _logger.LogInformation("Found no workflows with status 'Running'. Nothing to resume"); } foreach (var instance in instances) { await using var correlationLockHandle = await _distributedLockProvider.AcquireLockAsync(instance.CorrelationId, _elsaOptions.DistributedLockTimeout, cancellationToken); if (handle == null) { _logger.LogWarning("Failed to acquire lock on correlation {CorrelationId} for workflow instance {WorkflowInstanceId}", instance.CorrelationId, instance.Id); continue; } _logger.LogInformation("Resuming {WorkflowInstanceId}", instance.Id); var scheduledActivities = instance.ScheduledActivities; if (instance.CurrentActivity == null && !scheduledActivities.Any()) { if (instance.BlockingActivities.Any()) { _logger.LogWarning( "Workflow '{WorkflowInstanceId}' was in the Running state, but has no scheduled activities not has a currently executing one. However, it does have blocking activities, so switching to Suspended status", instance.Id); instance.WorkflowStatus = WorkflowStatus.Suspended; await _workflowInstanceStore.SaveAsync(instance, cancellationToken); continue; } _logger.LogWarning("Workflow '{WorkflowInstanceId}' was in the Running state, but has no scheduled activities nor has a currently executing one", instance.Id); continue; } var scheduledActivity = instance.CurrentActivity ?? instance.ScheduledActivities.Peek(); await _workflowInstanceDispatcher.DispatchAsync(new ExecuteWorkflowInstanceRequest(instance.Id, scheduledActivity.ActivityId), cancellationToken); } }
protected override async ValueTask <IActivityExecutionResult> OnExecuteAsync(ActivityExecutionContext context) { // Persist the workflow before sending the signal. This fixes a use case where a responding workflow sends a response signal handled by this workflow in a separate branch for example. await _workflowInstanceStore.SaveAsync(context.WorkflowInstance, context.CancellationToken); // Trigger the signal synchronously. If we dispatched the signal instead, we don;t have to explicitly save the workflow instance. For future reconsideration. await _signaler.DispatchSignalAsync(Signal, Input, correlationId : CorrelationId, cancellationToken : context.CancellationToken); return(Done()); }
private async Task <StartableWorkflow> InstantiateStartableWorkflow(StartableWorkflowDefinition startableWorkflowDefinition, CancellationToken cancellationToken) { var workflowInstance = await _workflowFactory.InstantiateAsync( startableWorkflowDefinition.WorkflowBlueprint, startableWorkflowDefinition.CorrelationId, startableWorkflowDefinition.ContextId, cancellationToken : cancellationToken); await _workflowInstanceStore.SaveAsync(workflowInstance, cancellationToken); return(new StartableWorkflow(startableWorkflowDefinition.WorkflowBlueprint, workflowInstance, startableWorkflowDefinition.ActivityId)); }
private async ValueTask SaveWorkflowAsync(WorkflowInstance workflowInstance, CancellationToken cancellationToken) { _logger.LogTrace("Persisting workflow instance {WorkflowInstanceId}", workflowInstance.Id); // Can't prune data - we need to figure out a better way to remove activity output data. // Doing it right now causes issues when transferring output data from composite activities. //workflowExecutionContext.PruneActivityData(); await _workflowInstanceStore.SaveAsync(workflowInstance, cancellationToken); _logger.LogDebug("Committed workflow {WorkflowInstanceId} to storage", workflowInstance.Id); }
public async Task Handle(WorkflowSuspended notification, CancellationToken cancellationToken) { try { await notification.WorkflowExecutionContext.ProcessRegisteredTasksAsync(cancellationToken); } catch (Exception e) { notification.WorkflowExecutionContext.Fault(e, "Error occurred while executing post-suspension task", null, null, false); await _workflowInstanceStore.SaveAsync(notification.WorkflowExecutionContext.WorkflowInstance, cancellationToken); } }
private async Task <StartableWorkflow?> CollectStartableWorkflowInternalAsync( IWorkflowBlueprint workflowBlueprint, string?activityId, string correlationId, string?contextId = default, string?tenantId = default, CancellationToken cancellationToken = default) { var workflowDefinitionId = workflowBlueprint.Id; if (!ValidatePreconditions(workflowDefinitionId, workflowBlueprint)) { return(null); } // Acquire a lock on the workflow definition so that we can ensure singleton-workflows never execute more than one instance. var lockKey = $"execute-workflow-definition:tenant:{tenantId}:workflow-definition:{workflowDefinitionId}"; await using var workflowDefinitionHandle = await AcquireLockAsync(lockKey, cancellationToken); if (workflowBlueprint.IsSingleton) { if (await GetWorkflowIsAlreadyExecutingAsync(tenantId, workflowDefinitionId)) { _logger.LogDebug("Workflow {WorkflowDefinitionId} is a singleton workflow and is already running", workflowDefinitionId); return(null); } } var startActivities = _getsStartActivities.GetStartActivities(workflowBlueprint).Select(x => x.Id).ToHashSet(); var startActivityId = activityId == null?startActivities.FirstOrDefault() : startActivities.Contains(activityId) ? activityId : default; if (startActivityId == null) { _logger.LogWarning("Cannot start workflow {WorkflowDefinitionId} with version {WorkflowDefinitionVersion} because it has no starting activities", workflowBlueprint.Id, workflowBlueprint.Version); return(null); } var workflowInstance = await _workflowFactory.InstantiateAsync( workflowBlueprint, correlationId, contextId, cancellationToken); await _workflowInstanceStore.SaveAsync(workflowInstance, cancellationToken); return(new StartableWorkflow(workflowBlueprint, workflowInstance, startActivityId)); }
public async Task <WorkflowInstance> StartWorkflowAsync( IWorkflowBlueprint workflowBlueprint, string?activityId = default, object?input = default, string?correlationId = default, string?contextId = default, CancellationToken cancellationToken = default) { var workflowInstance = await _workflowFactory.InstantiateAsync( workflowBlueprint, correlationId, contextId, cancellationToken); await _workflowInstanceStore.SaveAsync(workflowInstance, cancellationToken); return(await _workflowRunner.RunWorkflowAsync(workflowBlueprint, workflowInstance, activityId, input, cancellationToken)); }
public async Task <RunWorkflowResult> StartWorkflowAsync( IWorkflowBlueprint workflowBlueprint, string?activityId = default, WorkflowInput?input = default, string?correlationId = default, string?contextId = default, string?tenantId = default, CancellationToken cancellationToken = default) { var workflowInstance = await _workflowFactory.InstantiateAsync( workflowBlueprint, correlationId, contextId, tenantId, cancellationToken); await _workflowStorageService.UpdateInputAsync(workflowInstance, input, cancellationToken); await _workflowInstanceStore.SaveAsync(workflowInstance, cancellationToken); return(await _workflowRunner.RunWorkflowAsync(workflowBlueprint, workflowInstance, activityId, cancellationToken)); }
public async Task <WorkflowInstance> ReviveAsync(WorkflowInstance workflowInstance, CancellationToken cancellationToken) { if (workflowInstance.WorkflowStatus != WorkflowStatus.Faulted) { throw new InvalidOperationException($"Cannot revive non-faulted workflow with status {workflowInstance.WorkflowStatus}"); } var fault = workflowInstance.Fault; if (fault == null) { throw new WorkflowException("Cannot revive a workflow with no fault"); } var faultedActivityId = fault.FaultedActivityId; if (faultedActivityId == null) { // The workflow failed before or after running an activity. // If no activities were scheduled, the workflow faulted before finishing executing the first activity. var hasScheduledActivities = workflowInstance.CurrentActivity != null; workflowInstance.WorkflowStatus = !hasScheduledActivities ? WorkflowStatus.Idle : fault.Resuming ? WorkflowStatus.Suspended : WorkflowStatus.Running; } else { // An activity caused the fault. workflowInstance.WorkflowStatus = fault.Resuming ? WorkflowStatus.Suspended : WorkflowStatus.Running; workflowInstance.ScheduledActivities.Push(new ScheduledActivity(faultedActivityId, fault.ActivityInput)); } workflowInstance.Fault = null; workflowInstance.FaultedAt = null; await _workflowInstanceStore.SaveAsync(workflowInstance, cancellationToken); return(workflowInstance); }
protected override async ValueTask <IActivityExecutionResult> OnExecuteAsync(ActivityExecutionContext context) { // Persist the workflow before sending the signal. This fixes a use case where a responding workflow sends a response signal handled by this workflow in a separate branch for example. await _workflowInstanceStore.SaveAsync(context.WorkflowInstance, context.CancellationToken); // Trigger the signal synchronously. If we dispatched the signal instead, we don't have to explicitly save the workflow instance. For future reconsideration. // Warning: Sending a signal directly instead of dispatching it can potentially cause a deadlock if another workflow sends a signal back to this workflow immediately. switch (SendMode) { case SendSignalMode.Synchronously: await _signaler.TriggerSignalAsync(Signal, Input, correlationId : CorrelationId, cancellationToken : context.CancellationToken); break; case SendSignalMode.Asynchronously: default: await _signaler.DispatchSignalAsync(Signal, Input, correlationId : CorrelationId, cancellationToken : context.CancellationToken); break; } return(Done()); }
public async Task SaveAsync(WorkflowInstance entity, CancellationToken cancellationToken = default) { await _store.SaveAsync(entity, cancellationToken); await _mediator.Publish(new WorkflowInstanceSaved(entity), cancellationToken); }
public async Task <StartableWorkflow?> CollectStartableWorkflowAsync(IWorkflowBlueprint workflowBlueprint, string?activityId, string?correlationId = default, string?contextId = default, string?tenantId = default, CancellationToken cancellationToken = default) { var workflowDefinitionId = workflowBlueprint.Id; if (!ValidatePreconditions(workflowDefinitionId, workflowBlueprint)) { return(null); } var correlationLockHandle = default(IDistributedSynchronizationHandle?); // If we are creating a correlated workflow, make sure to acquire a lock on it to prevent duplicate workflow instances from being created. if (!string.IsNullOrWhiteSpace(correlationId)) { _logger.LogDebug("Acquiring lock on correlation ID {CorrelationId}", correlationId); correlationLockHandle = await _distributedLockProvider.AcquireLockAsync(correlationId, _elsaOptions.DistributedLockTimeout, cancellationToken); if (correlationLockHandle == null) { throw new LockAcquisitionException($"Failed to acquire a lock on correlation ID {correlationId}"); } } try { // Acquire a lock on the workflow definition so that we can ensure singleton-workflows never execute more than one instance. var lockKey = $"execute-workflow-definition:tenant:{tenantId}:workflow-definition:{workflowDefinitionId}"; await using var handle = await _distributedLockProvider.AcquireLockAsync(lockKey, _elsaOptions.DistributedLockTimeout, cancellationToken); if (handle == null) { throw new LockAcquisitionException($"Failed to acquire a lock on {lockKey}"); } if (!workflowBlueprint !.IsSingleton || await GetWorkflowIsAlreadyExecutingAsync(tenantId, workflowDefinitionId) == false) { var startActivities = _getsStartActivities.GetStartActivities(workflowBlueprint).Select(x => x.Id).ToHashSet(); var startActivityId = activityId == null?startActivities.FirstOrDefault() : startActivities.Contains(activityId) ? activityId : default; if (startActivityId == null) { _logger.LogWarning("Cannot start workflow {WorkflowDefinitionId} with version {WorkflowDefinitionVersion} because it has no starting activities", workflowBlueprint.Id, workflowBlueprint.Version); return(null); } var workflowInstance = await _workflowFactory.InstantiateAsync( workflowBlueprint, correlationId, contextId, cancellationToken); await _workflowInstanceStore.SaveAsync(workflowInstance, cancellationToken); return(new StartableWorkflow(workflowBlueprint, workflowInstance, startActivityId)); } } finally { if (correlationLockHandle != null) { await correlationLockHandle.DisposeAsync(); } } return(null); }
private async Task SaveWorkflowAsync(Workflow workflow, CancellationToken cancellationToken) { var workflowInstance = workflow.ToInstance(); await workflowInstanceStore.SaveAsync(workflowInstance, cancellationToken); }