Skip to content

Commit 3759ad6

Browse files
committed
Restart not-running flows from FlowsManagers.Push and deliver in-hand messages
When fetched messages target flows that are not currently running on this replica, route them through RestartExecutions instead of dropping them: - FlowsManagers.Push now materializes the not-running messages and forwards them to RestartExecutions, which claims the flows for this replica and resumes them with the in-hand messages. Messages for a stored type not registered on this replica are reported via the UnhandledExceptionHandler (TODO: downgrade to a warning once an ILogger abstraction exists). - FunctionsRegistry passes the UnhandledExceptionHandler into FlowsManagers. - Invoker pushes the in-hand messages straight into the queue manager on restart so the flow does not re-fetch them from the store. - QueueManager.Push initializes the queue manager first so the idempotency-key state is loaded before the pushed messages are processed.
1 parent e18b535 commit 3759ad6

4 files changed

Lines changed: 58 additions & 7 deletions

File tree

Core/Cleipnir.ResilientFunctions/CoreRuntime/FlowsManagers.cs

Lines changed: 44 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
using System.Threading.Tasks;
55
using Cleipnir.ResilientFunctions.CoreRuntime.Watchdogs;
66
using Cleipnir.ResilientFunctions.Domain;
7+
using Cleipnir.ResilientFunctions.Domain.Exceptions;
78
using Cleipnir.ResilientFunctions.Messaging;
89
using Cleipnir.ResilientFunctions.Storage;
910

@@ -22,14 +23,16 @@ public class FlowsManagers
2223
private readonly IFunctionStore _functionStore;
2324
private readonly MessageClearer _messageClearer;
2425
private readonly ClusterInfo _clusterInfo;
26+
private readonly UnhandledExceptionHandler _unhandledExceptionHandler;
2527

2628
private readonly Lock _lock = new();
2729

28-
internal FlowsManagers(IFunctionStore functionStore, MessageClearer messageClearer, ClusterInfo clusterInfo)
30+
internal FlowsManagers(IFunctionStore functionStore, MessageClearer messageClearer, ClusterInfo clusterInfo, UnhandledExceptionHandler unhandledExceptionHandler)
2931
{
3032
_functionStore = functionStore;
3133
_messageClearer = messageClearer;
3234
_clusterInfo = clusterInfo;
35+
_unhandledExceptionHandler = unhandledExceptionHandler;
3336
}
3437

3538
public FlowsManager GetOrCreate(StoredType storedType)
@@ -52,10 +55,12 @@ public FlowsManager GetOrCreate(StoredType storedType)
5255
public Task Push(IReadOnlyList<StoredMessages> messages)
5356
{
5457
List<Task> messageDeliveries;
58+
List<StoredMessages> notRunning;
5559
lock (_lock)
5660
{
57-
var notRunning = messages
58-
.Where(msg => !_managers.ContainsKey(msg.StoredId.Type));
61+
notRunning = messages
62+
.Where(msg => !_managers.ContainsKey(msg.StoredId.Type))
63+
.ToList();
5964

6065
var running = messages
6166
.Where(msg => _managers.ContainsKey(msg.StoredId.Type))
@@ -65,10 +70,45 @@ public Task Push(IReadOnlyList<StoredMessages> messages)
6570

6671
messageDeliveries = running;
6772
}
68-
73+
74+
messageDeliveries.Add(RestartExecutions(notRunning));
75+
6976
return Task.WhenAll(messageDeliveries);
7077
}
7178

79+
/// <summary>
80+
/// Routes restart-and-deliver requests to each flow type's manager: groups the messages by
81+
/// <see cref="StoredId.Type"/> and forwards each group to the matching <see cref="FlowsManager.RestartExecutions"/>,
82+
/// which claims the not-yet-owned flows for this replica and resumes them with the in-hand messages. Messages for
83+
/// a type not registered on this replica are reported to the unhandled-exception handler: this replica fetched
84+
/// them (it owns or published them) yet has no flow of that type to restart, which signals a deployment mismatch.
85+
/// </summary>
86+
public Task RestartExecutions(IReadOnlyList<StoredMessages> messages)
87+
{
88+
var restarts = new List<Task>();
89+
foreach (var group in messages.GroupBy(msg => msg.StoredId.Type))
90+
{
91+
if (TryGet(group.Key) is { } manager)
92+
{
93+
restarts.Add(manager.RestartExecutions(group.ToList()));
94+
continue;
95+
}
96+
97+
var unrunnable = group.ToList();
98+
// TODO: this is an expected condition during deployments, not a framework fault - it should be logged as a
99+
// warning rather than surfaced through the UnhandledExceptionHandler. Revisit once the framework has an
100+
// ILogger abstraction.
101+
_unhandledExceptionHandler.Invoke(
102+
new FrameworkException(
103+
$"Fetched messages for {unrunnable.Count} flow(s) of stored type '{group.Key.Value}' which is " +
104+
$"not registered on this replica - the targeted flow(s) cannot be restarted here"
105+
)
106+
);
107+
}
108+
109+
return Task.WhenAll(restarts);
110+
}
111+
72112
public IReadOnlyList<StoredId> FilterOwned(IEnumerable<StoredId> ids)
73113
{
74114
var owned = new List<StoredId>();

Core/Cleipnir.ResilientFunctions/CoreRuntime/Invocation/Invoker.cs

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -337,6 +337,12 @@ private async Task<PreparedReInvocation> PrepareForReInvocation(StoredId storedI
337337

338338
var queueManager = _invocationHelper.CreateQueueManager(flowId, storedId, effect, flowState, flowTimeouts, _unhandledExceptionHandler);
339339
disposables.Add(queueManager);
340+
341+
// Deliver the in-hand messages handed over by the restart straight into the queue manager's pipeline so
342+
// the flow does not have to re-fetch them from the store. Push initializes the queue manager first, which
343+
// loads the idempotency-key state before these messages are processed.
344+
await queueManager.Push(storedMessages);
345+
340346
var messageWriter = _invocationHelper.CreateMessageWriter(storedId);
341347

342348
var workflow = new Workflow(

Core/Cleipnir.ResilientFunctions/FunctionsRegistry.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -50,7 +50,7 @@ public FunctionsRegistry(IFunctionStore functionStore, Settings? settings = null
5050
);
5151
ClusterInfo = new ClusterInfo(ReplicaId.NewId());
5252

53-
_flowsManagers = new FlowsManagers(_functionStore, _messageClearer, ClusterInfo);
53+
_flowsManagers = new FlowsManagers(_functionStore, _messageClearer, ClusterInfo, _settings.UnhandledExceptionHandler);
5454

5555
_postponedWatchdog = new PostponedWatchdog(
5656
_functionStore,

Core/Cleipnir.ResilientFunctions/Queuing/QueueManager.cs

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -122,14 +122,19 @@ internal void Interrupt()
122122
}
123123

124124
/// <summary>
125-
/// Pushes messages fetched elsewhere (the MessageWatchdog) straight into the delivery pipeline,
126-
/// avoiding a per-flow re-fetch. Idempotent: positions already processed are skipped by ProcessMessages.
125+
/// Pushes messages fetched elsewhere (the MessageWatchdog, or the in-hand messages handed over on restart)
126+
/// straight into the delivery pipeline, avoiding a per-flow re-fetch. Ensures the queue manager is initialized
127+
/// first so the idempotency-key state is loaded before the messages are processed. Idempotent: positions
128+
/// already processed are skipped by ProcessMessages.
127129
/// </summary>
128130
public async Task Push(IReadOnlyList<StoredMessage> messages)
129131
{
130132
if (_disposed || messages.Count == 0)
131133
return;
132134

135+
if (!_initialized)
136+
await Initialize();
137+
133138
await _fetchSemaphore.WaitAsync();
134139
try
135140
{

0 commit comments

Comments
 (0)