* Enable multitenancy support and normalize tenant ID handling. - Activate multitenancy in `Program.cs`. - Introduce `NormalizeTenantId` method for consistent tenant ID usage. - Update tenant-related classes and features to support normalization logic. * Add ADR for adopting empty string as the default tenant ID - Standardized the tenant ID for the default tenant to use an empty string (`""`) instead of `null`. - Documented the rationale and migration considerations in ADR 0007. - Updated ADR table of contents and graph for new entry. * Apply suggestion from @sfmskywalker * Update doc/adr/graph.dot Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> * Normalize spacing and improve readability in `Program.cs`. Fix multitenancy condition formatting. * Fix ADR numbering and update TOC * Add ADRs for flowchart execution model, tenant deletion event, merge modes, and default tenant ID - Introduced ADR 0005: Token-centric flowchart execution model for improved loop and join handling. - Added ADR 0006: Tenant Deleted event for distinct handling of tenant removal. - Documented ADR 0007: Explicit merge modes for flowchart joins, improving reliability and configurability. - Included ADR 0008: Standardization of empty string as the default tenant ID for consistency and clarity. * Add unit tests for tenant ID normalization and multitenancy pipeline invoker - Added comprehensive unit tests for tenant ID normalization to ensure consistent handling of null, empty, and valid IDs. - Introduced tests for the multitenancy pipeline invoker covering various tenant resolution scenarios. - Updated solution to include new unit testing projects for `Elsa.Tenants` and `Elsa.Common`. * Update unit tests for `ActivityConstructionResult` - Refactor test parameterization to verify `HasExceptions` property more explicitly. - Simplify exception creation logic in helper methods. - Improve test assertions by combining act and assert phases where applicable. * Enable configuration-based multitenancy with tenant-specific settings - Introduced a configuration-based tenant provider to streamline tenant initialization and customization. - Added tenant ID handling filters to ensure tenant ID is applied and filtered automatically. - Deprecated the `CommonPersistenceFeature` in favor of modular persistence feature extension. * Update database indexes to include `TenantId` for multitenancy support - Added `TenantId` to unique constraints on `Triggers` table across all EFCore providers. - Adjusted index names to reflect the updated constraints. - Updated trigger configuration to ensure uniqueness includes `TenantId`. * Add tenant filtering to `DefaultWorkflowDefinitionStorePopulator` - Introduced `ITenantAccessor` to support tenant-specific filtering of workflow definitions. - Updated logic to skip workflows not matching the current tenant. * Update doc/adr/toc.md Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> * Remove `CommonPersistenceFeature` as it has been deprecated * Add tenant-specific filtering to workflow import logic in `DefaultWorkflowDefinitionStorePopulator` * Replace hardcoded tenant ID with `Tenant.DefaultTenantId` in integration tests * Update database indexes and migration logic to support `TenantId` for multitenancy - Added `TenantId` to unique constraints on the `Triggers` table and updated index names. - Included logic to drop outdated indexes without `TenantId` during migration. - Adjusted tests to account for `TenantId` in workflow identity and indexing scenarios. * Remove `TenantId` from workflow identity construction in concurrent trigger indexing tests * Introduce `SelectiveMockLockProvider` for precise lock mocking in tests - Added `SelectiveMockLockProvider` to allow targeted lock mocking without affecting unrelated background operations. - Updated test services to use `SelectiveMockLockProvider` in place of `TestDistributedLockProvider`. - Refactored `DistributedLockResilienceTests` to support selective mocking for deterministic and reliable assertions. * Update Elsa.sln Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> * Normalize tenant ID handling in `DefaultWorkflowDefinitionStorePopulator` for consistent filtering * Refactor `TenantResolverResult` to support explicit resolved/unresolved state handling - Updated `TenantResolverResult` to include an explicit `_isResolved` property. - Adjusted `ResolveTenantId()` and `IsResolved` logic for improved clarity and robustness. - Simplified tenant resolution invocation in `TenantResolverBase`. - Removed redundant normalization in `DefaultTenantResolverPipelineInvoker`. * Normalize tenant ID handling in `DefaultWorkflowDefinitionStorePopulator` and `ClrWorkflowsProvider`. * Refactor `DefaultWorkflowDefinitionStorePopulatorTests`: streamline object initializations and add tenant-specific test coverage for `PopulateStoreAsync`. --------- Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
3.3 KiB
3.3 KiB
5. Token-Centric Flowchart Execution Model
Date: 2025-05-06
Status
Accepted
Context
Elsa Workflows’ original flowchart used execution-count heuristics to drive joins, which fails in loops, XOR splits and resumable activities:
- Loop-back edges never emit a “forward” token, stalling AND-joins.
- Counting executions across iterations causes premature or missed firings.
- Resumable activities (e.g.
Delay) clear join state on resume. - Users cannot declaratively control join semantics without deep framework hacks.
We need a model that:
- Handles loops, forks, XORs and resumable activities reliably.
- Lets designers choose per-activity join behavior.
- Cleans up state to avoid memory leaks.
- Supports cancellation of in-flight branches.
Decision
Adopt a token-centric execution model with explicit MergeMode and blocking:
-
Tokens
- On each activity completion, for each active outbound connection, emit a
Tokenwith:FromActivityId,Outcome,ToActivityId,- Flags:
Consumed = false,Blocked = false.
- Persist the list in
ActivityExecutionContext.Properties["Flowchart.Tokens"].
- On each activity completion, for each active outbound connection, emit a
-
MergeMode
- Query each target activity’s
MergeModeviaGetMergeModeAsync(...). Supported values:- Race: “first wins”
- Stream: “first wins, but don’t cancel ancestors”
- Converge (default): “wait for all”
- Race
- Cancel inbound ancestors (
CancelInboundAncestorsAsync). - If no existing blocked token for this inbound connection, schedule the target and then block all other inbound branches by emitting
Token.Block()for each. - Subsequent branches see their blocked token and simply consume it.
- Cancel inbound ancestors (
- Stream
- Same as Race except you do not cancel inbound ancestors.
- Converge
- Wait until every inbound connection for the target has at least one unblocked, unconsumed token. Then schedule once.
- Query each target activity’s
-
Scheduling Loop
On each child completion:- Emit tokens for its outbound edges.
- Consume any tokens whose
ToActivityIdmatches the completed activity. - For each active outbound connection, inspect its target’s
MergeModeand apply the rules above to decide whether to schedule it.
-
State Cleanup
- After scheduling (or skipping) a target, remove any consumed tokens whose
ToActivityIdequals the completed activity. - When the flow has no pending work (
HasPendingWork()is false), clear the entire token list and complete the flowchart. - On activity cancellation (
OnTokenFlowActivityCanceledAsync), remove all tokens from or to that activity, then re-check for completion.
- After scheduling (or skipping) a target, remove any consumed tokens whose
Sequence Diagram
sequenceDiagram
participant A as Activity A
participant F as Flowchart
participant B as Activity B
A-->>F: Completed(outcome="Done")
F->>F: Emit Token(A→B,Block=false,Consumed=false)
F->>F: Consume any inbound tokens for A
F->>F: Get B.MergeMode()
alt Race & first branch
F->>F: CancelInboundAncestors(B)
F-->>B: Schedule B
F->>F: Emit blocked Tokens for other inbound edges into B
else Race & later branch
F->>F: Consume blocked Token
else Converge until all arrived
Note over F: wait
end
F->>F: Purge consumed tokens for A
F-->>F: Complete if no pending work