Files
git.stella-ops.org/docs/workflow/engine/06-implementation-structure.md
master f5b5f24d95 Add StellaOps.Workflow engine: 14 libraries, WebService, 8 test projects
Extract product-agnostic workflow engine from Ablera.Serdica.Workflow into
standalone StellaOps.Workflow.* libraries targeting net10.0.

Libraries (14):
- Contracts, Abstractions (compiler, decompiler, expression runtime)
- Engine (execution, signaling, scheduling, projections, hosted services)
- ElkSharp (generic graph layout algorithm)
- Renderer.ElkSharp, Renderer.ElkJs, Renderer.Msagl, Renderer.Svg
- Signaling.Redis, Signaling.OracleAq
- DataStore.MongoDB, DataStore.PostgreSQL, DataStore.Oracle

WebService: ASP.NET Core Minimal API with 22 endpoints

Tests (8 projects, 109 tests pass):
- Engine.Tests (105 pass), WebService.Tests (4 E2E pass)
- Renderer.Tests, DataStore.MongoDB/Oracle/PostgreSQL.Tests
- Signaling.Redis.Tests, IntegrationTests.Shared

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-20 19:14:44 +02:00

286 lines
7.3 KiB
Markdown

# 06. Implementation Structure
## 1. Implementation Goal
The implementation should mirror the architecture instead of collapsing everything into `Services/`.
The code layout should make it obvious which parts are:
- product orchestration
- engine runtime
- persistence
- signaling
- scheduling
- operations
## 2. Proposed Folder Layout
Recommended new structure across the workflow host, shared abstractions, and external service contracts.
### 2.1 Service Host Project
Proposed folders:
```text
Engine/
Contracts/
Execution/
Persistence/
Signaling/
Scheduling/
Hosting/
Diagnostics/
```
Detailed proposal:
```text
Engine/
Contracts/
IWorkflowRuntimeProvider.cs
IWorkflowSignalBus.cs
IWorkflowScheduleBus.cs
IWorkflowRuntimeSnapshotStore.cs
IWorkflowRuntimeDefinitionStore.cs
Execution/
SerdicaEngineRuntimeProvider.cs
WorkflowExecutionCoordinator.cs
WorkflowCanonicalInterpreter.cs
WorkflowResumePointerSerializer.cs
WorkflowExecutionSliceResult.cs
WorkflowWaitDescriptor.cs
WorkflowSubWorkflowCoordinator.cs
WorkflowTransportDispatcher.cs
Persistence/
OracleWorkflowRuntimeSnapshotStore.cs
WorkflowRuntimeSnapshotMapper.cs
WorkflowRuntimeStateMutator.cs
Signaling/
OracleAqWorkflowSignalBus.cs
WorkflowSignalEnvelope.cs
WorkflowSignalPump.cs
WorkflowSignalHandler.cs
Scheduling/
OracleAqWorkflowScheduleBus.cs
WorkflowScheduleRequest.cs
Hosting/
WorkflowEngineSignalHostedService.cs
WorkflowEngineStartupValidator.cs
Diagnostics/
WorkflowEngineMetrics.cs
WorkflowEngineLogScope.cs
```
### 2.2 Shared Abstractions Project
Keep these in abstractions:
- execution contracts
- signal/schedule bus interfaces
- runtime provider interfaces
- runtime snapshot records where shared
Do not put Oracle-specific details into the shared abstractions project.
### 2.3 Contracts Project
Keep only external service contracts there.
Do not leak engine-internal snapshot or AQ message contracts into public workflow contracts.
## 3. Recommended Core Interfaces
### 3.1 Runtime Provider
```csharp
public interface IWorkflowRuntimeProvider
{
string ProviderName { get; }
Task<WorkflowRuntimeExecutionResult> StartAsync(
WorkflowRegistration registration,
WorkflowDefinitionDescriptor definition,
WorkflowBusinessReference? businessReference,
StartWorkflowRequest request,
object startRequest,
CancellationToken cancellationToken = default);
Task<WorkflowRuntimeExecutionResult> CompleteAsync(
WorkflowRegistration registration,
WorkflowDefinitionDescriptor definition,
WorkflowTaskExecutionContext context,
CancellationToken cancellationToken = default);
}
```
### 3.2 Snapshot Store
```csharp
public interface IWorkflowRuntimeSnapshotStore
{
Task<WorkflowRuntimeSnapshot?> GetAsync(
string workflowInstanceId,
CancellationToken cancellationToken = default);
Task<bool> TryUpsertAsync(
WorkflowRuntimeSnapshot snapshot,
long expectedVersion,
CancellationToken cancellationToken = default);
}
```
### 3.3 Signal Bus
```csharp
public interface IWorkflowSignalBus
{
Task PublishAsync(
WorkflowSignalEnvelope envelope,
CancellationToken cancellationToken = default);
Task<IWorkflowSignalLease?> ReceiveAsync(
string consumerName,
CancellationToken cancellationToken = default);
}
```
### 3.4 Schedule Bus
```csharp
public interface IWorkflowScheduleBus
{
Task ScheduleAsync(
WorkflowSignalEnvelope envelope,
DateTime dueAtUtc,
CancellationToken cancellationToken = default);
}
```
### 3.5 Definition Store
```csharp
public interface IWorkflowRuntimeDefinitionStore
{
WorkflowRuntimeDefinition GetRequiredDefinition(
string workflowName,
string workflowVersion);
}
```
## 4. Runtime Definition Normalization
Recommended startup path:
1. read registrations from `WorkflowRegistrationCatalog`
2. compile each workflow to canonical definition
3. validate canonical definition
4. convert to `WorkflowRuntimeDefinition`
5. store in immutable in-memory cache
This startup step should be implemented once and reused by:
- runtime execution
- canonical inspection endpoints
- diagnostics
## 5. Snapshot Model
Recommended runtime snapshot record:
```csharp
public sealed record WorkflowRuntimeSnapshot
{
public required string WorkflowInstanceId { get; init; }
public required string WorkflowName { get; init; }
public required string WorkflowVersion { get; init; }
public required string RuntimeProvider { get; init; }
public required long Version { get; init; }
public WorkflowBusinessReference? BusinessReference { get; init; }
public required string RuntimeStatus { get; init; }
public required WorkflowEngineState EngineState { get; init; }
public DateTime CreatedOnUtc { get; init; }
public DateTime? CompletedOnUtc { get; init; }
public DateTime LastUpdatedOnUtc { get; init; }
}
```
## 6. AQ Adapter Design
AQ adapters should be isolated behind backend-neutral interfaces.
Do not let the rest of the engine know about:
- queue table names
- enqueue option types
- dequeue option types
- AQ-specific exception types
That isolation is the main swap seam for any future non-AQ backend.
## 7. Transaction Boundary Design
### 7.1 Coordinator Owns Transactions
`WorkflowExecutionCoordinator` should own the unit of work for:
- snapshot update
- projection update
- AQ publish
- AQ dequeue completion
This avoids split responsibility across product services and engine helpers.
### 7.2 Projection Store Remains Focused
`WorkflowProjectionStore` should stay focused on:
- read projection writes
- query paths
- task event history
It should not become the coordinator for AQ or engine versioning.
## 8. Startup Composition
`WorkflowServiceCollectionExtensions` should eventually compose the engine roughly like this:
```csharp
services.Configure<WorkflowRuntimeOptions>(...);
services.Configure<WorkflowEngineOptions>(...);
services.Configure<WorkflowAqOptions>(...);
services.AddScoped<IWorkflowRuntimeProvider, SerdicaEngineRuntimeProvider>();
services.AddScoped<IWorkflowRuntimeOrchestrator, WorkflowRuntimeOrchestrator>();
services.AddScoped<IWorkflowRuntimeSnapshotStore, OracleWorkflowRuntimeSnapshotStore>();
services.AddScoped<IWorkflowSignalBus, OracleAqWorkflowSignalBus>();
services.AddScoped<IWorkflowScheduleBus, OracleAqWorkflowScheduleBus>();
services.AddSingleton<IWorkflowRuntimeDefinitionStore, WorkflowRuntimeDefinitionStore>();
services.AddHostedService<WorkflowEngineSignalHostedService>();
```
## 9. Avoided Anti-Patterns
The implementation should explicitly avoid:
- a giant engine service that knows everything
- polling tables for due work
- in-memory only timer ownership
- transport-specific engine branches scattered across the codebase
- storing huge snapshots in AQ messages
- mixing public contracts with engine internal contracts
## 10. Implementation Rules
1. Put backend-specific code behind an interface.
2. Keep canonical interpretation pure and backend-agnostic.
3. Keep Oracle transaction handling close to the execution coordinator.
4. Make resume idempotency part of the snapshot model, not a side utility.
5. Keep projection writes product-oriented, not runtime-oriented.