Update workflow launchpad to prevent duplicate correlated workflow instances

This commit is contained in:
Sipke Schoorstra 2021-11-01 11:15:00 +01:00
parent 4967a35e43
commit 0e42ad5fe0
12 changed files with 181 additions and 30 deletions

View file

@ -368,6 +368,8 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "retention", "retention", "{
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Elsa.Retention", "src\modules\retention\Elsa.Retention\Elsa.Retention.csproj", "{FAEEA170-560C-4AF3-B4A8-92DEBD59D418}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Elsa.Samples.UniqueCorrelatedWorkflows", "src\samples\aspnet\Elsa.Samples.UniqueCorrelatedWorkflows\Elsa.Samples.UniqueCorrelatedWorkflows.csproj", "{4CD8293D-4544-4A23-BDEF-15B66D5E924F}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
@ -870,6 +872,10 @@ Global
{FAEEA170-560C-4AF3-B4A8-92DEBD59D418}.Debug|Any CPU.Build.0 = Debug|Any CPU
{FAEEA170-560C-4AF3-B4A8-92DEBD59D418}.Release|Any CPU.ActiveCfg = Release|Any CPU
{FAEEA170-560C-4AF3-B4A8-92DEBD59D418}.Release|Any CPU.Build.0 = Release|Any CPU
{4CD8293D-4544-4A23-BDEF-15B66D5E924F}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{4CD8293D-4544-4A23-BDEF-15B66D5E924F}.Debug|Any CPU.Build.0 = Debug|Any CPU
{4CD8293D-4544-4A23-BDEF-15B66D5E924F}.Release|Any CPU.ActiveCfg = Release|Any CPU
{4CD8293D-4544-4A23-BDEF-15B66D5E924F}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
@ -1034,6 +1040,7 @@ Global
{AB67BA19-5BC2-4C6D-A994-61D51A6A6FD5} = {FC9F520F-BA51-4AD2-BFEE-EF787798E734}
{235ABC3F-A075-4682-B6D7-837098BA6B00} = {69BB424A-AAA3-415C-9651-9F4349CA3AC5}
{FAEEA170-560C-4AF3-B4A8-92DEBD59D418} = {235ABC3F-A075-4682-B6D7-837098BA6B00}
{4CD8293D-4544-4A23-BDEF-15B66D5E924F} = {22E75696-6FE9-436A-9097-EE21C603F818}
EndGlobalSection
GlobalSection(ExtensibilityGlobals) = postSolution
SolutionGuid = {8B0975FD-7050-48B0-88C5-48C33378E158}

View file

@ -129,14 +129,13 @@ namespace Elsa.Services.Workflows
var startableWorkflowDefinition = await CollectStartableWorkflowInternalAsync(workflowBlueprint, activityId, correlationId, contextId, tenantId, cancellationToken);
return startableWorkflowDefinition != null ? await InstantiateStartableWorkflow(startableWorkflowDefinition, cancellationToken) : default;
}
if (string.IsNullOrEmpty(correlationId))
if (string.IsNullOrEmpty(correlationId))
return await CollectWorkflows();
// Acquire a lock on correlation ID to prevent duplicate workflow instances from being created.
await using var correlationLockHandle = await AcquireLockAsync(correlationId, cancellationToken);
return await CollectWorkflows();
}
public async Task FindAndExecuteStartableWorkflowAsync(
@ -282,6 +281,20 @@ namespace Elsa.Services.Workflows
return null;
}
}
// If a correlation ID was specified, make sure we don't already have a non-completed workflow instance.
if (correlationId != null)
{
var correlatedWorkflowInstances = !string.IsNullOrWhiteSpace(correlationId)
? await _workflowInstanceStore.FindManyAsync(new WorkflowInstanceCorrelationIdSpecification(workflowDefinitionId, correlationId).And(new WorkflowUnfinishedStatusSpecification()), cancellationToken: cancellationToken).ToArray()
: Array.Empty<WorkflowInstance>();
_logger.LogDebug("Found {CorrelatedWorkflowCount} workflows with correlation ID {CorrelationId}", correlatedWorkflowInstances.Length, correlationId);
// Return null if there is already a non-finished correlated workflow instance.
if (correlatedWorkflowInstances.Any())
return null;
}
var startActivities = _getsStartActivities.GetStartActivities(workflowBlueprint).Select(x => x.Id).ToHashSet();
var startActivityId = activityId == null ? startActivities.FirstOrDefault() : startActivities.Contains(activityId) ? activityId : default;
@ -299,21 +312,15 @@ namespace Elsa.Services.Workflows
{
var startableWorkflows = new List<StartableWorkflow>();
foreach (var (workflowBlueprint, activityId, correlationId, contextId) in startableWorkflowDefinitions)
foreach (var definition in startableWorkflowDefinitions)
{
var workflowInstance = await _workflowFactory.InstantiateAsync(
workflowBlueprint,
correlationId,
contextId,
cancellationToken: cancellationToken);
await _workflowInstanceStore.SaveAsync(workflowInstance, cancellationToken);
startableWorkflows.Add(new StartableWorkflow(workflowBlueprint, workflowInstance, activityId));
var startableWorkflow = await InstantiateStartableWorkflow(definition, cancellationToken);
startableWorkflows.Add(startableWorkflow);
}
return startableWorkflows;
}
private async Task<StartableWorkflow> InstantiateStartableWorkflow(StartableWorkflowDefinition startableWorkflowDefinition, CancellationToken cancellationToken)
{
var workflowInstance = await _workflowFactory.InstantiateAsync(
@ -321,7 +328,7 @@ namespace Elsa.Services.Workflows
startableWorkflowDefinition.CorrelationId,
startableWorkflowDefinition.ContextId,
cancellationToken: cancellationToken);
await _workflowInstanceStore.SaveAsync(workflowInstance, cancellationToken);
return new StartableWorkflow(startableWorkflowDefinition.WorkflowBlueprint, workflowInstance, startableWorkflowDefinition.ActivityId);
}
@ -341,7 +348,7 @@ namespace Elsa.Services.Workflows
var correlationId = query.CorrelationId!;
var existingHandle = AmbientLockContext.CurrentCorrelationLock;
var handle = existingHandle == null ? await AcquireLockAsync(correlationId, cancellationToken) : default;
try
{
var correlatedWorkflowInstances = !string.IsNullOrWhiteSpace(correlationId)
@ -351,14 +358,14 @@ namespace Elsa.Services.Workflows
_logger.LogDebug("Found {CorrelatedWorkflowCount} workflows with correlation ID {CorrelationId}", correlatedWorkflowInstances.Length, correlationId);
var collectedWorkflows = new List<CollectedWorkflow>();
if (correlatedWorkflowInstances.Any())
{
var bookmarkResults = query.Bookmark != null
? await _bookmarkFinder.FindBookmarksAsync(query.ActivityType, query.Bookmark, correlationId, query.TenantId, cancellationToken).ToList()
: new List<BookmarkFinderResult>();
_logger.LogDebug("Found {BookmarkCount} bookmarks for activity type {ActivityType}", bookmarkResults.Count, query.ActivityType);
collectedWorkflows.AddRange(bookmarkResults.Select(x => new CollectedWorkflow(x.WorkflowInstanceId, x.ActivityId)));
}

View file

@ -5,7 +5,7 @@
* It contains typing information for all components that exist in this project.
*/
import { HTMLStencilElement, JSXBase } from "@stencil/core/internal";
import { ActivityDefinitionProperty, ActivityDescriptor, ActivityModel, ActivityPropertyDescriptor, ElsaStudio, OrderBy, SelectListItem, VersionOptions, WorkflowBlueprint, WorkflowDefinition, WorkflowExecutionLogRecord, WorkflowInstance, WorkflowModel, WorkflowStatus } from "./models";
import { ActivityDefinitionProperty, ActivityDescriptor, ActivityModel, ActivityPropertyDescriptor, ElsaStudio, OrderBy, SelectListItem, VersionOptions, WorkflowBlueprint, WorkflowDefinition, WorkflowExecutionLogRecord, WorkflowFault, WorkflowInstance, WorkflowModel, WorkflowStatus } from "./models";
import { LocationSegments, MatchResults, RouterHistory } from "@stencil/router";
import { MenuItem } from "./components/controls/elsa-context-menu/models";
import { VNode } from "@stencil/core";
@ -16,6 +16,7 @@ import { Map } from "./utils/utils";
import { PagerData } from "./components/controls/elsa-pager/elsa-pager";
import { ToastNotificationOptions } from "./components/shared/elsa-toast-notification/elsa-toast-notification";
import { WebhookDefinition } from "./modules/elsa-webhooks/models";
import { ActivityStats } from ".";
export namespace Components {
interface ElsaActivityEditorModal {
"culture": string;
@ -280,6 +281,10 @@ export namespace Components {
"history"?: RouterHistory;
"serverUrl": string;
}
interface ElsaWorkflowFaultInformation {
"faultedAt": Date;
"workflowFault": WorkflowFault;
}
interface ElsaWorkflowInstanceJournal {
"activityDescriptors": Array<ActivityDescriptor>;
"selectActivityRecord": (activityId?: string) => Promise<void>;
@ -307,6 +312,9 @@ export namespace Components {
"serverUrl": string;
"workflowInstanceId": string;
}
interface ElsaWorkflowPerformanceInformation {
"activityStats": ActivityStats;
}
interface ElsaWorkflowPropertiesPanel {
"culture": string;
"serverUrl": string;
@ -629,6 +637,12 @@ declare global {
prototype: HTMLElsaWorkflowDefinitionsListScreenElement;
new (): HTMLElsaWorkflowDefinitionsListScreenElement;
};
interface HTMLElsaWorkflowFaultInformationElement extends Components.ElsaWorkflowFaultInformation, HTMLStencilElement {
}
var HTMLElsaWorkflowFaultInformationElement: {
prototype: HTMLElsaWorkflowFaultInformationElement;
new (): HTMLElsaWorkflowFaultInformationElement;
};
interface HTMLElsaWorkflowInstanceJournalElement extends Components.ElsaWorkflowInstanceJournal, HTMLStencilElement {
}
var HTMLElsaWorkflowInstanceJournalElement: {
@ -647,6 +661,12 @@ declare global {
prototype: HTMLElsaWorkflowInstanceViewerScreenElement;
new (): HTMLElsaWorkflowInstanceViewerScreenElement;
};
interface HTMLElsaWorkflowPerformanceInformationElement extends Components.ElsaWorkflowPerformanceInformation, HTMLStencilElement {
}
var HTMLElsaWorkflowPerformanceInformationElement: {
prototype: HTMLElsaWorkflowPerformanceInformationElement;
new (): HTMLElsaWorkflowPerformanceInformationElement;
};
interface HTMLElsaWorkflowPropertiesPanelElement extends Components.ElsaWorkflowPropertiesPanel, HTMLStencilElement {
}
var HTMLElsaWorkflowPropertiesPanelElement: {
@ -727,9 +747,11 @@ declare global {
"elsa-workflow-definition-editor-notifications": HTMLElsaWorkflowDefinitionEditorNotificationsElement;
"elsa-workflow-definition-editor-screen": HTMLElsaWorkflowDefinitionEditorScreenElement;
"elsa-workflow-definitions-list-screen": HTMLElsaWorkflowDefinitionsListScreenElement;
"elsa-workflow-fault-information": HTMLElsaWorkflowFaultInformationElement;
"elsa-workflow-instance-journal": HTMLElsaWorkflowInstanceJournalElement;
"elsa-workflow-instance-list-screen": HTMLElsaWorkflowInstanceListScreenElement;
"elsa-workflow-instance-viewer-screen": HTMLElsaWorkflowInstanceViewerScreenElement;
"elsa-workflow-performance-information": HTMLElsaWorkflowPerformanceInformationElement;
"elsa-workflow-properties-panel": HTMLElsaWorkflowPropertiesPanelElement;
"elsa-workflow-publish-button": HTMLElsaWorkflowPublishButtonElement;
"elsa-workflow-registry-list-screen": HTMLElsaWorkflowRegistryListScreenElement;
@ -998,6 +1020,10 @@ declare namespace LocalJSX {
"history"?: RouterHistory;
"serverUrl"?: string;
}
interface ElsaWorkflowFaultInformation {
"faultedAt"?: Date;
"workflowFault"?: WorkflowFault;
}
interface ElsaWorkflowInstanceJournal {
"activityDescriptors"?: Array<ActivityDescriptor>;
"onRecordSelected"?: (event: CustomEvent<WorkflowExecutionLogRecord>) => void;
@ -1022,6 +1048,9 @@ declare namespace LocalJSX {
"serverUrl"?: string;
"workflowInstanceId"?: string;
}
interface ElsaWorkflowPerformanceInformation {
"activityStats"?: ActivityStats;
}
interface ElsaWorkflowPropertiesPanel {
"culture"?: string;
"serverUrl"?: string;
@ -1102,9 +1131,11 @@ declare namespace LocalJSX {
"elsa-workflow-definition-editor-notifications": ElsaWorkflowDefinitionEditorNotifications;
"elsa-workflow-definition-editor-screen": ElsaWorkflowDefinitionEditorScreen;
"elsa-workflow-definitions-list-screen": ElsaWorkflowDefinitionsListScreen;
"elsa-workflow-fault-information": ElsaWorkflowFaultInformation;
"elsa-workflow-instance-journal": ElsaWorkflowInstanceJournal;
"elsa-workflow-instance-list-screen": ElsaWorkflowInstanceListScreen;
"elsa-workflow-instance-viewer-screen": ElsaWorkflowInstanceViewerScreen;
"elsa-workflow-performance-information": ElsaWorkflowPerformanceInformation;
"elsa-workflow-properties-panel": ElsaWorkflowPropertiesPanel;
"elsa-workflow-publish-button": ElsaWorkflowPublishButton;
"elsa-workflow-registry-list-screen": ElsaWorkflowRegistryListScreen;
@ -1165,9 +1196,11 @@ declare module "@stencil/core" {
"elsa-workflow-definition-editor-notifications": LocalJSX.ElsaWorkflowDefinitionEditorNotifications & JSXBase.HTMLAttributes<HTMLElsaWorkflowDefinitionEditorNotificationsElement>;
"elsa-workflow-definition-editor-screen": LocalJSX.ElsaWorkflowDefinitionEditorScreen & JSXBase.HTMLAttributes<HTMLElsaWorkflowDefinitionEditorScreenElement>;
"elsa-workflow-definitions-list-screen": LocalJSX.ElsaWorkflowDefinitionsListScreen & JSXBase.HTMLAttributes<HTMLElsaWorkflowDefinitionsListScreenElement>;
"elsa-workflow-fault-information": LocalJSX.ElsaWorkflowFaultInformation & JSXBase.HTMLAttributes<HTMLElsaWorkflowFaultInformationElement>;
"elsa-workflow-instance-journal": LocalJSX.ElsaWorkflowInstanceJournal & JSXBase.HTMLAttributes<HTMLElsaWorkflowInstanceJournalElement>;
"elsa-workflow-instance-list-screen": LocalJSX.ElsaWorkflowInstanceListScreen & JSXBase.HTMLAttributes<HTMLElsaWorkflowInstanceListScreenElement>;
"elsa-workflow-instance-viewer-screen": LocalJSX.ElsaWorkflowInstanceViewerScreen & JSXBase.HTMLAttributes<HTMLElsaWorkflowInstanceViewerScreenElement>;
"elsa-workflow-performance-information": LocalJSX.ElsaWorkflowPerformanceInformation & JSXBase.HTMLAttributes<HTMLElsaWorkflowPerformanceInformationElement>;
"elsa-workflow-properties-panel": LocalJSX.ElsaWorkflowPropertiesPanel & JSXBase.HTMLAttributes<HTMLElsaWorkflowPropertiesPanelElement>;
"elsa-workflow-publish-button": LocalJSX.ElsaWorkflowPublishButton & JSXBase.HTMLAttributes<HTMLElsaWorkflowPublishButtonElement>;
"elsa-workflow-registry-list-screen": LocalJSX.ElsaWorkflowRegistryListScreen & JSXBase.HTMLAttributes<HTMLElsaWorkflowRegistryListScreenElement>;

View file

@ -6,7 +6,7 @@
</PropertyGroup>
<ItemGroup>
<PackageReference Include="System.Text.Encodings.Web" Version="5.0.1"/>
<PackageReference Include="System.Text.Encodings.Web" Version="5.0.1" />
</ItemGroup>
<ItemGroup>

View file

@ -6,7 +6,7 @@
"environmentVariables": {
"ASPNETCORE_ENVIRONMENT": "Development"
},
"applicationUrl": "https://localhost:7309"
"applicationUrl": "https://localhost:11000"
}
}
}

View file

@ -1,9 +0,0 @@
{
"Logging": {
"LogLevel": {
"Default": "Information",
"Microsoft": "Warning",
"Microsoft.Hosting.Lifetime": "Information"
}
}
}

View file

@ -0,0 +1,17 @@
<Project Sdk="Microsoft.NET.Sdk.Web">
<PropertyGroup>
<TargetFramework>net5.0</TargetFramework>
<IsPackable>false</IsPackable>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="System.Text.Encodings.Web" Version="5.0.1" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\..\..\activities\Elsa.Activities.Http\Elsa.Activities.Http.csproj" />
<ProjectReference Include="..\..\..\core\Elsa\Elsa.csproj" />
<ProjectReference Include="..\..\..\server\Elsa.Server.Api\Elsa.Server.Api.csproj" />
</ItemGroup>
</Project>

View file

@ -0,0 +1,14 @@
using Microsoft.AspNetCore.Hosting;
using Microsoft.Extensions.Hosting;
namespace Elsa.Samples.UniqueCorrelatedWorkflows
{
public class Program
{
public static void Main(string[] args) => CreateHostBuilder(args).Build().Run();
public static IHostBuilder CreateHostBuilder(string[] args) =>
Host.CreateDefaultBuilder(args)
.ConfigureWebHostDefaults(webBuilder => { webBuilder.UseStartup<Startup>(); });
}
}

View file

@ -0,0 +1,12 @@
{
"profiles": {
"Elsa.Samples.UniqueCorrelatedWorkflows": {
"commandName": "Project",
"launchBrowser": false,
"environmentVariables": {
"ASPNETCORE_ENVIRONMENT": "Development"
},
"applicationUrl": "https://localhost:11000"
}
}
}

View file

@ -0,0 +1,37 @@
using Microsoft.AspNetCore.Builder;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
namespace Elsa.Samples.UniqueCorrelatedWorkflows
{
public class Startup
{
public Startup(IConfiguration configuration)
{
Configuration = configuration;
}
public IConfiguration Configuration { get; }
public void ConfigureServices(IServiceCollection services)
{
services
.AddControllers();
services
.AddElsa(options => options
.AddConsoleActivities()
.AddWorkflowsFrom<Startup>()
)
.AddElsaApiEndpoints();
}
public void Configure(IApplicationBuilder app)
{
app.UseRouting();
app.UseEndpoints(endpoints => endpoints.MapControllers());
app.UseWelcomePage();
}
}
}

View file

@ -0,0 +1,19 @@
using Elsa.Activities.Console;
using Elsa.Activities.ControlFlow;
using Elsa.Builders;
namespace Elsa.Samples.UniqueCorrelatedWorkflows.Workflows
{
public class MyCorrelatedWorkflow : IWorkflow
{
public void Build(IWorkflowBuilder builder)
{
builder
.WriteLine("I started! Send me the \"Continue\" signal to continue.")
.SignalReceived("Continue")
.WriteLine("Thank you. Now I can complete my work.")
.WriteLine("Good bye!")
;
}
}
}

View file

@ -0,0 +1,14 @@
{
"Logging": {
"LogLevel": {
"Default": "Information",
"Microsoft": "Warning",
"Microsoft.Hosting.Lifetime": "Information"
}
},
"Elsa": {
"Http": {
"BaseUrl": "https://localhost:7309"
}
}
}