11using System ;
2+ using System . Linq ;
23using System . Threading . Tasks ;
34using Cleipnir . ResilientFunctions . CoreRuntime . Invocation ;
5+ using Cleipnir . ResilientFunctions . CoreRuntime . Serialization ;
46using Cleipnir . ResilientFunctions . Domain ;
57using Cleipnir . ResilientFunctions . Domain . Exceptions ;
68using Cleipnir . ResilientFunctions . Helpers ;
9+ using Cleipnir . ResilientFunctions . Messaging ;
710using Cleipnir . ResilientFunctions . Storage ;
811using Cleipnir . ResilientFunctions . Tests . Utils ;
912using Shouldly ;
@@ -114,8 +117,293 @@ await BusyWait.Until(async () =>
114117 controlPanel . Result . ShouldNotBeNull ( ) ;
115118 var functionCompletion = controlPanel . Result ;
116119 functionCompletion . ShouldBe ( "hello world" ) ;
117-
120+
118121 unhandledExceptionHandler . ShouldNotHaveExceptions ( ) ;
119122 }
120123
124+ public abstract Task EmptyMessagesRestartSuspendedFlowsWithoutDeliveryAndAreRemovedAfterwards ( ) ;
125+ public async Task EmptyMessagesRestartSuspendedFlowsWithoutDeliveryAndAreRemovedAfterwards ( Task < IFunctionStore > functionStore )
126+ {
127+ var store = await functionStore ;
128+
129+ var flowType = TestFlowId . Create ( ) . Type ;
130+ var unhandledExceptionHandler = new UnhandledExceptionCatcher ( ) ;
131+ using var functionsRegistry = new FunctionsRegistry (
132+ store ,
133+ new Settings ( unhandledExceptionHandler . Catch , messagesPullFrequency : TimeSpan . FromMilliseconds ( 100 ) )
134+ ) ;
135+
136+ var invocations = new SyncedCounter ( ) ;
137+ var registration = functionsRegistry . RegisterParamless (
138+ flowType ,
139+ inner : async Task ( workflow ) =>
140+ {
141+ invocations . Increment ( ) ;
142+ await workflow . Message < string > ( ) ;
143+ }
144+ ) ;
145+
146+ await registration . Schedule ( "instance1" ) ;
147+ await registration . Schedule ( "instance2" ) ;
148+
149+ var controlPanel1 = await registration . ControlPanel ( "instance1" ) ;
150+ var controlPanel2 = await registration . ControlPanel ( "instance2" ) ;
151+ controlPanel1 . ShouldNotBeNull ( ) ;
152+ controlPanel2 . ShouldNotBeNull ( ) ;
153+ await controlPanel1 . BusyWaitUntil ( c => c . Status == Status . Suspended ) ;
154+ await controlPanel2 . BusyWaitUntil ( c => c . Status == Status . Suspended ) ;
155+ invocations . Current . ShouldBe ( 2 ) ;
156+
157+ var storedId1 = registration . MapToStoredId ( "instance1" . ToFlowInstance ( ) ) ;
158+ var storedId2 = registration . MapToStoredId ( "instance2" . ToFlowInstance ( ) ) ;
159+ var replicaId = functionsRegistry . ClusterInfo . ReplicaId ;
160+ await store . MessageStore . AppendMessages ( [
161+ new StoredIdAndMessage ( storedId1 , StoredMessage . CreateEmpty ( replicaId ) ) ,
162+ new StoredIdAndMessage ( storedId2 , StoredMessage . CreateEmpty ( replicaId ) )
163+ ] ) ;
164+
165+ // The empty messages restart both flows without being delivered...
166+ await BusyWait . Until ( ( ) => invocations . Current == 4 ) ;
167+
168+ // ...so both flows suspend on the awaited message again...
169+ await controlPanel1 . BusyWaitUntil ( c => c . Status == Status . Suspended ) ;
170+ await controlPanel2 . BusyWaitUntil ( c => c . Status == Status . Suspended ) ;
171+
172+ // ...and the empty messages are deleted from the store once the restarts have happened.
173+ await BusyWait . Until ( async ( ) =>
174+ ( await store . MessageStore . GetMessages ( storedId1 ) ) . Count == 0 &&
175+ ( await store . MessageStore . GetMessages ( storedId2 ) ) . Count == 0
176+ ) ;
177+
178+ invocations . Current . ShouldBe ( 4 ) ;
179+ unhandledExceptionHandler . ShouldNotHaveExceptions ( ) ;
180+ }
181+
182+ public abstract Task EmptyMessageIsNotDeliveredToRestartedFlowWhileNonEmptyMessageIs ( ) ;
183+ public async Task EmptyMessageIsNotDeliveredToRestartedFlowWhileNonEmptyMessageIs ( Task < IFunctionStore > functionStore )
184+ {
185+ var store = await functionStore ;
186+
187+ var flowId = TestFlowId . Create ( ) ;
188+ var unhandledExceptionHandler = new UnhandledExceptionCatcher ( ) ;
189+ using var functionsRegistry = new FunctionsRegistry (
190+ store ,
191+ new Settings ( unhandledExceptionHandler . Catch , messagesPullFrequency : TimeSpan . FromMilliseconds ( 100 ) )
192+ ) ;
193+
194+ var registration = functionsRegistry . RegisterFunc (
195+ flowId . Type ,
196+ inner : async Task < string > ( string _ , Workflow workflow ) => await workflow . Message < string > ( )
197+ ) ;
198+
199+ await registration . Schedule ( flowId . Instance . Value , "" ) ;
200+
201+ var controlPanel = await registration . ControlPanel ( flowId . Instance ) ;
202+ controlPanel . ShouldNotBeNull ( ) ;
203+ await controlPanel . BusyWaitUntil ( c => c . Status == Status . Suspended ) ;
204+
205+ var storedId = registration . MapToStoredId ( flowId . Instance ) ;
206+ var replicaId = functionsRegistry . ClusterInfo . ReplicaId ;
207+ var serializer = DefaultSerializer . Instance ;
208+ await store . MessageStore . AppendMessages ( [
209+ new StoredIdAndMessage ( storedId , StoredMessage . CreateEmpty ( replicaId ) ) ,
210+ new StoredIdAndMessage (
211+ storedId ,
212+ new StoredMessage (
213+ serializer . Serialize ( "hello world" , typeof ( string ) ) ,
214+ serializer . SerializeType ( typeof ( string ) ) ,
215+ Position : 0 ,
216+ Replica : replicaId
217+ )
218+ )
219+ ] ) ;
220+
221+ // The batch restarts the flow with only the non-empty message delivered.
222+ await controlPanel . WaitForCompletion ( allowPostponeAndSuspended : true ) ;
223+ await controlPanel . Refresh ( ) ;
224+ controlPanel . Status . ShouldBe ( Status . Succeeded ) ;
225+ controlPanel . Result . ShouldBe ( "hello world" ) ;
226+
227+ // Both the delivered and the empty message are eventually deleted from the store.
228+ await BusyWait . Until ( async ( ) => ( await store . MessageStore . GetMessages ( storedId ) ) . Count == 0 ) ;
229+
230+ unhandledExceptionHandler . ShouldNotHaveExceptions ( ) ;
231+ }
232+
233+ public abstract Task EmptyMessageIsNotDeliveredWhenFlowIsRestartedViaControlPanel ( ) ;
234+ public async Task EmptyMessageIsNotDeliveredWhenFlowIsRestartedViaControlPanel ( Task < IFunctionStore > functionStore )
235+ {
236+ var store = await functionStore ;
237+
238+ var flowId = TestFlowId . Create ( ) ;
239+ var unhandledExceptionHandler = new UnhandledExceptionCatcher ( ) ;
240+ using var functionsRegistry = new FunctionsRegistry (
241+ store ,
242+ new Settings ( unhandledExceptionHandler . Catch , messagesPullFrequency : TimeSpan . FromMilliseconds ( 100 ) )
243+ ) ;
244+
245+ var awaitMessage = new SyncedFlag ( ) ;
246+ var registration = functionsRegistry . RegisterFunc (
247+ flowId . Type ,
248+ inner : async Task < string > ( string _ , Workflow workflow ) =>
249+ awaitMessage . IsRaised ? await workflow . Message < string > ( ) : "no message awaited"
250+ ) ;
251+
252+ // Complete the flow without it consuming any messages.
253+ await registration . Run ( flowId . Instance . Value , "" ) ;
254+
255+ var storedId = registration . MapToStoredId ( flowId . Instance ) ;
256+ var replicaId = functionsRegistry . ClusterInfo . ReplicaId ;
257+ var serializer = DefaultSerializer . Instance ;
258+ await store . MessageStore . AppendMessages ( [
259+ new StoredIdAndMessage ( storedId , StoredMessage . CreateEmpty ( replicaId ) ) ,
260+ new StoredIdAndMessage (
261+ storedId ,
262+ new StoredMessage (
263+ serializer . Serialize ( "hello world" , typeof ( string ) ) ,
264+ serializer . SerializeType ( typeof ( string ) ) ,
265+ Position : 0 ,
266+ Replica : replicaId
267+ )
268+ )
269+ ] ) ;
270+
271+ // The pending messages must not resurrect the completed flow.
272+ await Task . Delay ( 500 ) ;
273+ var controlPanel = await registration . ControlPanel ( flowId . Instance ) ;
274+ controlPanel . ShouldNotBeNull ( ) ;
275+ controlPanel . Status . ShouldBe ( Status . Succeeded ) ;
276+ controlPanel . Result . ShouldBe ( "no message awaited" ) ;
277+
278+ // An explicit restart does not pull messages itself - the MessageWatchdog delivers the pending non-empty
279+ // message to the restarted flow, while the empty message is never delivered.
280+ awaitMessage . Raise ( ) ;
281+ await controlPanel . ScheduleRestart ( ) ;
282+ await controlPanel . WaitForCompletion ( allowPostponeAndSuspended : true ) ;
283+ await controlPanel . Refresh ( ) ;
284+ controlPanel . Status . ShouldBe ( Status . Succeeded ) ;
285+ controlPanel . Result . ShouldBe ( "hello world" ) ;
286+
287+ // Both rows end up deleted: the non-empty message is inlined into the completed flow's effect state (and
288+ // its row removed), while the empty poke is simply deleted - a completed flow needs no restart.
289+ await BusyWait . Until ( async ( ) => ( await store . MessageStore . GetMessages ( storedId ) ) . Count == 0 ) ;
290+
291+ unhandledExceptionHandler . ShouldNotHaveExceptions ( ) ;
292+ }
293+
294+ public abstract Task PendingMessageIsDeliveredWhenCompletedFlowIsPostponedAndRestartedByWatchdog ( ) ;
295+ public async Task PendingMessageIsDeliveredWhenCompletedFlowIsPostponedAndRestartedByWatchdog ( Task < IFunctionStore > functionStore )
296+ {
297+ var store = await functionStore ;
298+
299+ var flowId = TestFlowId . Create ( ) ;
300+ var unhandledExceptionHandler = new UnhandledExceptionCatcher ( ) ;
301+ using var functionsRegistry = new FunctionsRegistry (
302+ store ,
303+ new Settings (
304+ unhandledExceptionHandler . Catch ,
305+ messagesPullFrequency : TimeSpan . FromMilliseconds ( 100 ) ,
306+ watchdogCheckFrequency : TimeSpan . FromMilliseconds ( 100 )
307+ )
308+ ) ;
309+
310+ var awaitMessage = new SyncedFlag ( ) ;
311+ var registration = functionsRegistry . RegisterFunc (
312+ flowId . Type ,
313+ inner : async Task < string > ( string _ , Workflow workflow ) =>
314+ awaitMessage . IsRaised ? await workflow . Message < string > ( ) : "no message awaited"
315+ ) ;
316+
317+ // Complete the flow without it consuming any messages.
318+ await registration . Run ( flowId . Instance . Value , "" ) ;
319+
320+ var storedId = registration . MapToStoredId ( flowId . Instance ) ;
321+ var replicaId = functionsRegistry . ClusterInfo . ReplicaId ;
322+ var serializer = DefaultSerializer . Instance ;
323+ await store . MessageStore . AppendMessages ( [
324+ new StoredIdAndMessage (
325+ storedId ,
326+ new StoredMessage (
327+ serializer . Serialize ( "hello world" , typeof ( string ) ) ,
328+ serializer . SerializeType ( typeof ( string ) ) ,
329+ Position : 0 ,
330+ Replica : replicaId
331+ )
332+ )
333+ ] ) ;
334+
335+ // The message is inlined into the completed flow's effect state and its row deleted.
336+ await BusyWait . Until ( async ( ) => ( await store . MessageStore . GetMessages ( storedId ) ) . Count == 0 ) ;
337+
338+ // Resurrect the completed flow via Postpone - the PostponedWatchdog's restart path must also hand the
339+ // inlined message over (it travels in the effect snapshot, not through any restart-specific channel).
340+ awaitMessage . Raise ( ) ;
341+ var controlPanel = await registration . ControlPanel ( flowId . Instance ) ;
342+ controlPanel . ShouldNotBeNull ( ) ;
343+ await controlPanel . Postpone ( DateTime . UtcNow ) ;
344+
345+ await controlPanel . BusyWaitUntil ( c => c . Status == Status . Succeeded ) ;
346+ controlPanel . Result . ShouldBe ( "hello world" ) ;
347+
348+ unhandledExceptionHandler . ShouldNotHaveExceptions ( ) ;
349+ }
350+
351+ public abstract Task PendingMessageIsDeliveredWhenCompletedFlowIsRestartedOnDifferentReplica ( ) ;
352+ public async Task PendingMessageIsDeliveredWhenCompletedFlowIsRestartedOnDifferentReplica ( Task < IFunctionStore > functionStore )
353+ {
354+ var store = await functionStore ;
355+
356+ var flowId = TestFlowId . Create ( ) ;
357+ var unhandledExceptionHandler = new UnhandledExceptionCatcher ( ) ;
358+ var awaitMessage = new SyncedFlag ( ) ;
359+
360+ Func < FunctionsRegistry > createRegistry = ( ) => new FunctionsRegistry (
361+ store ,
362+ new Settings ( unhandledExceptionHandler . Catch , messagesPullFrequency : TimeSpan . FromMilliseconds ( 100 ) )
363+ ) ;
364+ Func < FunctionsRegistry , FuncRegistration < string , string > > register = registry => registry . RegisterFunc (
365+ flowId . Type ,
366+ inner : async Task < string > ( string _ , Workflow workflow ) =>
367+ awaitMessage . IsRaised ? await workflow . Message < string > ( ) : "no message awaited"
368+ ) ;
369+
370+ using var publisherRegistry = createRegistry ( ) ;
371+ var publisherRegistration = register ( publisherRegistry ) ;
372+
373+ // Complete the flow on the publisher replica without it consuming any messages.
374+ await publisherRegistration . Run ( flowId . Instance . Value , "" ) ;
375+
376+ // Append a message stamped with the publisher replica - its watchdog fetches it, finds the flow
377+ // completed and inlines it into the flow's effect state.
378+ var storedId = publisherRegistration . MapToStoredId ( flowId . Instance ) ;
379+ var serializer = DefaultSerializer . Instance ;
380+ await store . MessageStore . AppendMessages ( [
381+ new StoredIdAndMessage (
382+ storedId ,
383+ new StoredMessage (
384+ serializer . Serialize ( "hello world" , typeof ( string ) ) ,
385+ serializer . SerializeType ( typeof ( string ) ) ,
386+ Position : 0 ,
387+ Replica : publisherRegistry . ClusterInfo . ReplicaId
388+ )
389+ )
390+ ] ) ;
391+ await BusyWait . Until ( async ( ) => ( await store . MessageStore . GetMessages ( storedId ) ) . Count == 0 ) ;
392+
393+ // Restart the flow from a DIFFERENT replica: the inlined message travels in the effect snapshot the
394+ // restart hands over, so delivery does not depend on the publisher replica's in-memory state.
395+ using var restartingRegistry = createRegistry ( ) ;
396+ var restartingRegistration = register ( restartingRegistry ) ;
397+
398+ awaitMessage . Raise ( ) ;
399+ var controlPanel = await restartingRegistration . ControlPanel ( flowId . Instance ) ;
400+ controlPanel . ShouldNotBeNull ( ) ;
401+ await controlPanel . ScheduleRestart ( ) ;
402+ await controlPanel . WaitForCompletion ( allowPostponeAndSuspended : true ) ;
403+ await controlPanel . Refresh ( ) ;
404+ controlPanel . Status . ShouldBe ( Status . Succeeded ) ;
405+ controlPanel . Result . ShouldBe ( "hello world" ) ;
406+
407+ unhandledExceptionHandler . ShouldNotHaveExceptions ( ) ;
408+ }
121409}
0 commit comments