feat(workflow): 14-day beoordeling escalation timer + hosted worker (S-14, refs #15)
Add a non-interrupting P14D boundary timer on the Beoordelen user task that fires an external-worker task (BeoordelingEscaleren) to the escalation end; wire the hosted BeoordelingEscalatiePump and register the escalation client/processor in DI. On timeout the still-open beoordeling is reassigned to teamlead (ADR-0015). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -21,6 +21,7 @@ builder.Services.AddHttpClient<FlowableWorkflowClient>();
|
|||||||
builder.Services.AddTransient<IWorkflowClient>(sp => sp.GetRequiredService<FlowableWorkflowClient>());
|
builder.Services.AddTransient<IWorkflowClient>(sp => sp.GetRequiredService<FlowableWorkflowClient>());
|
||||||
builder.Services.AddTransient<IExternalWorkerClient>(sp => sp.GetRequiredService<FlowableWorkflowClient>());
|
builder.Services.AddTransient<IExternalWorkerClient>(sp => sp.GetRequiredService<FlowableWorkflowClient>());
|
||||||
builder.Services.AddTransient<IUserTaskClient>(sp => sp.GetRequiredService<FlowableWorkflowClient>());
|
builder.Services.AddTransient<IUserTaskClient>(sp => sp.GetRequiredService<FlowableWorkflowClient>());
|
||||||
|
builder.Services.AddTransient<IBeoordelingEscalatieClient>(sp => sp.GetRequiredService<FlowableWorkflowClient>());
|
||||||
builder.Services.AddHttpClient<IAclClient, AclHttpClient>();
|
builder.Services.AddHttpClient<IAclClient, AclHttpClient>();
|
||||||
|
|
||||||
builder.Services.AddScoped<SubmitRegistration>();
|
builder.Services.AddScoped<SubmitRegistration>();
|
||||||
@@ -30,9 +31,13 @@ builder.Services.AddScoped<WithdrawRegistration>();
|
|||||||
builder.Services.AddScoped<Werkbak>();
|
builder.Services.AddScoped<Werkbak>();
|
||||||
builder.Services.AddScoped<OpenZaakWorker>();
|
builder.Services.AddScoped<OpenZaakWorker>();
|
||||||
builder.Services.AddScoped<OpenZaakJobProcessor>();
|
builder.Services.AddScoped<OpenZaakJobProcessor>();
|
||||||
|
builder.Services.AddScoped<BeoordelingEscalatieProcessor>();
|
||||||
|
|
||||||
// The hosted external-task job worker polls Flowable and drives OpenZaakAanmaken to completion.
|
// The hosted external-task job worker polls Flowable and drives OpenZaakAanmaken to completion.
|
||||||
builder.Services.AddHostedService<OpenZaakJobPump>();
|
builder.Services.AddHostedService<OpenZaakJobPump>();
|
||||||
|
// The escalation worker polls the BeoordelingEscaleren jobs the 14-day timer parks and reassigns
|
||||||
|
// each overdue beoordeling to the teamlead (S-14).
|
||||||
|
builder.Services.AddHostedService<BeoordelingEscalatiePump>();
|
||||||
|
|
||||||
var app = builder.Build();
|
var app = builder.Build();
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,49 @@
|
|||||||
|
using Microsoft.Extensions.DependencyInjection;
|
||||||
|
using Microsoft.Extensions.Hosting;
|
||||||
|
using Microsoft.Extensions.Logging;
|
||||||
|
|
||||||
|
namespace Big.Infrastructure;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The hosted polling loop of the beoordeling-escalation worker (S-14, ADR-0015): on an interval it
|
||||||
|
/// resolves a scoped <see cref="BeoordelingEscalatieProcessor"/> and asks it to drain the parked
|
||||||
|
/// <c>BeoordelingEscaleren</c> jobs. A deliberately thin shell — all acquire/reassign/complete logic
|
||||||
|
/// lives in the processor, which is unit-tested; this class only owns the timer, the per-tick scope,
|
||||||
|
/// and loop resilience. Structurally identical to <see cref="OpenZaakJobPump"/>.
|
||||||
|
/// </summary>
|
||||||
|
public sealed class BeoordelingEscalatiePump(
|
||||||
|
IServiceScopeFactory scopeFactory,
|
||||||
|
FlowableOptions options,
|
||||||
|
ILogger<BeoordelingEscalatiePump> logger) : BackgroundService
|
||||||
|
{
|
||||||
|
protected override async Task ExecuteAsync(CancellationToken stoppingToken)
|
||||||
|
{
|
||||||
|
while (!stoppingToken.IsCancellationRequested)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
using var scope = scopeFactory.CreateScope();
|
||||||
|
var processor = scope.ServiceProvider.GetRequiredService<BeoordelingEscalatieProcessor>();
|
||||||
|
await processor.PumpOnceAsync(options.MaxJobsPerPoll, stoppingToken);
|
||||||
|
}
|
||||||
|
catch (OperationCanceledException) when (stoppingToken.IsCancellationRequested)
|
||||||
|
{
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
// A transient fault (e.g. Flowable briefly unreachable) must not kill the loop.
|
||||||
|
logger.LogError(ex, "BeoordelingEscaleren job poll failed; retrying after the poll interval.");
|
||||||
|
}
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
await Task.Delay(options.PollInterval, stoppingToken);
|
||||||
|
}
|
||||||
|
catch (OperationCanceledException) when (stoppingToken.IsCancellationRequested)
|
||||||
|
{
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -15,7 +15,12 @@
|
|||||||
S-11 adds withdrawal: while parked at Beoordelen the citizen can trek de aanvraag in — an
|
S-11 adds withdrawal: while parked at Beoordelen the citizen can trek de aanvraag in — an
|
||||||
interrupting message boundary event (RegistratieIngetrokken) cancels the task and ends the
|
interrupting message boundary event (RegistratieIngetrokken) cancels the task and ends the
|
||||||
process via a dedicated "ingetrokken" end (ADR-0014). The Workflow Client delivers the message
|
process via a dedicated "ingetrokken" end (ADR-0014). The Workflow Client delivers the message
|
||||||
to the task's execution; the BPMN owns the cancellation path. -->
|
to the task's execution; the BPMN owns the cancellation path.
|
||||||
|
S-14 adds escalation: a NON-interrupting boundary timer (P14D) on Beoordelen. If a behandelaar
|
||||||
|
has not picked the task up within 14 days it fires a parallel token to an external-worker task
|
||||||
|
(BeoordelingEscaleren); the Workflow Client reassigns the still-open Beoordelen task from the
|
||||||
|
behandelaar group to teamlead (ADR-0015). The Beoordelen task stays open throughout — the timer
|
||||||
|
only changes who may claim it. -->
|
||||||
<message id="Message_Ingetrokken" name="RegistratieIngetrokken"/>
|
<message id="Message_Ingetrokken" name="RegistratieIngetrokken"/>
|
||||||
|
|
||||||
<process id="registratie" name="Registratie ontvangen" isExecutable="true">
|
<process id="registratie" name="Registratie ontvangen" isExecutable="true">
|
||||||
@@ -45,6 +50,25 @@
|
|||||||
<sequenceFlow id="flow4" sourceRef="Ingetrokken" targetRef="endIngetrokken"/>
|
<sequenceFlow id="flow4" sourceRef="Ingetrokken" targetRef="endIngetrokken"/>
|
||||||
|
|
||||||
<endEvent id="endIngetrokken" name="Registratie ingetrokken"/>
|
<endEvent id="endIngetrokken" name="Registratie ingetrokken"/>
|
||||||
|
|
||||||
|
<!-- Escalation (S-14): non-interrupting P14D boundary timer on Beoordelen. On timeout a parallel
|
||||||
|
token runs EscaleerBeoordeling, an external-worker task the Workflow Client picks up to
|
||||||
|
reassign the still-open Beoordelen task from behandelaar to teamlead (ADR-0015). -->
|
||||||
|
<boundaryEvent id="EscaleerTimer" attachedToRef="Beoordelen" cancelActivity="false">
|
||||||
|
<timerEventDefinition>
|
||||||
|
<timeDuration>P14D</timeDuration>
|
||||||
|
</timerEventDefinition>
|
||||||
|
</boundaryEvent>
|
||||||
|
|
||||||
|
<sequenceFlow id="flow5" sourceRef="EscaleerTimer" targetRef="EscaleerBeoordeling"/>
|
||||||
|
|
||||||
|
<serviceTask id="EscaleerBeoordeling" name="Beoordeling escaleren"
|
||||||
|
flowable:type="external-worker"
|
||||||
|
flowable:topic="BeoordelingEscaleren"/>
|
||||||
|
|
||||||
|
<sequenceFlow id="flow6" sourceRef="EscaleerBeoordeling" targetRef="endEscaleren"/>
|
||||||
|
|
||||||
|
<endEvent id="endEscaleren" name="Beoordeling geëscaleerd"/>
|
||||||
</process>
|
</process>
|
||||||
|
|
||||||
<bpmndi:BPMNDiagram id="diagram">
|
<bpmndi:BPMNDiagram id="diagram">
|
||||||
@@ -83,6 +107,24 @@
|
|||||||
<omgdi:waypoint x="450" y="165"/>
|
<omgdi:waypoint x="450" y="165"/>
|
||||||
<omgdi:waypoint x="450" y="220"/>
|
<omgdi:waypoint x="450" y="220"/>
|
||||||
</bpmndi:BPMNEdge>
|
</bpmndi:BPMNEdge>
|
||||||
|
<bpmndi:BPMNShape id="s_escaleerTimer" bpmnElement="EscaleerTimer">
|
||||||
|
<omgdc:Bounds x="480" y="70" width="30" height="30"/>
|
||||||
|
</bpmndi:BPMNShape>
|
||||||
|
<bpmndi:BPMNShape id="s_escaleerBeoordeling" bpmnElement="EscaleerBeoordeling">
|
||||||
|
<omgdc:Bounds x="580" y="20" width="120" height="60"/>
|
||||||
|
</bpmndi:BPMNShape>
|
||||||
|
<bpmndi:BPMNShape id="s_endEscaleren" bpmnElement="endEscaleren">
|
||||||
|
<omgdc:Bounds x="760" y="35" width="30" height="30"/>
|
||||||
|
</bpmndi:BPMNShape>
|
||||||
|
<bpmndi:BPMNEdge id="e_flow5" bpmnElement="flow5">
|
||||||
|
<omgdi:waypoint x="495" y="70"/>
|
||||||
|
<omgdi:waypoint x="495" y="50"/>
|
||||||
|
<omgdi:waypoint x="580" y="50"/>
|
||||||
|
</bpmndi:BPMNEdge>
|
||||||
|
<bpmndi:BPMNEdge id="e_flow6" bpmnElement="flow6">
|
||||||
|
<omgdi:waypoint x="700" y="50"/>
|
||||||
|
<omgdi:waypoint x="760" y="50"/>
|
||||||
|
</bpmndi:BPMNEdge>
|
||||||
</bpmndi:BPMNPlane>
|
</bpmndi:BPMNPlane>
|
||||||
</bpmndi:BPMNDiagram>
|
</bpmndi:BPMNDiagram>
|
||||||
</definitions>
|
</definitions>
|
||||||
|
|||||||
Reference in New Issue
Block a user