| | | 1 | | namespace Elsa.Bpmn.Hosting; |
| | | 2 | | |
| | | 3 | | /// <summary> |
| | | 4 | | /// Runs BPMN scope evaluations one at a time, in arrival order, for one workflow instance. |
| | | 5 | | /// </summary> |
| | | 6 | | /// <remarks> |
| | | 7 | | /// <para> |
| | | 8 | | /// A nested scope can terminalize while its parent is still applying a command list, and a scope raising an |
| | | 9 | | /// escalation delivers it to its parent from inside its own command loop. Either way the parent must not be |
| | | 10 | | /// evaluated on the spot: doing so re-enters the interpreter with a half-applied live-work set and an evaluation |
| | | 11 | | /// already in flight. Posting instead of calling is what keeps that from happening — a post made while the queue is |
| | | 12 | | /// draining is appended and picked up once the evaluation in flight has fully applied. |
| | | 13 | | /// </para> |
| | | 14 | | /// <para> |
| | | 15 | | /// One queue serves every scope in the instance, which is what makes the ordering total rather than per-scope. |
| | | 16 | | /// It lives in the workflow execution context's transient properties: it coordinates a single burst of execution |
| | | 17 | | /// and has nothing to persist. |
| | | 18 | | /// </para> |
| | | 19 | | /// </remarks> |
| | | 20 | | internal sealed class BpmnScopeDispatcher |
| | | 21 | | { |
| | 58 | 22 | | private readonly Queue<Func<ValueTask>> _queue = new(); |
| | | 23 | | |
| | | 24 | | private bool _draining; |
| | | 25 | | |
| | | 26 | | /// <summary>Whether an evaluation is in flight, so a post will be queued rather than run.</summary> |
| | 1 | 27 | | public bool IsDraining => _draining; |
| | | 28 | | |
| | | 29 | | /// <summary> |
| | | 30 | | /// Queues an evaluation and, unless one is already in flight, drains the queue to exhaustion. |
| | | 31 | | /// </summary> |
| | | 32 | | public async ValueTask PostAsync(Func<ValueTask> evaluation) |
| | | 33 | | { |
| | 142 | 34 | | _queue.Enqueue(evaluation); |
| | | 35 | | |
| | 142 | 36 | | if (_draining) |
| | 15 | 37 | | return; |
| | | 38 | | |
| | 127 | 39 | | _draining = true; |
| | | 40 | | |
| | | 41 | | try |
| | | 42 | | { |
| | 265 | 43 | | while (_queue.Count > 0) |
| | 141 | 44 | | await _queue.Dequeue()(); |
| | 124 | 45 | | } |
| | 3 | 46 | | catch |
| | | 47 | | { |
| | | 48 | | // An evaluation that threw may have applied part of a command list, so everything queued behind it was |
| | | 49 | | // computed against a state that no longer describes this instance. Dropping the rest is the conservative |
| | | 50 | | // direction: the failure is on its way to the incident strategy, and resuming half-planned work would |
| | | 51 | | // bury it under activity nobody asked for. |
| | 3 | 52 | | _queue.Clear(); |
| | 3 | 53 | | throw; |
| | | 54 | | } |
| | | 55 | | finally |
| | | 56 | | { |
| | 127 | 57 | | _draining = false; |
| | | 58 | | } |
| | 139 | 59 | | } |
| | | 60 | | } |