-
Notifications
You must be signed in to change notification settings - Fork 183
feat(workflows): project BPMN diagnostics onto an element-keyed instance overlay #1024
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
Merged
Changes from all commits
Commits
Show all changes
6 commits
Select commit
Hold shift + click to select a range
2b9defe
feat(workflows): project BPMN diagnostics onto an element-keyed insta…
sfmskywalker e392324
perf(workflows): make the BPMN element-stats overlay refresh incremental
sfmskywalker b4e655d
fix(workflows): bound the element-stats catch-up loop to a per-tick p…
sfmskywalker 5d96c04
test(workflows): use an internal seam for the element-stats page cap …
sfmskywalker db0f03c
fix(workflows): retain the element-stats overlay across the BPMN canv…
sfmskywalker 7739c76
refactor(workflows): stop discarding the BPMN element-stats handoff task
sfmskywalker File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
98 changes: 98 additions & 0 deletions
98
src/modules/Elsa.Studio.Workflows.Core/Domain/Models/Bpmn/BpmnDiagnosticEventNames.cs
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,98 @@ | ||
| namespace Elsa.Studio.Workflows.Domain.Models.Bpmn; | ||
|
|
||
| /// <summary> | ||
| /// The stable execution log <c>eventName</c> for each <c>Bpmn.Model.State.BpmnDiagnosticKind</c> member, and the | ||
| /// <c>source</c> every one of them carries. Mirrors elsa-core's own | ||
| /// <c>Elsa.Bpmn.Hosting.BpmnDiagnosticEventNames</c> -- the source of truth for these strings, and for the | ||
| /// projection rules that decide which diagnostics are journaled at all -- so Studio has one place to keep in step | ||
| /// with it rather than depending on <c>Bpmn.Model</c>'s own enum values, which it never references. | ||
| /// </summary> | ||
| public static class BpmnDiagnosticEventNames | ||
| { | ||
| /// <summary>The <c>source</c> every execution log entry projected from a BPMN diagnostic carries.</summary> | ||
| public const string Source = "BPMN"; | ||
|
|
||
| /// <summary> | ||
| /// A token arrived at an element via a sequence flow, or an element (a start event, or an error/cancel boundary | ||
| /// firing without an inbound flow) emitted a token of its own. | ||
| /// </summary> | ||
| public const string TokenEmitted = "TokenEmitted"; | ||
|
|
||
| /// <summary>An element started bound work: a single unit, or one instance of a multi-instance loop.</summary> | ||
| public const string Scheduled = "Scheduled"; | ||
|
|
||
| /// <summary>A token arrived at a join and is waiting for its siblings.</summary> | ||
| public const string Waiting = "Waiting"; | ||
|
|
||
| /// <summary>A join fired after its arrivals were satisfied.</summary> | ||
| public const string Joined = "Joined"; | ||
|
|
||
| /// <summary>An end event consumed a token, or a multi-instance loop consumed a finished instance's token.</summary> | ||
| public const string Consumed = "Consumed"; | ||
|
|
||
| /// <summary>A unit of work was cancelled.</summary> | ||
| public const string Canceled = "Canceled"; | ||
|
|
||
| /// <summary>A terminate end event ended the process.</summary> | ||
| public const string Terminated = "Terminated"; | ||
|
|
||
| /// <summary>An element's behavior failed.</summary> | ||
| public const string BehaviorFailure = "BehaviorFailure"; | ||
|
|
||
| /// <summary> | ||
| /// The scope itself finished. Never projected: unlike every other kind, it names neither an element nor a flow, | ||
| /// and the scope's own activity lifecycle already journals its completion. | ||
| /// </summary> | ||
| public const string Completed = "Completed"; | ||
|
|
||
| /// <summary>A unit of work faulted.</summary> | ||
| public const string Faulted = "Faulted"; | ||
|
|
||
| /// <summary>A host completion carrying an attached compensation boundary registered a compensable.</summary> | ||
| public const string CompensationRegistered = "CompensationRegistered"; | ||
|
|
||
| /// <summary>A compensate throw/end event triggered a compensation replay.</summary> | ||
| public const string CompensationTriggered = "CompensationTriggered"; | ||
|
|
||
| /// <summary>A compensation handler ran to completion for one registered compensable.</summary> | ||
| public const string Compensated = "Compensated"; | ||
|
|
||
| /// <summary>A cancel end event began (or completed) cancelling a transaction scope.</summary> | ||
| public const string TransactionCancelled = "TransactionCancelled"; | ||
|
|
||
| /// <summary>An escalation throw/end event staged an enclosing-scope signal notification.</summary> | ||
| public const string EscalationRaised = "EscalationRaised"; | ||
|
|
||
| /// <summary>An escalation notification matched an attached boundary and fired it.</summary> | ||
| public const string EscalationCaught = "EscalationCaught"; | ||
|
|
||
| /// <summary>An escalation reached a scope that could not catch it; a no-op, never a fault.</summary> | ||
| public const string EscalationUnhandled = "EscalationUnhandled"; | ||
|
|
||
| /// <summary> | ||
| /// An interrupting escalation boundary matched a notification whose host had already terminalized; a no-op, | ||
| /// never a fault. | ||
| /// </summary> | ||
| public const string EscalationLate = "EscalationLate"; | ||
|
|
||
| /// <summary>An event subprocess was activated by its start-event trigger.</summary> | ||
| public const string EventSubprocessActivated = "EventSubprocessActivated"; | ||
|
|
||
| /// <summary>An event subprocess body ran to completion.</summary> | ||
| public const string EventSubprocessCompleted = "EventSubprocessCompleted"; | ||
|
|
||
| /// <summary> | ||
| /// A call activity's bound child failed and the engine routed the call-activity failure ladder instead of | ||
| /// normal outbound flows. | ||
| /// </summary> | ||
| public const string CallActivityFailureRouted = "CallActivityFailureRouted"; | ||
|
|
||
| /// <summary>A message, signal or timer triggered scope listener was armed.</summary> | ||
| public const string ScopeListenerArmed = "ScopeListenerArmed"; | ||
|
|
||
| /// <summary>A message, signal or timer triggered scope listener fired.</summary> | ||
| public const string ScopeListenerFired = "ScopeListenerFired"; | ||
|
|
||
| /// <summary>A message, signal or timer triggered scope listener was retired.</summary> | ||
| public const string ScopeListenerRetired = "ScopeListenerRetired"; | ||
| } |
21 changes: 21 additions & 0 deletions
21
src/modules/Elsa.Studio.Workflows.Core/Domain/Models/Bpmn/BpmnDiagnosticLogPayload.cs
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,21 @@ | ||
| namespace Elsa.Studio.Workflows.Domain.Models.Bpmn; | ||
|
|
||
| /// <summary> | ||
| /// The payload every execution log entry projected from a BPMN diagnostic carries. Mirrors elsa-core's | ||
| /// <c>Elsa.Bpmn.Hosting.BpmnDiagnosticLogPayload</c> -- the source of truth for this shape -- field for field, since | ||
| /// that record is exactly what arrives, camelCase, as <see cref="Elsa.Api.Client.Resources.WorkflowInstances.Models.WorkflowExecutionLogRecord.Payload"/> | ||
| /// for an entry whose <c>Source</c> is <see cref="BpmnDiagnosticEventNames.Source"/>. | ||
| /// </summary> | ||
| /// <param name="DiagnosticId">The diagnostic's id in the interpreter's own pinned id stream (<c>diag:N</c>).</param> | ||
| /// <param name="ElementId">The BPMN element the diagnostic is about, or <c>null</c> when it names none.</param> | ||
| /// <param name="FlowId">The BPMN sequence flow the diagnostic is about, or <c>null</c> when it names none.</param> | ||
| /// <param name="TokenId">The token the diagnostic is about, or <c>null</c> when it names none.</param> | ||
| /// <param name="Kind">The diagnostic kind's enum member name; see <see cref="BpmnDiagnosticEventNames"/>.</param> | ||
| /// <param name="Details">Free-form key/value details the interpreter attached, carried verbatim.</param> | ||
| public sealed record BpmnDiagnosticLogPayload( | ||
| string DiagnosticId, | ||
| string? ElementId, | ||
| string? FlowId, | ||
| string? TokenId, | ||
| string Kind, | ||
| IReadOnlyDictionary<string, string>? Details); |
35 changes: 35 additions & 0 deletions
35
src/modules/Elsa.Studio.Workflows.Core/Domain/Models/Bpmn/BpmnElementStats.cs
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,35 @@ | ||
| namespace Elsa.Studio.Workflows.Domain.Models.Bpmn; | ||
|
|
||
| /// <summary> | ||
| /// Instance state for one BPMN element or sequence flow, keyed by its BPMN document id -- an element id, or, for a | ||
| /// sequence flow, a flow id, since element and flow ids are both document-unique. | ||
| /// </summary> | ||
| /// <remarks> | ||
| /// This is a write-only projection derived from diagnostics: <see cref="BpmnElementStatsProjector"/> is the only | ||
| /// thing that produces it, and nothing reads it back into the engine or edits it. It exists so a gateway, an | ||
| /// intermediate event or a sequence flow -- none of which has an Elsa activity id under Option A -- has something | ||
| /// for the instance viewer's overlay to show, alongside the existing activity-keyed <see cref="ActivityStats"/> for | ||
| /// bound work. Mirrors the ClientLib's canvas-neutral <c>BpmnElementStats</c> interface (<c>src/bpmn/model.ts</c>), | ||
| /// which owns the shape from the rendering side; every field here is nullable for the same reason that one is | ||
| /// all-optional: a projection that cannot compute a field says nothing about it rather than reporting a zero. | ||
| /// </remarks> | ||
| public sealed class BpmnElementStats | ||
| { | ||
| /// <summary>How many tokens have entered this element or been carried by this flow.</summary> | ||
| public int? Started { get; set; } | ||
|
|
||
| /// <summary>How many tokens have left this element having completed, or how many times this flow was taken.</summary> | ||
| public int? Completed { get; set; } | ||
|
|
||
| /// <summary>How many tokens are sitting on this element right now (a waiting catch event, an armed listener).</summary> | ||
| public int? Active { get; set; } | ||
|
|
||
| /// <summary>Whether a token is parked here waiting for something external (e.g. a join awaiting its siblings).</summary> | ||
| public bool? Blocked { get; set; } | ||
|
|
||
| /// <summary>Whether execution faulted at this element.</summary> | ||
| public bool? Faulted { get; set; } | ||
|
|
||
| /// <summary>Whether a token here was cancelled (an interrupted activity, a lost event race, a torn-down scope).</summary> | ||
| public bool? Canceled { get; set; } | ||
| } |
164 changes: 164 additions & 0 deletions
164
src/modules/Elsa.Studio.Workflows.Core/Domain/Models/Bpmn/BpmnElementStatsProjector.cs
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,164 @@ | ||
| using System.Linq; | ||
| using System.Text.Json; | ||
| using Elsa.Api.Client.Resources.WorkflowInstances.Models; | ||
|
|
||
| namespace Elsa.Studio.Workflows.Domain.Models.Bpmn; | ||
|
|
||
| /// <summary> | ||
| /// Folds the BPMN diagnostics projected onto a workflow instance's journal (see | ||
| /// <see cref="BpmnDiagnosticEventNames"/> and <see cref="BpmnDiagnosticLogPayload"/>) into a single element-keyed | ||
| /// <see cref="BpmnElementStats"/> map. | ||
| /// </summary> | ||
| /// <remarks> | ||
| /// The one place the mapping from a diagnostic kind to a stats change lives: nothing on the ClientLib side repeats | ||
| /// it, so a BPMN element's badge and a sequence flow's "taken" styling always agree with what this class decided. | ||
| /// Entries from a nested scope (each nested <c>BpmnProcess</c> writes diagnostics on its own activity) fold into | ||
| /// the very same map, because BPMN element and flow ids are unique across the whole document, not merely within | ||
| /// one scope. | ||
| /// </remarks> | ||
| public static class BpmnElementStatsProjector | ||
| { | ||
| private static readonly JsonSerializerOptions PayloadSerializerOptions = new() { PropertyNamingPolicy = JsonNamingPolicy.CamelCase }; | ||
|
|
||
| /// <summary> | ||
| /// Projects the BPMN diagnostic entries in <paramref name="journalEntries"/> into an element-keyed stats map. | ||
| /// Entries whose <c>Source</c> is not <see cref="BpmnDiagnosticEventNames.Source"/>, or whose payload does not | ||
| /// deserialize as a <see cref="BpmnDiagnosticLogPayload"/>, are ignored, so callers may pass an unfiltered | ||
| /// journal page. | ||
| /// </summary> | ||
| public static IReadOnlyDictionary<string, BpmnElementStats> Project(IEnumerable<WorkflowExecutionLogRecord> journalEntries) | ||
| { | ||
| var stats = new Dictionary<string, BpmnElementStats>(); | ||
| Fold(journalEntries, stats); | ||
| return stats; | ||
| } | ||
|
|
||
| /// <summary> | ||
| /// Folds <paramref name="journalEntries"/> into an already-populated element-keyed stats map, mutating it in | ||
| /// place, so a caller that has already folded earlier journal pages need only fold the new ones rather than | ||
| /// re-fold the whole history on every refresh. | ||
| /// </summary> | ||
| public static void Fold(IEnumerable<WorkflowExecutionLogRecord> journalEntries, Dictionary<string, BpmnElementStats> stats) | ||
| { | ||
| foreach (var entry in journalEntries.Where(e => e.Source == BpmnDiagnosticEventNames.Source)) | ||
| { | ||
| if (!TryReadPayload(entry.Payload, out var payload)) | ||
| continue; | ||
|
|
||
| if (!string.IsNullOrEmpty(payload.ElementId)) | ||
| ApplyToElement(GetOrAdd(stats, payload.ElementId), payload.Kind); | ||
|
|
||
| // A diagnostic that names a flow id represents that flow having just been taken -- a sequence flow | ||
| // has no state beyond that -- regardless of which kind carried it (normally TokenEmitted). | ||
| if (!string.IsNullOrEmpty(payload.FlowId)) | ||
| ApplyToFlow(GetOrAdd(stats, payload.FlowId)); | ||
| } | ||
| } | ||
|
|
||
| private static BpmnElementStats GetOrAdd(Dictionary<string, BpmnElementStats> stats, string id) | ||
| { | ||
| if (stats.TryGetValue(id, out var entry)) | ||
| return entry; | ||
|
|
||
| entry = new BpmnElementStats(); | ||
| stats[id] = entry; | ||
| return entry; | ||
| } | ||
|
|
||
| /// <summary> | ||
| /// Applies one diagnostic kind's effect on the element (or flow-target) it names. See the parenthetical list in | ||
| /// the design: token emitted/consumed change how many tokens are present; a join that is <c>Waiting</c> is | ||
| /// blocked, and stops being blocked once it is <c>Joined</c>; work being scheduled, completed or torn down | ||
| /// moves the same active/completed counters bound work's own <c>ActivityStats</c> reports, so an element with | ||
| /// no activity id still tells the same story. | ||
| /// </summary> | ||
| private static void ApplyToElement(BpmnElementStats stats, string kind) | ||
| { | ||
| switch (kind) | ||
| { | ||
| case BpmnDiagnosticEventNames.TokenEmitted: | ||
| case BpmnDiagnosticEventNames.Scheduled: | ||
| case BpmnDiagnosticEventNames.EventSubprocessActivated: | ||
| case BpmnDiagnosticEventNames.CompensationTriggered: | ||
| case BpmnDiagnosticEventNames.ScopeListenerArmed: | ||
| stats.Started = (stats.Started ?? 0) + 1; | ||
| stats.Active = (stats.Active ?? 0) + 1; | ||
| break; | ||
|
|
||
| case BpmnDiagnosticEventNames.Waiting: | ||
| // A join arrived and is waiting on its siblings: a token is present (blocked), not yet consumed. | ||
| stats.Blocked = true; | ||
| break; | ||
|
|
||
| case BpmnDiagnosticEventNames.Joined: | ||
| stats.Blocked = false; | ||
| stats.Completed = (stats.Completed ?? 0) + 1; | ||
| stats.Active = Decrement(stats.Active); | ||
| break; | ||
|
|
||
| case BpmnDiagnosticEventNames.Consumed: | ||
| case BpmnDiagnosticEventNames.Terminated: | ||
| case BpmnDiagnosticEventNames.EventSubprocessCompleted: | ||
| case BpmnDiagnosticEventNames.Compensated: | ||
| case BpmnDiagnosticEventNames.EscalationCaught: | ||
| case BpmnDiagnosticEventNames.ScopeListenerFired: | ||
| stats.Completed = (stats.Completed ?? 0) + 1; | ||
| stats.Active = Decrement(stats.Active); | ||
| break; | ||
|
|
||
| case BpmnDiagnosticEventNames.ScopeListenerRetired: | ||
| stats.Active = Decrement(stats.Active); | ||
| break; | ||
|
|
||
| case BpmnDiagnosticEventNames.Canceled: | ||
| case BpmnDiagnosticEventNames.TransactionCancelled: | ||
| stats.Canceled = true; | ||
| stats.Active = Decrement(stats.Active); | ||
| break; | ||
|
|
||
| case BpmnDiagnosticEventNames.Faulted: | ||
| case BpmnDiagnosticEventNames.BehaviorFailure: | ||
| case BpmnDiagnosticEventNames.CallActivityFailureRouted: | ||
| stats.Faulted = true; | ||
| break; | ||
|
|
||
| // CompensationRegistered, EscalationRaised, EscalationUnhandled and EscalationLate are bookkeeping | ||
| // that leaves no per-element state worth showing; EscalationUnhandled and EscalationLate are | ||
| // documented as never a fault, so they must not be folded into Faulted. | ||
| default: | ||
| break; | ||
| } | ||
| } | ||
|
|
||
| private static void ApplyToFlow(BpmnElementStats stats) | ||
| { | ||
| stats.Started = (stats.Started ?? 0) + 1; | ||
| stats.Completed = (stats.Completed ?? 0) + 1; | ||
| } | ||
|
|
||
| private static int Decrement(int? value) => Math.Max(0, (value ?? 0) - 1); | ||
|
|
||
| private static bool TryReadPayload(object? payload, out BpmnDiagnosticLogPayload result) | ||
| { | ||
| switch (payload) | ||
| { | ||
| case BpmnDiagnosticLogPayload direct: | ||
| result = direct; | ||
| return true; | ||
|
|
||
| case JsonElement { ValueKind: JsonValueKind.Object } element: | ||
| var deserialized = element.Deserialize<BpmnDiagnosticLogPayload>(PayloadSerializerOptions); | ||
|
|
||
| if (deserialized != null) | ||
| { | ||
| result = deserialized; | ||
| return true; | ||
| } | ||
|
|
||
| break; | ||
| } | ||
|
|
||
| result = null!; | ||
| return false; | ||
| } | ||
| } | ||
22 changes: 22 additions & 0 deletions
22
src/modules/Elsa.Studio.Workflows.Core/UI/Contracts/IBpmnElementStatsSink.cs
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,22 @@ | ||
| using Elsa.Studio.Workflows.Domain.Models.Bpmn; | ||
|
|
||
| namespace Elsa.Studio.Workflows.UI.Contracts; | ||
|
|
||
| /// <summary> | ||
| /// An optional capability an <see cref="IDiagramDesigner"/> may implement to accept the element-keyed instance | ||
| /// overlay: a gateway, an intermediate event or a sequence flow has no Elsa activity id under BPMN's Option A, so | ||
| /// <see cref="IDiagramDesigner.UpdateActivityStatsAsync"/> alone never lights one up. | ||
| /// </summary> | ||
| /// <remarks> | ||
| /// Kept as a separate interface, rather than a member on <see cref="IDiagramDesigner"/> itself, so that every other | ||
| /// diagram designer -- which has no BPMN element ids to key anything on -- is untouched by this projection. | ||
| /// </remarks> | ||
| public interface IBpmnElementStatsSink | ||
| { | ||
| /// <summary> | ||
| /// Replaces the whole element-keyed instance overlay. The map is authoritative: an element or flow it no | ||
| /// longer mentions loses its badge or "taken" styling. | ||
| /// </summary> | ||
| /// <param name="elementStats">The stats, keyed by BPMN element id or, for a sequence flow, by flow id.</param> | ||
| Task UpdateElementStatsAsync(IReadOnlyDictionary<string, BpmnElementStats> elementStats); | ||
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.