Skip to content

Commit 35b534e

Browse files
authored
Add IProcessEnd interface (#1140)
1 parent 2e0df54 commit 35b534e

6 files changed

Lines changed: 248 additions & 12 deletions

File tree

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
using Altinn.Platform.Storage.Interface.Models;
2+
3+
namespace Altinn.App.Core.Features;
4+
5+
/// <summary>
6+
/// Custom logic to run when the entire process has ended. E.g. when we have arrived at an `endEvent` in the BPMN model
7+
/// </summary>
8+
[ImplementableByApps]
9+
public interface IProcessEnd
10+
{
11+
/// <summary>
12+
/// This method is called when the process has ended
13+
/// </summary>
14+
/// <param name="instance">The instance</param>
15+
/// <param name="events">Events that were dispatched in the last processing step</param>
16+
public Task End(Instance instance, List<InstanceEvent>? events);
17+
}

src/Altinn.App.Core/Features/Telemetry/Telemetry.Processes.cs

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -69,6 +69,20 @@ internal void ProcessEnded(ProcessStateChange processChange)
6969
return activity;
7070
}
7171

72+
internal Activity? StartProcessEndHandlersActivity(Instance instance)
73+
{
74+
var activity = ActivitySource.StartActivity($"{Prefix}.EndHandlers");
75+
activity?.SetInstanceId(instance);
76+
return activity;
77+
}
78+
79+
internal Activity? StartProcessEndHandlerActivity(Instance instance, IProcessEnd handler)
80+
{
81+
var activity = ActivitySource.StartActivity($"{Prefix}.EndHandler.{handler.GetType()}");
82+
activity?.SetInstanceId(instance);
83+
return activity;
84+
}
85+
7286
internal static class Processes
7387
{
7488
internal const string Prefix = "Process";

src/Altinn.App.Core/Internal/Process/ProcessEngine.cs

Lines changed: 40 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
using System.Diagnostics;
2+
using System.Diagnostics.CodeAnalysis;
23
using Altinn.App.Core.Extensions;
34
using Altinn.App.Core.Features;
45
using Altinn.App.Core.Features.Action;
@@ -30,6 +31,7 @@ public class ProcessEngine : IProcessEngine
3031
private readonly IAuthenticationContext _authenticationContext;
3132
private readonly InstanceDataUnitOfWorkInitializer _instanceDataUnitOfWorkInitializer;
3233
private readonly IProcessTaskCleaner _processTaskCleaner;
34+
private readonly AppImplementationFactory _appImplementationFactory;
3335

3436
/// <summary>
3537
/// Initializes a new instance of the <see cref="ProcessEngine"/> class
@@ -54,6 +56,7 @@ public ProcessEngine(
5456
_userActionService = userActionService;
5557
_telemetry = telemetry;
5658
_authenticationContext = authenticationContext;
59+
_appImplementationFactory = serviceProvider.GetRequiredService<AppImplementationFactory>();
5760
_instanceDataUnitOfWorkInitializer = serviceProvider.GetRequiredService<InstanceDataUnitOfWorkInitializer>();
5861
}
5962

@@ -197,14 +200,19 @@ public async Task<ProcessChangeResult> Next(ProcessNextRequest request)
197200

198201
// TODO: consider using the same cachedDataMutator for the rest of the process to avoid refetching data from storage
199202

200-
ProcessStateChange? nextResult = await HandleMoveToNext(instance, request.Action);
203+
MoveToNextResult moveToNextResult = await HandleMoveToNext(instance, request.Action);
201204

202-
if (nextResult?.NewProcessState?.Ended is not null)
205+
if (moveToNextResult.IsEndEvent)
203206
{
204-
_telemetry?.ProcessEnded(nextResult);
207+
_telemetry?.ProcessEnded(moveToNextResult.ProcessStateChange);
208+
await RunAppDefinedProcessEndHandlers(instance, moveToNextResult.ProcessStateChange?.Events);
205209
}
206210

207-
var changeResult = new ProcessChangeResult() { Success = true, ProcessStateChange = nextResult };
211+
var changeResult = new ProcessChangeResult()
212+
{
213+
Success = true,
214+
ProcessStateChange = moveToNextResult.ProcessStateChange,
215+
};
208216
activity?.SetProcessChangeResult(changeResult);
209217
return changeResult;
210218
}
@@ -421,18 +429,42 @@ private async Task<InstanceEvent> GenerateProcessChangeEvent(string eventType, I
421429
return instanceEvent;
422430
}
423431

424-
private async Task<ProcessStateChange?> HandleMoveToNext(Instance instance, string? action)
432+
private async Task<MoveToNextResult> HandleMoveToNext(Instance instance, string? action)
425433
{
426434
ProcessStateChange? processStateChange = await ProcessNext(instance, action);
427435

428-
if (processStateChange == null)
436+
if (processStateChange is null)
429437
{
430-
return processStateChange;
438+
return new MoveToNextResult(instance, null);
431439
}
432440

433441
instance = await HandleEventsAndUpdateStorage(instance, null, processStateChange.Events);
434442
await _processEventDispatcher.RegisterEventWithEventsComponent(instance);
435443

436-
return processStateChange;
444+
return new MoveToNextResult(instance, processStateChange);
437445
}
446+
447+
/// <summary>
448+
/// Runs IProcessEnd implementations defined in the app.
449+
/// </summary>
450+
private async Task RunAppDefinedProcessEndHandlers(Instance instance, List<InstanceEvent>? events)
451+
{
452+
var processEnds = _appImplementationFactory.GetAll<IProcessEnd>().ToList();
453+
if (processEnds.Count is 0)
454+
return;
455+
456+
using var mainActivity = _telemetry?.StartProcessEndHandlersActivity(instance);
457+
458+
foreach (IProcessEnd processEnd in processEnds)
459+
{
460+
using var nestedActivity = _telemetry?.StartProcessEndHandlerActivity(instance, processEnd);
461+
await processEnd.End(instance, events);
462+
}
463+
}
464+
465+
private sealed record MoveToNextResult(Instance Instance, ProcessStateChange? ProcessStateChange)
466+
{
467+
[MemberNotNullWhen(true, nameof(ProcessStateChange))]
468+
public bool IsEndEvent => ProcessStateChange?.NewProcessState?.Ended is not null;
469+
};
438470
}
Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
{
2+
Activities: [
3+
{
4+
ActivityName: Process.End,
5+
Tags: [
6+
{
7+
instance.guid: Guid_1
8+
}
9+
],
10+
IdFormat: W3C
11+
},
12+
{
13+
ActivityName: Process.HandleEvents,
14+
Tags: [
15+
{
16+
instance.guid: Guid_1
17+
}
18+
],
19+
IdFormat: W3C
20+
},
21+
{
22+
ActivityName: Process.Next,
23+
Tags: [
24+
{
25+
instance.guid: Guid_1
26+
}
27+
],
28+
IdFormat: W3C,
29+
Status: Ok,
30+
Events: [
31+
{
32+
Name: change,
33+
Timestamp: DateTimeOffset_1,
34+
Tags: [
35+
{
36+
events: [
37+
Type=process_EndTask DataId=,
38+
Type=process_EndEvent DataId=,
39+
Type=Submited DataId=
40+
]
41+
},
42+
{},
43+
{},
44+
{},
45+
{}
46+
]
47+
}
48+
]
49+
},
50+
{
51+
ActivityName: Process.StoreEvents,
52+
Tags: [
53+
{
54+
instance.guid: Guid_1
55+
}
56+
],
57+
IdFormat: W3C
58+
}
59+
],
60+
Metrics: []
61+
}
Lines changed: 79 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,79 @@
1+
{
2+
Activities: [
3+
{
4+
ActivityName: Process.End,
5+
Tags: [
6+
{
7+
instance.guid: Guid_1
8+
}
9+
],
10+
IdFormat: W3C
11+
},
12+
{
13+
ActivityName: Process.EndHandler.Castle.Proxies.IProcessEndProxy,
14+
Tags: [
15+
{
16+
instance.guid: Guid_1
17+
}
18+
],
19+
IdFormat: W3C
20+
},
21+
{
22+
ActivityName: Process.EndHandlers,
23+
Tags: [
24+
{
25+
instance.guid: Guid_1
26+
}
27+
],
28+
IdFormat: W3C
29+
},
30+
{
31+
ActivityName: Process.HandleEvents,
32+
Tags: [
33+
{
34+
instance.guid: Guid_1
35+
}
36+
],
37+
IdFormat: W3C
38+
},
39+
{
40+
ActivityName: Process.Next,
41+
Tags: [
42+
{
43+
instance.guid: Guid_1
44+
}
45+
],
46+
IdFormat: W3C,
47+
Status: Ok,
48+
Events: [
49+
{
50+
Name: change,
51+
Timestamp: DateTimeOffset_1,
52+
Tags: [
53+
{
54+
events: [
55+
Type=process_EndTask DataId=,
56+
Type=process_EndEvent DataId=,
57+
Type=Submited DataId=
58+
]
59+
},
60+
{},
61+
{},
62+
{},
63+
{}
64+
]
65+
}
66+
]
67+
},
68+
{
69+
ActivityName: Process.StoreEvents,
70+
Tags: [
71+
{
72+
instance.guid: Guid_1
73+
}
74+
],
75+
IdFormat: W3C
76+
}
77+
],
78+
Metrics: []
79+
}

test/Altinn.App.Core.Tests/Internal/Process/ProcessEngineTest.cs

Lines changed: 37 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -828,8 +828,11 @@ public async Task Next_moves_instance_to_next_task_and_produces_abandon_instance
828828
);
829829
}
830830

831-
[Fact]
832-
public async Task Next_moves_instance_to_end_event_and_ends_proces()
831+
[Theory]
832+
[InlineData(true, true)]
833+
[InlineData(false, false)]
834+
[InlineData(false, true)]
835+
public async Task Next_moves_instance_to_end_event_and_ends_process(bool registerProcessEnd, bool useTelemetry)
833836
{
834837
var expectedInstance = new Instance()
835838
{
@@ -844,11 +847,24 @@ public async Task Next_moves_instance_to_end_event_and_ends_proces()
844847
EndEvent = "EndEvent_1",
845848
},
846849
};
847-
using var fixture = Fixture.Create(updatedInstance: expectedInstance);
850+
using var fixture = Fixture.Create(
851+
updatedInstance: expectedInstance,
852+
registerProcessEnd: registerProcessEnd,
853+
withTelemetry: useTelemetry
854+
);
848855
fixture
849856
.Mock<IAppMetadata>()
850857
.Setup(x => x.GetApplicationMetadata())
851858
.ReturnsAsync(new ApplicationMetadata("org/app"));
859+
860+
if (registerProcessEnd)
861+
{
862+
fixture
863+
.Mock<IProcessEnd>()
864+
.Setup(x => x.End(It.IsAny<Instance>(), It.IsAny<List<InstanceEvent>>()))
865+
.Verifiable(Times.Once);
866+
}
867+
852868
ProcessEngine processEngine = fixture.ProcessEngine;
853869
Instance instance = new Instance()
854870
{
@@ -978,6 +994,18 @@ public async Task Next_moves_instance_to_end_event_and_ends_proces()
978994
d.RegisterEventWithEventsComponent(It.Is<Instance>(i => CompareInstance(expectedInstance, i)))
979995
);
980996

997+
if (registerProcessEnd)
998+
{
999+
fixture.Mock<IProcessEnd>().Verify();
1000+
}
1001+
1002+
if (useTelemetry)
1003+
{
1004+
var snapshotFilename =
1005+
$"ProcessEngineTest.Telemetry.IProcessEnd_{(registerProcessEnd ? "registered" : "not_registered")}.json";
1006+
await Verify(fixture.TelemetrySink.GetSnapshot()).UseFileName(snapshotFilename);
1007+
}
1008+
9811009
result.Success.Should().BeTrue();
9821010
result
9831011
.ProcessStateChange.Should()
@@ -1103,7 +1131,8 @@ public static Fixture Create(
11031131
Instance? updatedInstance = null,
11041132
IEnumerable<IUserAction>? userActions = null,
11051133
bool withTelemetry = false,
1106-
TestJwtToken? token = null
1134+
TestJwtToken? token = null,
1135+
bool registerProcessEnd = false
11071136
)
11081137
{
11091138
services ??= new ServiceCollection();
@@ -1203,6 +1232,10 @@ public static Fixture Create(
12031232
services.TryAddTransient<IAppMetadata>(_ => appMetadataMock.Object);
12041233
services.TryAddTransient<IAppResources>(_ => appResourcesMock.Object);
12051234
services.TryAddTransient<InstanceDataUnitOfWorkInitializer>();
1235+
1236+
if (registerProcessEnd)
1237+
services.AddSingleton<IProcessEnd>(_ => new Mock<IProcessEnd>().Object);
1238+
12061239
services.TryAddTransient<ModelSerializationService>();
12071240

12081241
foreach (var userAction in userActions ?? [])

0 commit comments

Comments
 (0)