Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
17 changes: 17 additions & 0 deletions src/Altinn.App.Core/Features/IProcessEnd.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
using Altinn.Platform.Storage.Interface.Models;

namespace Altinn.App.Core.Features;

/// <summary>
/// Custom logic to run when the entire process has ended. E.g. when we have arrived at an `endEvent` in the BPMN model
/// </summary>
[ImplementableByApps]
public interface IProcessEnd
{
/// <summary>
/// This method is called when the process has ended
/// </summary>
/// <param name="instance">The instance</param>
/// <param name="events">Events that were dispatched in the last processing step</param>
public Task End(Instance instance, List<InstanceEvent>? events);
}
14 changes: 14 additions & 0 deletions src/Altinn.App.Core/Features/Telemetry/Telemetry.Processes.cs
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,20 @@ internal void ProcessEnded(ProcessStateChange processChange)
return activity;
}

internal Activity? StartProcessEndHandlersActivity(Instance instance)
{
var activity = ActivitySource.StartActivity($"{Prefix}.EndHandlers");
activity?.SetInstanceId(instance);
return activity;
}

internal Activity? StartProcessEndHandlerActivity(Instance instance, IProcessEnd handler)
{
var activity = ActivitySource.StartActivity($"{Prefix}.EndHandler.{handler.GetType()}");
activity?.SetInstanceId(instance);
return activity;
}

internal static class Processes
{
internal const string Prefix = "Process";
Expand Down
48 changes: 40 additions & 8 deletions src/Altinn.App.Core/Internal/Process/ProcessEngine.cs
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
using System.Diagnostics;
using System.Diagnostics.CodeAnalysis;
using Altinn.App.Core.Extensions;
using Altinn.App.Core.Features;
using Altinn.App.Core.Features.Action;
Expand Down Expand Up @@ -30,6 +31,7 @@ public class ProcessEngine : IProcessEngine
private readonly IAuthenticationContext _authenticationContext;
private readonly InstanceDataUnitOfWorkInitializer _instanceDataUnitOfWorkInitializer;
private readonly IProcessTaskCleaner _processTaskCleaner;
private readonly AppImplementationFactory _appImplementationFactory;

/// <summary>
/// Initializes a new instance of the <see cref="ProcessEngine"/> class
Expand All @@ -54,6 +56,7 @@ public ProcessEngine(
_userActionService = userActionService;
_telemetry = telemetry;
_authenticationContext = authenticationContext;
_appImplementationFactory = serviceProvider.GetRequiredService<AppImplementationFactory>();
_instanceDataUnitOfWorkInitializer = serviceProvider.GetRequiredService<InstanceDataUnitOfWorkInitializer>();
}

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

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

ProcessStateChange? nextResult = await HandleMoveToNext(instance, request.Action);
MoveToNextResult moveToNextResult = await HandleMoveToNext(instance, request.Action);

if (nextResult?.NewProcessState?.Ended is not null)
if (moveToNextResult.IsEndEvent)
{
_telemetry?.ProcessEnded(nextResult);
_telemetry?.ProcessEnded(moveToNextResult.ProcessStateChange);
await RunAppDefinedProcessEndHandlers(instance, moveToNextResult.ProcessStateChange?.Events);
}

var changeResult = new ProcessChangeResult() { Success = true, ProcessStateChange = nextResult };
var changeResult = new ProcessChangeResult()
{
Success = true,
ProcessStateChange = moveToNextResult.ProcessStateChange,
};
activity?.SetProcessChangeResult(changeResult);
return changeResult;
}
Expand Down Expand Up @@ -421,18 +429,42 @@ private async Task<InstanceEvent> GenerateProcessChangeEvent(string eventType, I
return instanceEvent;
}

private async Task<ProcessStateChange?> HandleMoveToNext(Instance instance, string? action)
private async Task<MoveToNextResult> HandleMoveToNext(Instance instance, string? action)
{
ProcessStateChange? processStateChange = await ProcessNext(instance, action);

if (processStateChange == null)
if (processStateChange is null)
{
return processStateChange;
return new MoveToNextResult(instance, null);
}

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

return processStateChange;
return new MoveToNextResult(instance, processStateChange);
}

/// <summary>
/// Runs IProcessEnd implementations defined in the app.
/// </summary>
private async Task RunAppDefinedProcessEndHandlers(Instance instance, List<InstanceEvent>? events)
{
var processEnds = _appImplementationFactory.GetAll<IProcessEnd>().ToList();
if (processEnds.Count is 0)
return;

using var mainActivity = _telemetry?.StartProcessEndHandlersActivity(instance);

foreach (IProcessEnd processEnd in processEnds)
{
using var nestedActivity = _telemetry?.StartProcessEndHandlerActivity(instance, processEnd);
await processEnd.End(instance, events);
}
}

private sealed record MoveToNextResult(Instance Instance, ProcessStateChange? ProcessStateChange)
{
[MemberNotNullWhen(true, nameof(ProcessStateChange))]
public bool IsEndEvent => ProcessStateChange?.NewProcessState?.Ended is not null;
};
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
{
Activities: [
{
ActivityName: Process.End,
Tags: [
{
instance.guid: Guid_1
}
],
IdFormat: W3C
},
{
ActivityName: Process.HandleEvents,
Tags: [
{
instance.guid: Guid_1
}
],
IdFormat: W3C
},
{
ActivityName: Process.Next,
Tags: [
{
instance.guid: Guid_1
}
],
IdFormat: W3C,
Status: Ok,
Events: [
{
Name: change,
Timestamp: DateTimeOffset_1,
Tags: [
{
events: [
Type=process_EndTask DataId=,
Type=process_EndEvent DataId=,
Type=Submited DataId=
]
},
{},
{},
{},
{}
]
}
]
},
{
ActivityName: Process.StoreEvents,
Tags: [
{
instance.guid: Guid_1
}
],
IdFormat: W3C
}
],
Metrics: []
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
{
Activities: [
{
ActivityName: Process.End,
Tags: [
{
instance.guid: Guid_1
}
],
IdFormat: W3C
},
{
ActivityName: Process.EndHandler.Castle.Proxies.IProcessEndProxy,
Tags: [
{
instance.guid: Guid_1
}
],
IdFormat: W3C
},
{
ActivityName: Process.EndHandlers,
Tags: [
{
instance.guid: Guid_1
}
],
IdFormat: W3C
},
{
ActivityName: Process.HandleEvents,
Tags: [
{
instance.guid: Guid_1
}
],
IdFormat: W3C
},
{
ActivityName: Process.Next,
Tags: [
{
instance.guid: Guid_1
}
],
IdFormat: W3C,
Status: Ok,
Events: [
{
Name: change,
Timestamp: DateTimeOffset_1,
Tags: [
{
events: [
Type=process_EndTask DataId=,
Type=process_EndEvent DataId=,
Type=Submited DataId=
]
},
{},
{},
{},
{}
]
}
]
},
{
ActivityName: Process.StoreEvents,
Tags: [
{
instance.guid: Guid_1
}
],
IdFormat: W3C
}
],
Metrics: []
}
41 changes: 37 additions & 4 deletions test/Altinn.App.Core.Tests/Internal/Process/ProcessEngineTest.cs
Original file line number Diff line number Diff line change
Expand Up @@ -828,8 +828,11 @@ public async Task Next_moves_instance_to_next_task_and_produces_abandon_instance
);
}

[Fact]
public async Task Next_moves_instance_to_end_event_and_ends_proces()
[Theory]
[InlineData(true, true)]
[InlineData(false, false)]
[InlineData(false, true)]
public async Task Next_moves_instance_to_end_event_and_ends_process(bool registerProcessEnd, bool useTelemetry)
{
var expectedInstance = new Instance()
{
Expand All @@ -844,11 +847,24 @@ public async Task Next_moves_instance_to_end_event_and_ends_proces()
EndEvent = "EndEvent_1",
},
};
using var fixture = Fixture.Create(updatedInstance: expectedInstance);
using var fixture = Fixture.Create(
updatedInstance: expectedInstance,
registerProcessEnd: registerProcessEnd,
withTelemetry: useTelemetry
);
fixture
.Mock<IAppMetadata>()
.Setup(x => x.GetApplicationMetadata())
.ReturnsAsync(new ApplicationMetadata("org/app"));

if (registerProcessEnd)
{
fixture
.Mock<IProcessEnd>()
.Setup(x => x.End(It.IsAny<Instance>(), It.IsAny<List<InstanceEvent>>()))
.Verifiable(Times.Once);
}

ProcessEngine processEngine = fixture.ProcessEngine;
Instance instance = new Instance()
{
Expand Down Expand Up @@ -978,6 +994,18 @@ public async Task Next_moves_instance_to_end_event_and_ends_proces()
d.RegisterEventWithEventsComponent(It.Is<Instance>(i => CompareInstance(expectedInstance, i)))
);

if (registerProcessEnd)
{
fixture.Mock<IProcessEnd>().Verify();
}

if (useTelemetry)
{
var snapshotFilename =
$"ProcessEngineTest.Telemetry.IProcessEnd_{(registerProcessEnd ? "registered" : "not_registered")}.json";
await Verify(fixture.TelemetrySink.GetSnapshot()).UseFileName(snapshotFilename);
}

result.Success.Should().BeTrue();
result
.ProcessStateChange.Should()
Expand Down Expand Up @@ -1103,7 +1131,8 @@ public static Fixture Create(
Instance? updatedInstance = null,
IEnumerable<IUserAction>? userActions = null,
bool withTelemetry = false,
TestJwtToken? token = null
TestJwtToken? token = null,
bool registerProcessEnd = false
)
{
services ??= new ServiceCollection();
Expand Down Expand Up @@ -1203,6 +1232,10 @@ public static Fixture Create(
services.TryAddTransient<IAppMetadata>(_ => appMetadataMock.Object);
services.TryAddTransient<IAppResources>(_ => appResourcesMock.Object);
services.TryAddTransient<InstanceDataUnitOfWorkInitializer>();

if (registerProcessEnd)
services.AddSingleton<IProcessEnd>(_ => new Mock<IProcessEnd>().Object);

services.TryAddTransient<ModelSerializationService>();

foreach (var userAction in userActions ?? [])
Expand Down