diff --git a/Elsa.sln b/Elsa.sln index 0210bc3ce..7e5d65d6b 100644 --- a/Elsa.sln +++ b/Elsa.sln @@ -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} diff --git a/src/core/Elsa.Core/Services/Workflows/WorkflowLaunchpad.cs b/src/core/Elsa.Core/Services/Workflows/WorkflowLaunchpad.cs index 06af13e66..f17851769 100644 --- a/src/core/Elsa.Core/Services/Workflows/WorkflowLaunchpad.cs +++ b/src/core/Elsa.Core/Services/Workflows/WorkflowLaunchpad.cs @@ -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(); + + _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(); - 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 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(); - + if (correlatedWorkflowInstances.Any()) { var bookmarkResults = query.Bookmark != null ? await _bookmarkFinder.FindBookmarksAsync(query.ActivityType, query.Bookmark, correlationId, query.TenantId, cancellationToken).ToList() : new List(); _logger.LogDebug("Found {BookmarkCount} bookmarks for activity type {ActivityType}", bookmarkResults.Count, query.ActivityType); - + collectedWorkflows.AddRange(bookmarkResults.Select(x => new CollectedWorkflow(x.WorkflowInstanceId, x.ActivityId))); } diff --git a/src/designer/elsa-workflows-studio/src/components.d.ts b/src/designer/elsa-workflows-studio/src/components.d.ts index f1803f8ee..f78c8b235 100644 --- a/src/designer/elsa-workflows-studio/src/components.d.ts +++ b/src/designer/elsa-workflows-studio/src/components.d.ts @@ -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; "selectActivityRecord": (activityId?: string) => Promise; @@ -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; "onRecordSelected"?: (event: CustomEvent) => 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; "elsa-workflow-definition-editor-screen": LocalJSX.ElsaWorkflowDefinitionEditorScreen & JSXBase.HTMLAttributes; "elsa-workflow-definitions-list-screen": LocalJSX.ElsaWorkflowDefinitionsListScreen & JSXBase.HTMLAttributes; + "elsa-workflow-fault-information": LocalJSX.ElsaWorkflowFaultInformation & JSXBase.HTMLAttributes; "elsa-workflow-instance-journal": LocalJSX.ElsaWorkflowInstanceJournal & JSXBase.HTMLAttributes; "elsa-workflow-instance-list-screen": LocalJSX.ElsaWorkflowInstanceListScreen & JSXBase.HTMLAttributes; "elsa-workflow-instance-viewer-screen": LocalJSX.ElsaWorkflowInstanceViewerScreen & JSXBase.HTMLAttributes; + "elsa-workflow-performance-information": LocalJSX.ElsaWorkflowPerformanceInformation & JSXBase.HTMLAttributes; "elsa-workflow-properties-panel": LocalJSX.ElsaWorkflowPropertiesPanel & JSXBase.HTMLAttributes; "elsa-workflow-publish-button": LocalJSX.ElsaWorkflowPublishButton & JSXBase.HTMLAttributes; "elsa-workflow-registry-list-screen": LocalJSX.ElsaWorkflowRegistryListScreen & JSXBase.HTMLAttributes; diff --git a/src/samples/aspnet/Elsa.Samples.SignalApi/Elsa.Samples.SignalApi.csproj b/src/samples/aspnet/Elsa.Samples.SignalApi/Elsa.Samples.SignalApi.csproj index 44c7a950f..84f81bffd 100644 --- a/src/samples/aspnet/Elsa.Samples.SignalApi/Elsa.Samples.SignalApi.csproj +++ b/src/samples/aspnet/Elsa.Samples.SignalApi/Elsa.Samples.SignalApi.csproj @@ -6,7 +6,7 @@ - + diff --git a/src/samples/aspnet/Elsa.Samples.SignalApi/Properties/launchSettings.json b/src/samples/aspnet/Elsa.Samples.SignalApi/Properties/launchSettings.json index 51cfa973b..aa69f8623 100644 --- a/src/samples/aspnet/Elsa.Samples.SignalApi/Properties/launchSettings.json +++ b/src/samples/aspnet/Elsa.Samples.SignalApi/Properties/launchSettings.json @@ -6,7 +6,7 @@ "environmentVariables": { "ASPNETCORE_ENVIRONMENT": "Development" }, - "applicationUrl": "https://localhost:7309" + "applicationUrl": "https://localhost:11000" } } } \ No newline at end of file diff --git a/src/samples/aspnet/Elsa.Samples.SignalApi/appsettings.Development.json b/src/samples/aspnet/Elsa.Samples.SignalApi/appsettings.Development.json deleted file mode 100644 index 8983e0fc1..000000000 --- a/src/samples/aspnet/Elsa.Samples.SignalApi/appsettings.Development.json +++ /dev/null @@ -1,9 +0,0 @@ -{ - "Logging": { - "LogLevel": { - "Default": "Information", - "Microsoft": "Warning", - "Microsoft.Hosting.Lifetime": "Information" - } - } -} diff --git a/src/samples/aspnet/Elsa.Samples.UniqueCorrelatedWorkflows/Elsa.Samples.UniqueCorrelatedWorkflows.csproj b/src/samples/aspnet/Elsa.Samples.UniqueCorrelatedWorkflows/Elsa.Samples.UniqueCorrelatedWorkflows.csproj new file mode 100644 index 000000000..417037718 --- /dev/null +++ b/src/samples/aspnet/Elsa.Samples.UniqueCorrelatedWorkflows/Elsa.Samples.UniqueCorrelatedWorkflows.csproj @@ -0,0 +1,17 @@ + + + + net5.0 + false + + + + + + + + + + + + diff --git a/src/samples/aspnet/Elsa.Samples.UniqueCorrelatedWorkflows/Program.cs b/src/samples/aspnet/Elsa.Samples.UniqueCorrelatedWorkflows/Program.cs new file mode 100644 index 000000000..8c3d926d0 --- /dev/null +++ b/src/samples/aspnet/Elsa.Samples.UniqueCorrelatedWorkflows/Program.cs @@ -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(); }); + } +} \ No newline at end of file diff --git a/src/samples/aspnet/Elsa.Samples.UniqueCorrelatedWorkflows/Properties/launchSettings.json b/src/samples/aspnet/Elsa.Samples.UniqueCorrelatedWorkflows/Properties/launchSettings.json new file mode 100644 index 000000000..570087d3f --- /dev/null +++ b/src/samples/aspnet/Elsa.Samples.UniqueCorrelatedWorkflows/Properties/launchSettings.json @@ -0,0 +1,12 @@ +{ + "profiles": { + "Elsa.Samples.UniqueCorrelatedWorkflows": { + "commandName": "Project", + "launchBrowser": false, + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + }, + "applicationUrl": "https://localhost:11000" + } + } +} \ No newline at end of file diff --git a/src/samples/aspnet/Elsa.Samples.UniqueCorrelatedWorkflows/Startup.cs b/src/samples/aspnet/Elsa.Samples.UniqueCorrelatedWorkflows/Startup.cs new file mode 100644 index 000000000..88157325a --- /dev/null +++ b/src/samples/aspnet/Elsa.Samples.UniqueCorrelatedWorkflows/Startup.cs @@ -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() + ) + .AddElsaApiEndpoints(); + + } + + public void Configure(IApplicationBuilder app) + { + app.UseRouting(); + app.UseEndpoints(endpoints => endpoints.MapControllers()); + app.UseWelcomePage(); + } + } +} \ No newline at end of file diff --git a/src/samples/aspnet/Elsa.Samples.UniqueCorrelatedWorkflows/Workflows/MyCorrelatedWorkflow.cs b/src/samples/aspnet/Elsa.Samples.UniqueCorrelatedWorkflows/Workflows/MyCorrelatedWorkflow.cs new file mode 100644 index 000000000..a584673de --- /dev/null +++ b/src/samples/aspnet/Elsa.Samples.UniqueCorrelatedWorkflows/Workflows/MyCorrelatedWorkflow.cs @@ -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!") + ; + } + } +} \ No newline at end of file diff --git a/src/samples/aspnet/Elsa.Samples.UniqueCorrelatedWorkflows/appsettings.json b/src/samples/aspnet/Elsa.Samples.UniqueCorrelatedWorkflows/appsettings.json new file mode 100644 index 000000000..34d0f7a23 --- /dev/null +++ b/src/samples/aspnet/Elsa.Samples.UniqueCorrelatedWorkflows/appsettings.json @@ -0,0 +1,14 @@ +{ + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft": "Warning", + "Microsoft.Hosting.Lifetime": "Information" + } + }, + "Elsa": { + "Http": { + "BaseUrl": "https://localhost:7309" + } + } +}