diff --git a/Core/Cleipnir.ResilientFunctions.Tests/InMemoryTests/LeaseUpdaterTests/LeaseUpdaterTestFunctionStore.cs b/Core/Cleipnir.ResilientFunctions.Tests/InMemoryTests/LeaseUpdaterTests/LeaseUpdaterTestFunctionStore.cs index 4cdeebe6..8f6ead23 100644 --- a/Core/Cleipnir.ResilientFunctions.Tests/InMemoryTests/LeaseUpdaterTests/LeaseUpdaterTestFunctionStore.cs +++ b/Core/Cleipnir.ResilientFunctions.Tests/InMemoryTests/LeaseUpdaterTests/LeaseUpdaterTestFunctionStore.cs @@ -67,10 +67,9 @@ public Task SucceedFunction( byte[]? result, long timestamp, int expectedEpoch, - IReadOnlyList? effects, - IReadOnlyList? messages, + IReadOnlyList? effects, ComplimentaryState complimentaryState - ) => _inner.SucceedFunction(storedId, result, timestamp, expectedEpoch, effects, messages, complimentaryState); + ) => _inner.SucceedFunction(storedId, result, timestamp, expectedEpoch, effects, complimentaryState); public Task PostponeFunction( StoredId storedId, @@ -78,29 +77,26 @@ public Task PostponeFunction( long timestamp, bool ignoreInterrupted, int expectedEpoch, - IReadOnlyList? effects, - IReadOnlyList? messages, + IReadOnlyList? effects, ComplimentaryState complimentaryState - ) => _inner.PostponeFunction(storedId, postponeUntil, timestamp, ignoreInterrupted, expectedEpoch, effects, messages, complimentaryState); + ) => _inner.PostponeFunction(storedId, postponeUntil, timestamp, ignoreInterrupted, expectedEpoch, effects, complimentaryState); public Task FailFunction( StoredId storedId, StoredException storedException, long timestamp, int expectedEpoch, - IReadOnlyList? effects, - IReadOnlyList? messages, + IReadOnlyList? effects, ComplimentaryState complimentaryState - ) => _inner.FailFunction(storedId, storedException, timestamp, expectedEpoch, effects, messages, complimentaryState); + ) => _inner.FailFunction(storedId, storedException, timestamp, expectedEpoch, effects, complimentaryState); public Task SuspendFunction( StoredId storedId, long timestamp, int expectedEpoch, - IReadOnlyList? effects, - IReadOnlyList? messages, + IReadOnlyList? effects, ComplimentaryState complimentaryState - ) => _inner.SuspendFunction(storedId, timestamp, expectedEpoch, effects, messages, complimentaryState); + ) => _inner.SuspendFunction(storedId, timestamp, expectedEpoch, effects, complimentaryState); public Task Interrupt(StoredId storedId, bool onlyIfExecuting) => _inner.Interrupt(storedId, onlyIfExecuting); diff --git a/Core/Cleipnir.ResilientFunctions.Tests/InMemoryTests/RFunctionTests/ControlPanelTests.cs b/Core/Cleipnir.ResilientFunctions.Tests/InMemoryTests/RFunctionTests/ControlPanelTests.cs index fd011a0d..84a8a14a 100644 --- a/Core/Cleipnir.ResilientFunctions.Tests/InMemoryTests/RFunctionTests/ControlPanelTests.cs +++ b/Core/Cleipnir.ResilientFunctions.Tests/InMemoryTests/RFunctionTests/ControlPanelTests.cs @@ -166,14 +166,6 @@ public override Task ExistingStateCanBeReplacedRemovedAndAdded() public override Task SaveChangesPersistsChangedResult() => SaveChangesPersistsChangedResult(Utils.CreateInMemoryFunctionStoreTask()); - [TestMethod] - public override Task ExistingTimeoutCanBeUpdatedForAction() - => ExistingTimeoutCanBeUpdatedForAction(Utils.CreateInMemoryFunctionStoreTask()); - - [TestMethod] - public override Task ExistingTimeoutCanBeUpdatedForFunc() - => ExistingTimeoutCanBeUpdatedForFunc(Utils.CreateInMemoryFunctionStoreTask()); - [TestMethod] public override Task CorrelationsCanBeChanged() => CorrelationsCanBeChanged(FunctionStoreFactory.Create()); diff --git a/Core/Cleipnir.ResilientFunctions.Tests/InMemoryTests/RFunctionTests/DelayedStartUpTests.cs b/Core/Cleipnir.ResilientFunctions.Tests/InMemoryTests/RFunctionTests/DelayedStartUpTests.cs index e20ef450..d57a8ab7 100644 --- a/Core/Cleipnir.ResilientFunctions.Tests/InMemoryTests/RFunctionTests/DelayedStartUpTests.cs +++ b/Core/Cleipnir.ResilientFunctions.Tests/InMemoryTests/RFunctionTests/DelayedStartUpTests.cs @@ -111,7 +111,6 @@ await store.PostponeFunction( ignoreInterrupted: true, expectedEpoch: 0, effects: null, - messages: null, complimentaryState: new ComplimentaryState(storedParameter.ToUtf8Bytes().ToFunc(), LeaseLength: 0) ).ShouldBeTrueAsync(); @@ -151,7 +150,6 @@ await store.PostponeFunction( ignoreInterrupted: true, expectedEpoch: 0, effects: null, - messages: null, new ComplimentaryState(storedParameter.ToFunc(), LeaseLength: 0) ); diff --git a/Core/Cleipnir.ResilientFunctions.Tests/InMemoryTests/RFunctionTests/EffectTests.cs b/Core/Cleipnir.ResilientFunctions.Tests/InMemoryTests/RFunctionTests/EffectTests.cs index 0df15285..6f7b4339 100644 --- a/Core/Cleipnir.ResilientFunctions.Tests/InMemoryTests/RFunctionTests/EffectTests.cs +++ b/Core/Cleipnir.ResilientFunctions.Tests/InMemoryTests/RFunctionTests/EffectTests.cs @@ -34,6 +34,14 @@ public override Task TaskWhenAnyFuncTest() public override Task TaskWhenAllFuncTest() => TaskWhenAllFuncTest(FunctionStoreFactory.Create()); + [TestMethod] + public override Task TaskWhenAnyPostponeTest() + => TaskWhenAnyPostponeTest(FunctionStoreFactory.Create()); + + [TestMethod] + public override Task TaskWhenAllPostponeTest() + => TaskWhenAllPostponeTest(FunctionStoreFactory.Create()); + [TestMethod] public override Task ClearEffectsTest() => ClearEffectsTest(FunctionStoreFactory.Create()); @@ -42,6 +50,10 @@ public override Task ClearEffectsTest() public override Task EffectsCrudTest() => EffectsCrudTest(FunctionStoreFactory.Create()); + [TestMethod] + public override Task EffectsCreateOrGetWithoutFlushTest() + => EffectsCreateOrGetWithoutFlushTest(FunctionStoreFactory.Create()); + [TestMethod] public override Task ExistingEffectsFuncIsOnlyInvokedAfterGettingValue() => ExistingEffectsFuncIsOnlyInvokedAfterGettingValue(FunctionStoreFactory.Create()); diff --git a/Core/Cleipnir.ResilientFunctions.Tests/InMemoryTests/RFunctionTests/SunshineTests.cs b/Core/Cleipnir.ResilientFunctions.Tests/InMemoryTests/RFunctionTests/SunshineTests.cs index 06e826fc..4d2ac81e 100644 --- a/Core/Cleipnir.ResilientFunctions.Tests/InMemoryTests/RFunctionTests/SunshineTests.cs +++ b/Core/Cleipnir.ResilientFunctions.Tests/InMemoryTests/RFunctionTests/SunshineTests.cs @@ -91,4 +91,20 @@ public override Task ParamlessCanBeCreatedWithInitialFailedEffect() [TestMethod] public override Task FunctionCanAcceptAndReturnOptionType() => FunctionCanAcceptAndReturnOptionType(FunctionStoreFactory.Create()); + + [TestMethod] + public override Task PendingEffectChangesArePersistedWithSucceedFunctionResult() + => PendingEffectChangesArePersistedWithSucceedFunctionResult(FunctionStoreFactory.Create()); + + [TestMethod] + public override Task PendingEffectChangesArePersistedWithPostponedFunctionResult() + => PendingEffectChangesArePersistedWithPostponedFunctionResult(FunctionStoreFactory.Create()); + + [TestMethod] + public override Task PendingEffectChangesArePersistedWithSuspendedFunctionResult() + => PendingEffectChangesArePersistedWithSuspendedFunctionResult(FunctionStoreFactory.Create()); + + [TestMethod] + public override Task PendingEffectChangesArePersistedWithFailedFunctionResult() + => PendingEffectChangesArePersistedWithFailedFunctionResult(FunctionStoreFactory.Create()); } \ No newline at end of file diff --git a/Core/Cleipnir.ResilientFunctions.Tests/InMemoryTests/RFunctionTests/TimeoutTests.cs b/Core/Cleipnir.ResilientFunctions.Tests/InMemoryTests/RFunctionTests/TimeoutTests.cs index 0c77edc9..cc393f4b 100644 --- a/Core/Cleipnir.ResilientFunctions.Tests/InMemoryTests/RFunctionTests/TimeoutTests.cs +++ b/Core/Cleipnir.ResilientFunctions.Tests/InMemoryTests/RFunctionTests/TimeoutTests.cs @@ -14,18 +14,6 @@ public override Task ExpiredTimeoutIsAddedToMessages() public override Task ExpiredTimeoutMakesReactiveChainThrowTimeoutException() => ExpiredTimeoutMakesReactiveChainThrowTimeoutException(FunctionStoreFactory.Create()); - [TestMethod] - public override Task RegisteredTimeoutIsCancelledAfterReactiveChainCompletes() - => RegisteredTimeoutIsCancelledAfterReactiveChainCompletes(FunctionStoreFactory.Create()); - - [TestMethod] - public override Task PendingTimeoutCanBeRemovedFromControlPanel() - => PendingTimeoutCanBeRemovedFromControlPanel(FunctionStoreFactory.Create()); - - [TestMethod] - public override Task PendingTimeoutCanBeUpdatedFromControlPanel() - => PendingTimeoutCanBeUpdatedFromControlPanel(FunctionStoreFactory.Create()); - [TestMethod] public override Task ExpiredImplicitTimeoutsAreAddedToMessages() => ExpiredImplicitTimeoutsAreAddedToMessages(FunctionStoreFactory.Create()); diff --git a/Core/Cleipnir.ResilientFunctions.Tests/InMemoryTests/ShutdownCoordinationTests/RFunctionsShutdownTests.cs b/Core/Cleipnir.ResilientFunctions.Tests/InMemoryTests/ShutdownCoordinationTests/RFunctionsShutdownTests.cs index 05282ad3..b4f9dc3f 100644 --- a/Core/Cleipnir.ResilientFunctions.Tests/InMemoryTests/ShutdownCoordinationTests/RFunctionsShutdownTests.cs +++ b/Core/Cleipnir.ResilientFunctions.Tests/InMemoryTests/ShutdownCoordinationTests/RFunctionsShutdownTests.cs @@ -184,7 +184,6 @@ await store.PostponeFunction( ignoreInterrupted: true, expectedEpoch: 0, effects: null, - messages: null, new ComplimentaryState(() => storedParameter.ToUtf8Bytes(), LeaseLength: 0) ).ShouldBeTrueAsync(); diff --git a/Core/Cleipnir.ResilientFunctions.Tests/InMemoryTests/StoreTests.cs b/Core/Cleipnir.ResilientFunctions.Tests/InMemoryTests/StoreTests.cs index f743ff28..d4738efd 100644 --- a/Core/Cleipnir.ResilientFunctions.Tests/InMemoryTests/StoreTests.cs +++ b/Core/Cleipnir.ResilientFunctions.Tests/InMemoryTests/StoreTests.cs @@ -227,4 +227,24 @@ public override Task RestartExecutionReturnsEffectsAndMessages() [TestMethod] public override Task RestartExecutionWorksWithEmptyEffectsAndMessages() => RestartExecutionWorksWithEmptyEffectsAndMessages(FunctionStoreFactory.Create()); + + [TestMethod] + public override Task EffectsArePersistedOnSuspendFunction() + => EffectsArePersistedOnSuspendFunction(FunctionStoreFactory.Create()); + + [TestMethod] + public override Task EffectsArePersistedOnSucceededFunction() + => EffectsArePersistedOnSucceededFunction(FunctionStoreFactory.Create()); + + [TestMethod] + public override Task EffectsArePersistedOnPostponeFunction() + => EffectsArePersistedOnPostponeFunction(FunctionStoreFactory.Create()); + + [TestMethod] + public override Task EffectsArePersistedOnFailFunction() + => EffectsArePersistedOnFailFunction(FunctionStoreFactory.Create()); + + [TestMethod] + public override Task AppendMessageNoStatusAndInterruptWorks() + => AppendMessageNoStatusAndInterruptWorks(FunctionStoreFactory.Create()); } \ No newline at end of file diff --git a/Core/Cleipnir.ResilientFunctions.Tests/InMemoryTests/TimeoutStoreTests.cs b/Core/Cleipnir.ResilientFunctions.Tests/InMemoryTests/TimeoutStoreTests.cs index d289c42b..2f405182 100644 --- a/Core/Cleipnir.ResilientFunctions.Tests/InMemoryTests/TimeoutStoreTests.cs +++ b/Core/Cleipnir.ResilientFunctions.Tests/InMemoryTests/TimeoutStoreTests.cs @@ -1,6 +1,5 @@ using System.Threading.Tasks; using Cleipnir.ResilientFunctions.Helpers; -using Cleipnir.ResilientFunctions.Storage; using Microsoft.VisualStudio.TestTools.UnitTesting; namespace Cleipnir.ResilientFunctions.Tests.InMemoryTests; @@ -20,27 +19,11 @@ public override Task ExistingTimeoutCanUpdatedSuccessfully() public override Task OverwriteFalseDoesNotAffectExistingTimeout() => OverwriteFalseDoesNotAffectExistingTimeout(FunctionStoreFactory.Create().SelectAsync(fs => fs.TimeoutStore)); - [TestMethod] - public override Task RegisteredTimeoutIsReturnedFromRegisteredTimeouts() - => RegisteredTimeoutIsReturnedFromRegisteredTimeouts(FunctionStoreFactory.Create()); - [TestMethod] public override Task TimeoutStoreCanBeInitializedMultipleTimes() => TimeoutStoreCanBeInitializedMultipleTimes(FunctionStoreFactory.Create().SelectAsync(fs => fs.TimeoutStore)); - - [TestMethod] - public override Task RegisteredTimeoutIsReturnedFromRegisteredTimeoutsForFunctionId() - => RegisteredTimeoutIsReturnedFromRegisteredTimeoutsForFunctionId(FunctionStoreFactory.Create()); - - [TestMethod] - public override Task TimeoutIsNotRegisteredAgainWhenProviderAlreadyContainsTimeout() - => TimeoutIsNotRegisteredAgainWhenProviderAlreadyContainsTimeout(FunctionStoreFactory.Create()); - + [TestMethod] public override Task TimeoutsForDifferentTypesCanBeCreatedFetchedSuccessfully() => TimeoutsForDifferentTypesCanBeCreatedFetchedSuccessfully(FunctionStoreFactory.Create().SelectAsync(fs => fs.TimeoutStore)); - - [TestMethod] - public override Task CancellingNonExistingTimeoutDoesResultInIO() - => CancellingNonExistingTimeoutDoesResultInIO(FunctionStoreFactory.Create()); } \ No newline at end of file diff --git a/Core/Cleipnir.ResilientFunctions.Tests/Messaging/InMemoryTests/EventSourcesTests.cs b/Core/Cleipnir.ResilientFunctions.Tests/Messaging/InMemoryTests/EventSourcesTests.cs index 30a281fd..973b7013 100644 --- a/Core/Cleipnir.ResilientFunctions.Tests/Messaging/InMemoryTests/EventSourcesTests.cs +++ b/Core/Cleipnir.ResilientFunctions.Tests/Messaging/InMemoryTests/EventSourcesTests.cs @@ -25,6 +25,10 @@ public override Task MessagesFirstOfTypesReturnsFirstForFirstOfTypesOnFirst() public override Task MessagesFirstOfTypesReturnsSecondForFirstOfTypesOnSecond() => MessagesFirstOfTypesReturnsSecondForFirstOfTypesOnSecond(FunctionStoreFactory.Create()); + [TestMethod] + public override Task MessagesFirstOfTypesReturnsNoneForTimeout() + => MessagesFirstOfTypesReturnsNoneForTimeout(FunctionStoreFactory.Create()); + [TestMethod] public override Task ExistingEventsShouldBeSameAsAllAfterEmit() => ExistingEventsShouldBeSameAsAllAfterEmit(FunctionStoreFactory.Create()); diff --git a/Core/Cleipnir.ResilientFunctions.Tests/Messaging/TestTemplates/CustomMessageSerializerTests.cs b/Core/Cleipnir.ResilientFunctions.Tests/Messaging/TestTemplates/CustomMessageSerializerTests.cs index 3503e0b5..e189cd87 100644 --- a/Core/Cleipnir.ResilientFunctions.Tests/Messaging/TestTemplates/CustomMessageSerializerTests.cs +++ b/Core/Cleipnir.ResilientFunctions.Tests/Messaging/TestTemplates/CustomMessageSerializerTests.cs @@ -31,10 +31,6 @@ await functionStore.CreateFunction( ); var eventSerializer = new EventSerializer(); var messagesWriter = new MessageWriter(storedId, functionStore, eventSerializer, scheduleReInvocation: (_, _) => Task.CompletedTask); - var lazyExistingEffects = new Lazy>>(() => Task.FromResult((IReadOnlyList) new List())); - var effectResults = new EffectResults(flowId, storedId, lazyExistingEffects, functionStore.EffectsStore, DefaultSerializer.Instance); - var effect = new Effect(effectResults); - var registeredTimeouts = new RegisteredTimeouts(storedId, functionStore.TimeoutStore, effect); var messagesPullerAndEmitter = new MessagesPullerAndEmitter( storedId, defaultDelay: TimeSpan.FromSeconds(1), @@ -42,10 +38,9 @@ await functionStore.CreateFunction( isWorkflowRunning: () => true, functionStore, eventSerializer, - registeredTimeouts, initialMessages: null ); - var messages = new Messages(messagesWriter, registeredTimeouts, messagesPullerAndEmitter); + var messages = new Messages(messagesWriter, messagesPullerAndEmitter); await messages.AppendMessage("hello world"); diff --git a/Core/Cleipnir.ResilientFunctions.Tests/Messaging/TestTemplates/MessagesTests.cs b/Core/Cleipnir.ResilientFunctions.Tests/Messaging/TestTemplates/MessagesTests.cs index 803ee0f5..a475c756 100644 --- a/Core/Cleipnir.ResilientFunctions.Tests/Messaging/TestTemplates/MessagesTests.cs +++ b/Core/Cleipnir.ResilientFunctions.Tests/Messaging/TestTemplates/MessagesTests.cs @@ -2,12 +2,12 @@ using System.Collections.Generic; using System.Linq; using System.Threading.Tasks; -using Cleipnir.ResilientFunctions.CoreRuntime; using Cleipnir.ResilientFunctions.CoreRuntime.Serialization; using Cleipnir.ResilientFunctions.Domain; using Cleipnir.ResilientFunctions.Helpers; using Cleipnir.ResilientFunctions.Messaging; using Cleipnir.ResilientFunctions.Reactive.Extensions; +using Cleipnir.ResilientFunctions.Reactive.Utilities; using Cleipnir.ResilientFunctions.Storage; using Cleipnir.ResilientFunctions.Tests.Messaging.Utils; using Cleipnir.ResilientFunctions.Tests.Utils; @@ -33,7 +33,6 @@ await functionStore.CreateFunction( parent: null ); var messagesWriter = new MessageWriter(storedId, functionStore, DefaultSerializer.Instance, scheduleReInvocation: (_, _) => Task.CompletedTask); - var registeredTimeouts = new RegisteredTimeouts(storedId, functionStore.TimeoutStore, CreateEffect(storedId, flowId, functionStore)); var messagesPullerAndEmitter = new MessagesPullerAndEmitter( storedId, defaultDelay: TimeSpan.FromMilliseconds(250), @@ -41,10 +40,9 @@ await functionStore.CreateFunction( isWorkflowRunning: () => true, functionStore, DefaultSerializer.Instance, - registeredTimeouts, initialMessages: [] ); - var messages = new Messages(messagesWriter, registeredTimeouts, messagesPullerAndEmitter); + var messages = new Messages(messagesWriter, messagesPullerAndEmitter); var task = messages.First(); @@ -87,36 +85,18 @@ protected async Task MessagesFirstOfTypesReturnsNoneForFirstOfTypesOnTimeout(Tas protected async Task MessagesFirstOfTypesReturnsFirstForFirstOfTypesOnFirst(Task functionStoreTask) { var flowId = TestFlowId.Create(); - var storedId = flowId.ToStoredId(new StoredType(1)); var functionStore = await functionStoreTask; - await functionStore.CreateFunction( - storedId, - "humanInstanceId", - Test.SimpleStoredParameter, - leaseExpiration: DateTime.UtcNow.Ticks, - postponeUntil: null, - timestamp: DateTime.UtcNow.Ticks, - parent: null - ); - var messagesWriter = new MessageWriter(storedId, functionStore, DefaultSerializer.Instance, scheduleReInvocation: (_, _) => Task.CompletedTask); - var registeredTimeouts = new RegisteredTimeouts(storedId, functionStore.TimeoutStore, CreateEffect(storedId, flowId, functionStore)); - var messagesPullerAndEmitter = new MessagesPullerAndEmitter( - storedId, - defaultDelay: TimeSpan.FromMilliseconds(250), - defaultMaxWait: TimeSpan.MaxValue, - isWorkflowRunning: () => true, - functionStore, - DefaultSerializer.Instance, - registeredTimeouts, - initialMessages: [] + + using var registry = new FunctionsRegistry(functionStore); + var registration = registry + .RegisterFunc>(flowId.Type, + (_, workflow) => workflow.Messages.FirstOfTypes(expiresIn: TimeSpan.FromSeconds(10)) ); - var messages = new Messages(messagesWriter, registeredTimeouts, messagesPullerAndEmitter); - var eitherOrNoneTask = messages.FirstOfTypes(expiresIn: TimeSpan.FromSeconds(10)); - - await messages.AppendMessage("Hello"); - - var eitherOrNone = await eitherOrNoneTask; + var scheduled = await registration.Schedule(flowId.Instance, param: ""); + await registration.SendMessage(flowId.Instance, "Hello"); + + var eitherOrNone = await scheduled.Completion(); eitherOrNone.HasFirst.ShouldBeTrue(); eitherOrNone.First.ShouldBe("Hello"); eitherOrNone.HasNone.ShouldBeFalse(); @@ -127,42 +107,45 @@ await functionStore.CreateFunction( protected async Task MessagesFirstOfTypesReturnsSecondForFirstOfTypesOnSecond(Task functionStoreTask) { var flowId = TestFlowId.Create(); - var storedId = flowId.ToStoredId(new StoredType(1)); var functionStore = await functionStoreTask; - await functionStore.CreateFunction( - storedId, - "humanInstanceId", - Test.SimpleStoredParameter, - leaseExpiration: DateTime.UtcNow.Ticks, - postponeUntil: null, - timestamp: DateTime.UtcNow.Ticks, - parent: null - ); - var messagesWriter = new MessageWriter(storedId, functionStore, DefaultSerializer.Instance, scheduleReInvocation: (_, _) => Task.CompletedTask); - var registeredTimeouts = new RegisteredTimeouts(storedId, functionStore.TimeoutStore, CreateEffect(storedId, flowId, functionStore)); - var messagesPullerAndEmitter = new MessagesPullerAndEmitter( - storedId, - defaultDelay: TimeSpan.FromMilliseconds(250), - defaultMaxWait: TimeSpan.MaxValue, - isWorkflowRunning: () => true, - functionStore, - DefaultSerializer.Instance, - registeredTimeouts, - initialMessages: [] - ); - var messages = new Messages(messagesWriter, registeredTimeouts, messagesPullerAndEmitter); - - var eitherOrNoneTask = messages.FirstOfTypes(expiresIn: TimeSpan.FromSeconds(10)); - await messages.AppendMessage(1); - - var eitherOrNone = await eitherOrNoneTask; + using var registry = new FunctionsRegistry(functionStore); + var registration = registry + .RegisterFunc>(flowId.Type, + (_, workflow) => workflow.Messages.FirstOfTypes(expiresIn: TimeSpan.FromSeconds(10)) + ); + + var scheduled = await registration.Schedule(flowId.Instance, param: ""); + await registration.SendMessage(flowId.Instance, 1); + + var eitherOrNone = await scheduled.Completion(); eitherOrNone.HasSecond.ShouldBeTrue(); eitherOrNone.Second.ShouldBe(1); eitherOrNone.HasNone.ShouldBeFalse(); eitherOrNone.HasFirst.ShouldBeFalse(); } + public abstract Task MessagesFirstOfTypesReturnsNoneForTimeout(); + protected async Task MessagesFirstOfTypesReturnsNoneForTimeout(Task functionStoreTask) + { + var flowId = TestFlowId.Create(); + var functionStore = await functionStoreTask; + + using var registry = new FunctionsRegistry(functionStore); + var registration = registry + .RegisterFunc>(flowId.Type, + async (_, workflow) => + { + var result = await workflow.Messages.FirstOfTypes(expiresIn: TimeSpan.FromMilliseconds(10)); + return result; + }); + + var scheduled = await registration.Schedule(flowId.Instance, param: ""); + + var eitherOrNone = await scheduled.Completion(maxWait: TimeSpan.FromSeconds(10)); + eitherOrNone.HasNone.ShouldBeTrue(); + } + public abstract Task ExistingEventsShouldBeSameAsAllAfterEmit(); protected async Task ExistingEventsShouldBeSameAsAllAfterEmit(Task functionStoreTask) { @@ -179,7 +162,6 @@ await functionStore.CreateFunction( parent: null ); var messagesWriter = new MessageWriter(storedId, functionStore, DefaultSerializer.Instance, scheduleReInvocation: (_, _) => Task.CompletedTask); - var registeredTimeouts = new RegisteredTimeouts(storedId, functionStore.TimeoutStore, CreateEffect(storedId, flowId, functionStore)); var messagesPullerAndEmitter = new MessagesPullerAndEmitter( storedId, defaultDelay: TimeSpan.FromMilliseconds(250), @@ -187,10 +169,9 @@ await functionStore.CreateFunction( isWorkflowRunning: () => true, functionStore, DefaultSerializer.Instance, - registeredTimeouts, initialMessages: [] ); - var messages = new Messages(messagesWriter, registeredTimeouts, messagesPullerAndEmitter); + var messages = new Messages(messagesWriter, messagesPullerAndEmitter); await messages.AppendMessage("hello world"); @@ -221,7 +202,6 @@ await functionStore.CreateFunction( parent: null ); var messagesWriter = new MessageWriter(storedId, functionStore, DefaultSerializer.Instance, scheduleReInvocation: (_, _) => Task.CompletedTask); - var registeredTimeouts = new RegisteredTimeouts(storedId, functionStore.TimeoutStore, CreateEffect(storedId, flowId, functionStore)); var messagesPullerAndEmitter = new MessagesPullerAndEmitter( storedId, defaultDelay: TimeSpan.FromMilliseconds(250), @@ -229,10 +209,9 @@ await functionStore.CreateFunction( isWorkflowRunning: () => true, functionStore, DefaultSerializer.Instance, - registeredTimeouts, initialMessages: [] ); - var messages = new Messages(messagesWriter, registeredTimeouts, messagesPullerAndEmitter); + var messages = new Messages(messagesWriter, messagesPullerAndEmitter); var task = messages.Take(2).ToList(); @@ -268,7 +247,6 @@ await functionStore.CreateFunction( parent: null ); var messagesWriter = new MessageWriter(storedId, functionStore, DefaultSerializer.Instance, scheduleReInvocation: (_, _) => Task.CompletedTask); - var registeredTimeouts = new RegisteredTimeouts(storedId, functionStore.TimeoutStore, CreateEffect(storedId, flowId, functionStore)); var messagesPullerAndEmitter = new MessagesPullerAndEmitter( storedId, defaultDelay: TimeSpan.FromMilliseconds(250), @@ -276,10 +254,9 @@ await functionStore.CreateFunction( isWorkflowRunning: () => true, functionStore, DefaultSerializer.Instance, - registeredTimeouts, initialMessages: [] ); - var messages = new Messages(messagesWriter, registeredTimeouts, messagesPullerAndEmitter); + var messages = new Messages(messagesWriter, messagesPullerAndEmitter); var task = messages.Take(2).ToList(); @@ -314,7 +291,6 @@ await functionStore.CreateFunction( parent: null ); var messagesWriter = new MessageWriter(storedId, functionStore, DefaultSerializer.Instance, scheduleReInvocation: (_, _) => Task.CompletedTask); - var registeredTimeouts = new RegisteredTimeouts(storedId, functionStore.TimeoutStore, CreateEffect(storedId, flowId, functionStore)); var messagesPullerAndEmitter = new MessagesPullerAndEmitter( storedId, defaultDelay: TimeSpan.FromMilliseconds(250), @@ -322,10 +298,9 @@ await functionStore.CreateFunction( isWorkflowRunning: () => true, functionStore, DefaultSerializer.Instance, - registeredTimeouts, initialMessages: [] ); - var messages = new Messages(messagesWriter, registeredTimeouts, messagesPullerAndEmitter); + var messages = new Messages(messagesWriter, messagesPullerAndEmitter); var task = messages.First(); @@ -356,7 +331,6 @@ await functionStore.CreateFunction( parent: null ); var messagesWriter = new MessageWriter(storedId, functionStore, DefaultSerializer.Instance, scheduleReInvocation: (_, _) => Task.CompletedTask); - var registeredTimeouts = new RegisteredTimeouts(storedId, functionStore.TimeoutStore, CreateEffect(storedId, flowId, functionStore)); var messagesPullerAndEmitter = new MessagesPullerAndEmitter( storedId, defaultDelay: TimeSpan.FromMilliseconds(250), @@ -364,10 +338,9 @@ await functionStore.CreateFunction( isWorkflowRunning: () => true, functionStore, DefaultSerializer.Instance, - registeredTimeouts, initialMessages: [] ); - var messages = new Messages(messagesWriter, registeredTimeouts, messagesPullerAndEmitter); + var messages = new Messages(messagesWriter, messagesPullerAndEmitter); var task = messages.Take(2).ToList(); @@ -410,7 +383,6 @@ await functionStore.CreateFunction( parent: null ); var messagesWriter = new MessageWriter(storedId, functionStore, DefaultSerializer.Instance, scheduleReInvocation: (_, _) => Task.CompletedTask); - var registeredTimeouts = new RegisteredTimeouts(storedId, functionStore.TimeoutStore, CreateEffect(storedId, flowId, functionStore)); var messagesPullerAndEmitter = new MessagesPullerAndEmitter( storedId, defaultDelay: TimeSpan.FromMilliseconds(250), @@ -418,10 +390,9 @@ await functionStore.CreateFunction( isWorkflowRunning: () => true, functionStore, new ExceptionThrowingEventSerializer(typeof(int)), - registeredTimeouts, initialMessages: [] ); - var messages = new Messages(messagesWriter, registeredTimeouts, messagesPullerAndEmitter); + var messages = new Messages(messagesWriter, messagesPullerAndEmitter); await messages.AppendMessage("hello world"); await Should.ThrowAsync(messages.AppendMessage(1)); diff --git a/Core/Cleipnir.ResilientFunctions.Tests/ReactiveTests/LeafOperatorsTests.cs b/Core/Cleipnir.ResilientFunctions.Tests/ReactiveTests/LeafOperatorsTests.cs index 7bbca18c..10dfb9cb 100644 --- a/Core/Cleipnir.ResilientFunctions.Tests/ReactiveTests/LeafOperatorsTests.cs +++ b/Core/Cleipnir.ResilientFunctions.Tests/ReactiveTests/LeafOperatorsTests.cs @@ -5,12 +5,11 @@ using System.Threading.Tasks; using Cleipnir.ResilientFunctions.Domain; using Cleipnir.ResilientFunctions.Domain.Events; -using Cleipnir.ResilientFunctions.Domain.Exceptions; using Cleipnir.ResilientFunctions.Domain.Exceptions.Commands; using Cleipnir.ResilientFunctions.Helpers; using Cleipnir.ResilientFunctions.Reactive.Extensions; -using Cleipnir.ResilientFunctions.Reactive.Origin; using Cleipnir.ResilientFunctions.Reactive.Utilities; +using Cleipnir.ResilientFunctions.Storage; using Microsoft.VisualStudio.TestTools.UnitTesting; using Shouldly; @@ -119,18 +118,34 @@ await Should.ThrowAsync( [TestMethod] public async Task FirstOperatorWithSuspensionAndTimeoutEventThrowsSuspensionExceptionWhenNothingIsSignalled() { - var source = new TestSource(); - var timeoutEventId = "TimeoutEventId".ToEffectId(); var expiresAt = DateTime.UtcNow.AddDays(1); - source.SignalNext(new TimeoutEvent("OtherEventId".ToEffectId(), expiresAt)); - - var nextOrSuspend = source - .TakeUntilTimeout(timeoutEventId, expiresAt) - .OfType() - .First(maxWait: TimeSpan.Zero); - - await Should.ThrowAsync(nextOrSuspend); + using var registry = new FunctionsRegistry(new InMemoryFunctionStore()); + var registration = registry.RegisterFunc( + "SomeFlowType", + async Task (_, workflow) => + { + var timeoutEventId = "TimeoutEventId"; + await workflow.Messages.AppendMessage(new TimeoutEvent("OtherEventId".ToEffectId(), expiresAt)); + try + { + await workflow + .Messages + .TakeUntilTimeout(timeoutEventId, expiresAt) + .OfType() + .First(maxWait: TimeSpan.Zero); + } + catch (PostponeInvocationException e) + { + return e.PostponeUntil; + } + + return null; + } + ); + + var result = await registration.Invoke("SomeFlowInstance", "SomeParam"); + result.ShouldBe(expiresAt); } [TestMethod] @@ -412,17 +427,32 @@ public async Task LastOperatorWithSuspensionAndTimeoutThrowsNoResultExceptionWhe [TestMethod] public async Task LastOperatorWithSuspensionAndTimeoutEventThrowsSuspensionExceptionWhenNothingIsSignalled() { - var source = new TestSource(); - var timeoutEventId = "TimeoutEventId".ToEffectId(); var expiresAt = DateTime.UtcNow.AddDays(1); - - source.SignalNext(new TimeoutEvent("OtherEventId".ToEffectId(), expiresAt)); - - var nextOrSuspend = source - .TakeUntilTimeout(timeoutEventId, expiresAt) - .Last(maxWait: TimeSpan.Zero); - - await Should.ThrowAsync(nextOrSuspend); + using var registry = new FunctionsRegistry(new InMemoryFunctionStore()); + var registration = registry.RegisterFunc( + "SomeFlowType", + async Task (_, workflow) => + { + var timeoutEventId = "TimeoutEventId"; + await workflow.Messages.AppendMessage(new TimeoutEvent("OtherEventId".ToEffectId(), expiresAt)); + try + { + await workflow + .Messages + .TakeUntilTimeout(timeoutEventId, expiresAt) + .Last(maxWait: TimeSpan.Zero); + } + catch (PostponeInvocationException e) + { + return e.PostponeUntil; + } + + return null; + } + ); + + var result = await registration.Invoke("SomeFlowInstance", "SomeParam"); + result.ShouldBe(expiresAt); } [TestMethod] diff --git a/Core/Cleipnir.ResilientFunctions.Tests/ReactiveTests/NoOpTimeoutProvider.cs b/Core/Cleipnir.ResilientFunctions.Tests/ReactiveTests/NoOpTimeoutProvider.cs deleted file mode 100644 index 2d7699b9..00000000 --- a/Core/Cleipnir.ResilientFunctions.Tests/ReactiveTests/NoOpTimeoutProvider.cs +++ /dev/null @@ -1,25 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Threading.Tasks; -using Cleipnir.ResilientFunctions.CoreRuntime; -using Cleipnir.ResilientFunctions.Domain; -using Cleipnir.ResilientFunctions.Helpers; - -namespace Cleipnir.ResilientFunctions.Tests.ReactiveTests; - -public class NoOpRegisteredTimeouts : IRegisteredTimeouts -{ - public static NoOpRegisteredTimeouts Instance { get; } = new(); - public Task RegisterTimeout(EffectId timeoutId, DateTime expiresAt) - => Task.CompletedTask; - - public Task RegisterTimeout(EffectId timeoutId, TimeSpan expiresIn) - => Task.CompletedTask; - - public Task CancelTimeout(EffectId timeoutId) - => Task.CompletedTask; - - public Task> PendingTimeouts() => new List() - .CastTo>() - .ToTask(); -} \ No newline at end of file diff --git a/Core/Cleipnir.ResilientFunctions.Tests/ReactiveTests/ReactiveIntegrationTests.cs b/Core/Cleipnir.ResilientFunctions.Tests/ReactiveTests/ReactiveIntegrationTests.cs index cd4f8d7a..93ee1e9c 100644 --- a/Core/Cleipnir.ResilientFunctions.Tests/ReactiveTests/ReactiveIntegrationTests.cs +++ b/Core/Cleipnir.ResilientFunctions.Tests/ReactiveTests/ReactiveIntegrationTests.cs @@ -29,7 +29,7 @@ public async Task FunctionCanBeSuspendedForASecondSuccessfully() await messages.SuspendFor(timeoutEventId: "timeout", resumeAfter: TimeSpan.FromSeconds(1)); }); - await Should.ThrowAsync(rAction.Invoke(flowInstance.Value, "param")); + await Should.ThrowAsync(rAction.Invoke(flowInstance.Value, "param")); await BusyWait.Until(() => store.GetFunction(rAction.MapToStoredId(functionId.Instance)).SelectAsync(sf => sf?.Status == Status.Succeeded) @@ -42,7 +42,6 @@ public async Task SyncingStopsAfterReactiveChainCompletion() var counter = new SyncedCounter(); var source = new TestSource( - NoOpRegisteredTimeouts.Instance, syncStore: _ => { counter.Increment(); diff --git a/Core/Cleipnir.ResilientFunctions.Tests/ReactiveTests/TestSource.cs b/Core/Cleipnir.ResilientFunctions.Tests/ReactiveTests/TestSource.cs index 0a92eb18..146fa6b3 100644 --- a/Core/Cleipnir.ResilientFunctions.Tests/ReactiveTests/TestSource.cs +++ b/Core/Cleipnir.ResilientFunctions.Tests/ReactiveTests/TestSource.cs @@ -1,6 +1,5 @@ using System; using System.Threading.Tasks; -using Cleipnir.ResilientFunctions.CoreRuntime; using Cleipnir.ResilientFunctions.Reactive; using Cleipnir.ResilientFunctions.Reactive.Origin; @@ -8,8 +7,7 @@ namespace Cleipnir.ResilientFunctions.Tests.ReactiveTests; public class TestSource : Source { - public TestSource(IRegisteredTimeouts? registeredTimeouts = null, SyncStore? syncStore = null, TimeSpan? maxWait = null) : base( - registeredTimeouts ?? NoOpRegisteredTimeouts.Instance, + public TestSource(SyncStore? syncStore = null, TimeSpan? maxWait = null) : base( syncStore: syncStore ?? (_ => Task.CompletedTask), defaultDelay: TimeSpan.FromMilliseconds(10), defaultMaxWait: maxWait ?? TimeSpan.MaxValue, diff --git a/Core/Cleipnir.ResilientFunctions.Tests/ReactiveTests/TimeoutTests.cs b/Core/Cleipnir.ResilientFunctions.Tests/ReactiveTests/TimeoutTests.cs index 6e9811de..feb308d1 100644 --- a/Core/Cleipnir.ResilientFunctions.Tests/ReactiveTests/TimeoutTests.cs +++ b/Core/Cleipnir.ResilientFunctions.Tests/ReactiveTests/TimeoutTests.cs @@ -1,14 +1,13 @@ using System; -using System.Collections.Generic; using System.Linq; -using System.Threading; using System.Threading.Tasks; -using Cleipnir.ResilientFunctions.CoreRuntime; using Cleipnir.ResilientFunctions.Domain; using Cleipnir.ResilientFunctions.Domain.Events; using Cleipnir.ResilientFunctions.Helpers; using Cleipnir.ResilientFunctions.Reactive.Extensions; using Cleipnir.ResilientFunctions.Reactive.Utilities; +using Cleipnir.ResilientFunctions.Storage; +using Cleipnir.ResilientFunctions.Tests.Utils; using Microsoft.VisualStudio.TestTools.UnitTesting; using Shouldly; @@ -17,149 +16,115 @@ namespace Cleipnir.ResilientFunctions.Tests.ReactiveTests; [TestClass] public class TimeoutTests { + //rewrite into integration tests [TestMethod] public async Task StreamCompletesAndThrowsNoResultExceptionAfterFiredTimeoutEvent() { - var timeoutId = "TimeoutId".ToEffectId(); + var timeoutId = "TimeoutId"; var expiresAt = DateTime.UtcNow.Add(TimeSpan.FromMinutes(15)); - var registeredTimeoutsStub = new RegisteredTimeoutsStub(); - var source = new TestSource(registeredTimeoutsStub); - - var task = source.TakeUntilTimeout(timeoutId, expiresAt).First(); + using var registry = new FunctionsRegistry(new InMemoryFunctionStore()); + var flow = registry.RegisterFunc("Flow", + async (_, workflow) => + { + var messages = workflow.Messages; + try + { + await messages.TakeUntilTimeout(timeoutId, expiresAt).First(maxWait: TimeSpan.FromSeconds(5)); + } + catch (NoResultException) + { + return true; + } + + return false; + }); + + var scheduled = await flow.Schedule("Instance", param: ""); - await BusyWait.Until(() => registeredTimeoutsStub.Registrations.Any()); + var cp = await flow.ControlPanel("Instance").ShouldNotBeNullAsync(); + await cp.BusyWaitUntil(c => c.Effects.AllIds.SelectAsync(ids => ids.Any())); - var (id, expiry) = registeredTimeoutsStub.Registrations.Single(); - id.ShouldBe(timeoutId); - expiry.ShouldBe(expiresAt); + var timeoutEffectId = (await cp.Effects.AllIds).Single(eId => eId.Id == timeoutId); + var effectTimeout = await cp.Effects.GetValue(timeoutEffectId); - source.SignalNext(new TimeoutEvent(timeoutId, expiresAt)); + effectTimeout.ShouldBe(expiresAt); - await BusyWait.Until(() => task.IsCompleted); - task.IsCompleted.ShouldBeTrue(); - - await Should.ThrowAsync(task); + await flow.SendMessage("Instance", new TimeoutEvent(timeoutEffectId, effectTimeout)); + var result = await scheduled.Completion(); + result.ShouldBeTrue(); } + [TestMethod] public async Task StreamCompletesAndReturnsNothingAfterFiredTimeoutEvent() { - var timeoutId = "TimeoutId".ToEffectId(); + var timeoutId = "TimeoutId"; + var effectId = new EffectId(timeoutId, EffectType.Timeout, Context: ""); var expiresAt = DateTime.UtcNow.Add(TimeSpan.FromMinutes(15)); - - var registeredTimeoutsStub = new RegisteredTimeoutsStub(); - var source = new TestSource(registeredTimeoutsStub); + + using var registry = new FunctionsRegistry(new InMemoryFunctionStore()); + var flow = registry.RegisterFunc>("Flow", + async (_, workflow) => + { + var messages = workflow.Messages; + return await messages.TakeUntilTimeout(timeoutId, expiresAt).FirstOrNone(); + }); - var task = source.TakeUntilTimeout(timeoutId, expiresAt).FirstOrNone(); - - await BusyWait.Until(() => registeredTimeoutsStub.Registrations.Any()); + var scheduled = await flow.Schedule("Instance", param: ""); - source.SignalNext(new TimeoutEvent(timeoutId, expiresAt)); + var cp = await flow.ControlPanel("Instance").ShouldNotBeNullAsync(); + await cp.BusyWaitUntil(c => c.Effects.AllIds.SelectAsync(ids => ids.Any())); - await BusyWait.Until(() => task.IsCompleted); - task.IsCompletedSuccessfully.ShouldBeTrue(); + var effectExpiresAt = await cp.Effects.GetValue(effectId with { Id = effectId.Id + "_Expires"}); + effectExpiresAt.ShouldBe(expiresAt); - var option = await task; - option.HasValue.ShouldBeFalse(); - - var (id, expiry) = registeredTimeoutsStub.Registrations.Single(); - id.ShouldBe(timeoutId); - expiry.ShouldBe(expiresAt); + await flow.SendMessage("Instance", new TimeoutEvent(effectId, expiresAt)); + + var result = await scheduled.Completion(); + result.HasValue.ShouldBeFalse(); } [TestMethod] public async Task StreamCompletesSuccessfullyWhenEventSupersedesTimeout() { - var timeoutId = "TimeoutId".ToEffectId(); + var timeoutId = "TimeoutId"; var expiresAt = DateTime.UtcNow.Add(TimeSpan.FromMinutes(15)); - - var registeredTimeoutsStub = new RegisteredTimeoutsStub(); - var source = new TestSource(registeredTimeoutsStub); + + using var registry = new FunctionsRegistry(new InMemoryFunctionStore()); + var flow = registry.RegisterFunc("Flow", + async (_, workflow) => + { + var messages = workflow.Messages; + return await messages.TakeUntilTimeout(timeoutId, expiresAt).FirstOfType(); + }); - var task = source.TakeUntilTimeout(timeoutId, expiresAt).First(); - - source.SignalNext("Hello"); + var scheduled = await flow.Schedule("Instance", param: ""); + await flow.SendMessage("Instance", "Hello"); - await BusyWait.Until(() => task.IsCompleted); - task.IsCompletedSuccessfully.ShouldBeTrue(); - task.Result.ShouldBe("Hello"); + var result = await scheduled.Completion(); + result.ShouldBe("Hello"); } [TestMethod] public async Task StreamCompletesSuccessfullyWithValuedOptionWhenEventSupersedesTimeout() { - var timeoutId = "TimeoutId".ToEffectId(); - var expiresAt = DateTime.UtcNow.Add(TimeSpan.FromMinutes(15)); - - var registeredTimeoutsStub = new RegisteredTimeoutsStub(); - var source = new TestSource(registeredTimeoutsStub); - - var task = source.TakeUntilTimeout(timeoutId, expiresAt).FirstOrNone(); - - source.SignalNext("Hello"); - - await BusyWait.Until(() => task.IsCompleted); - task.IsCompletedSuccessfully.ShouldBeTrue(); - var option = task.Result; - option.HasValue.ShouldBeTrue(); - option.Value.ShouldBe("Hello"); - - registeredTimeoutsStub.Cancelled.ShouldBe(timeoutId); - } - - [TestMethod] - public async Task ExistingTimeoutEventInMessagesAvoidRegisteredTimeoutsCancellation() - { - var timeoutId = "TimeoutId".ToEffectId(); + var timeoutId = "TimeoutId"; var expiresAt = DateTime.UtcNow.Add(TimeSpan.FromMinutes(15)); - var registeredTimeoutsStub = new RegisteredTimeoutsStub(); - var source = new TestSource(registeredTimeoutsStub); - source.SignalNext(new TimeoutEvent(timeoutId, expiresAt)); - - var task = await source.TakeUntilTimeout(timeoutId, expiresAt).FirstOrNone(); - task.HasValue.ShouldBeFalse(); - - registeredTimeoutsStub.Cancelled.ShouldBeNull(); - } - - private class RegisteredTimeoutsStub : IRegisteredTimeouts - { - public List> Registrations - { - get + using var registry = new FunctionsRegistry(new InMemoryFunctionStore()); + var flow = registry.RegisterFunc>("Flow", + async (_, workflow) => { - lock (_sync) - return _registrations - .Select(kv => Tuple.Create(kv.Key, kv.Value)) - .ToList(); - } - } + var messages = workflow.Messages; + return await messages.TakeUntilTimeout(timeoutId, expiresAt).OfType().FirstOrNone(); + }); - public volatile EffectId? Cancelled = null; + var scheduled = await flow.Schedule("Instance", param: ""); + await flow.SendMessage("Instance", "Hello"); - private readonly Lock _sync = new(); - private readonly Dictionary _registrations = new(); - - public Task RegisterTimeout(EffectId timeoutId, DateTime expiresAt) - { - lock (_sync) - _registrations[timeoutId] = expiresAt; - - return Task.CompletedTask; - } - - public Task RegisterTimeout(EffectId timeoutId, TimeSpan expiresIn) - => RegisterTimeout(timeoutId, DateTime.UtcNow.Add(expiresIn)); - - public Task CancelTimeout(EffectId timeoutId) - { - Cancelled = timeoutId; - return Task.CompletedTask; - } - - public Task> PendingTimeouts() - => Task.FromException>(new Exception("Stub-method invocation")); + var result = await scheduled.Completion(); + result.HasValue.ShouldBeTrue(); + result.Value.ShouldBe("Hello"); } } \ No newline at end of file diff --git a/Core/Cleipnir.ResilientFunctions.Tests/TestTemplates/FunctionTests/ControlPanelTests.cs b/Core/Cleipnir.ResilientFunctions.Tests/TestTemplates/FunctionTests/ControlPanelTests.cs index dbe54c97..78f08efa 100644 --- a/Core/Cleipnir.ResilientFunctions.Tests/TestTemplates/FunctionTests/ControlPanelTests.cs +++ b/Core/Cleipnir.ResilientFunctions.Tests/TestTemplates/FunctionTests/ControlPanelTests.cs @@ -35,7 +35,6 @@ protected async Task ExistingActionCanBeDeletedFromControlPanel(Task(); state.Value = "State"; - await workflow.Messages.RegisteredTimeouts.RegisterTimeout("Timeout", TimeSpan.FromDays(1)); await state.Save(); } ); @@ -97,7 +96,6 @@ async Task(string _, Workflow workflow) => var state = await states.CreateOrGetDefault(); state.Value = "State"; await state.Save(); - await workflow.Messages.RegisteredTimeouts.RegisterTimeout("Timeout", TimeSpan.FromDays(1)); return "hello"; }); @@ -1525,94 +1523,6 @@ protected async Task SaveChangesPersistsChangedResult(Task store unhandledExceptionCatcher.ShouldNotHaveExceptions(); } - public abstract Task ExistingTimeoutCanBeUpdatedForAction(); - protected async Task ExistingTimeoutCanBeUpdatedForAction(Task storeTask) - { - var unhandledExceptionCatcher = new UnhandledExceptionCatcher(); - - var store = await storeTask; - var functionId = TestFlowId.Create(); - var (flowType, flowInstance) = functionId; - using var functionsRegistry = new FunctionsRegistry(store, new Settings(unhandledExceptionCatcher.Catch)); - - var actionRegistration = functionsRegistry.RegisterAction( - flowType, - Task (string param, Workflow workflow) => - workflow.Messages.RegisteredTimeouts.RegisterTimeout( - "someTimeoutId", - expiresAt: new DateTime(2100, 1,1, 1,1,1, DateTimeKind.Utc) - ) - ); - - await actionRegistration.Invoke(flowInstance.Value, param: "param"); - - var controlPanel = await actionRegistration.ControlPanel(flowInstance.Value); - controlPanel.ShouldNotBeNull(); - var timeouts = controlPanel.RegisteredTimeouts; - (await timeouts.All).Count.ShouldBe(1); - await timeouts["someTimeoutId"].ShouldBeAsync(new DateTime(2100, 1,1, 1,1,1, DateTimeKind.Utc)); - - await timeouts.Upsert("someOtherTimeoutId", new DateTime(2101, 1, 1, 1, 1, 1, DateTimeKind.Utc)); - (await timeouts.All).Count.ShouldBe(2); - await timeouts["someOtherTimeoutId"].ShouldBeAsync(new DateTime(2101, 1,1, 1,1,1, DateTimeKind.Utc)); - - await timeouts.Remove("someTimeoutId"); - (await timeouts.All).Count.ShouldBe(1); - - await controlPanel.Refresh(); - - (await timeouts.All).Count.ShouldBe(1); - await timeouts["someOtherTimeoutId"].ShouldBeAsync(new DateTime(2101, 1,1, 1,1,1, DateTimeKind.Utc)); - - unhandledExceptionCatcher.ShouldNotHaveExceptions(); - } - - public abstract Task ExistingTimeoutCanBeUpdatedForFunc(); - protected async Task ExistingTimeoutCanBeUpdatedForFunc(Task storeTask) - { - var unhandledExceptionCatcher = new UnhandledExceptionCatcher(); - - var store = await storeTask; - var functionId = TestFlowId.Create(); - var (flowType, flowInstance) = functionId; - using var functionsRegistry = new FunctionsRegistry(store, new Settings(unhandledExceptionCatcher.Catch)); - - var funcRegistration = functionsRegistry.RegisterFunc( - flowType, - async Task (string param, Workflow workflow) => - { - await workflow.Messages.RegisteredTimeouts.RegisterTimeout( - "someTimeoutId", - expiresAt: new DateTime(2100, 1, 1, 1, 1, 1, DateTimeKind.Utc) - ); - - return param; - } - ); - - await funcRegistration.Invoke(flowInstance.Value, param: "param"); - - var controlPanel = await funcRegistration.ControlPanel(flowInstance.Value); - controlPanel.ShouldNotBeNull(); - var timeouts = controlPanel.RegisteredTimeouts; - (await timeouts.All).Count.ShouldBe(1); - await timeouts["someTimeoutId"].ShouldBeAsync(new DateTime(2100, 1,1, 1,1,1, DateTimeKind.Utc)); - - await timeouts.Upsert("someOtherTimeoutId", new DateTime(2101, 1, 1, 1, 1, 1, DateTimeKind.Utc)); - (await timeouts.All).Count.ShouldBe(2); - await timeouts["someOtherTimeoutId"].ShouldBeAsync(new DateTime(2101, 1,1, 1,1,1, DateTimeKind.Utc)); - - await timeouts.Remove("someTimeoutId"); - (await timeouts.All).Count.ShouldBe(1); - - await controlPanel.Refresh(); - - (await timeouts.All).Count.ShouldBe(1); - await timeouts["someOtherTimeoutId"].ShouldBeAsync(new DateTime(2101, 1,1, 1,1,1, DateTimeKind.Utc)); - - unhandledExceptionCatcher.ShouldNotHaveExceptions(); - } - public abstract Task CorrelationsCanBeChanged(); protected async Task CorrelationsCanBeChanged(Task storeTask) { @@ -1673,7 +1583,6 @@ protected async Task DeleteRemovesFunctionFromAllStores(Task sto await controlPanel.Effects.SetSucceeded("SomeEffect"); await controlPanel.States.Set("SomeStateId", new TestState()); await controlPanel.Messages.Append("Some Message"); - await controlPanel.RegisteredTimeouts.Upsert("SomeTimeout", expiresAt: DateTime.UtcNow.Add(TimeSpan.FromDays(1))); await controlPanel.Delete(); diff --git a/Core/Cleipnir.ResilientFunctions.Tests/TestTemplates/FunctionTests/EffectTests.cs b/Core/Cleipnir.ResilientFunctions.Tests/TestTemplates/FunctionTests/EffectTests.cs index 527e31a9..33d16468 100644 --- a/Core/Cleipnir.ResilientFunctions.Tests/TestTemplates/FunctionTests/EffectTests.cs +++ b/Core/Cleipnir.ResilientFunctions.Tests/TestTemplates/FunctionTests/EffectTests.cs @@ -1,10 +1,13 @@ using System; using System.Collections.Generic; using System.Linq; +using System.Threading; using System.Threading.Tasks; using Cleipnir.ResilientFunctions.CoreRuntime.Invocation; using Cleipnir.ResilientFunctions.CoreRuntime.Serialization; using Cleipnir.ResilientFunctions.Domain; +using Cleipnir.ResilientFunctions.Domain.Exceptions; +using Cleipnir.ResilientFunctions.Domain.Exceptions.Commands; using Cleipnir.ResilientFunctions.Helpers; using Cleipnir.ResilientFunctions.Reactive.Utilities; using Cleipnir.ResilientFunctions.Storage; @@ -287,6 +290,61 @@ async Task (string param, Workflow workflow) => storedEffect.Result!.ToStringFromUtf8Bytes().DeserializeFromJsonTo().ShouldBe(new [] {1, 2}); } + public abstract Task TaskWhenAnyPostponeTest(); + public async Task TaskWhenAnyPostponeTest(Task storeTask) + { + var store = await storeTask; + using var functionsRegistry = new FunctionsRegistry(store); + var flowId = TestFlowId.Create(); + var (flowType, flowInstance) = flowId; + var postponeUntil = DateTime.UtcNow; + var rAction = functionsRegistry.RegisterFunc( + flowType, + async Task (string param, Workflow workflow) => + { + var (effect, _, _) = workflow; + var t1 = Task.Delay(Timeout.Infinite).ContinueWith(_ => 1); + var t2 = Task.FromException(new PostponeInvocationException(postponeUntil)); + return await effect.WhenAny("WhenAll", t1, t2); + }); + + await Should.ThrowAsync( + () => rAction.Invoke(flowInstance.ToString(), param: "hello") + ); + } + + public abstract Task TaskWhenAllPostponeTest(); + public async Task TaskWhenAllPostponeTest(Task storeTask) + { + var store = await storeTask; + using var functionsRegistry = new FunctionsRegistry(store); + var flowId = TestFlowId.Create(); + var (flowType, flowInstance) = flowId; + var postponeUntil = DateTime.UtcNow; + var rAction = functionsRegistry.RegisterParamless( + flowType, + async Task (workflow) => + { + var (effect, _, _) = workflow; + var t1 = Task.FromException(new PostponeInvocationException(DateTime.MaxValue)); + var t2 = Task.FromException(new PostponeInvocationException(postponeUntil)); + + try + { + await effect.WhenAll("WhenAll", t1, t2); + } + catch (PostponeInvocationException e) + { + e.PostponeUntil.ShouldBe(postponeUntil); + throw; + } + }); + + await Should.ThrowAsync( + () => rAction.Invoke(flowInstance.ToString()) + ); + } + public abstract Task ClearEffectsTest(); public async Task ClearEffectsTest(Task storeTask) { @@ -359,6 +417,36 @@ public async Task EffectsCrudTest(Task storeTask) await effect.Contains("Id1").ShouldBeTrueAsync(); } + public abstract Task EffectsCreateOrGetWithoutFlushTest(); + public async Task EffectsCreateOrGetWithoutFlushTest(Task storeTask) + { + var store = await storeTask; + var storedId = TestStoredId.Create(); + var effectResults = new EffectResults( + TestFlowId.Create(), + storedId, + lazyExistingEffects: new Lazy>>(() => store.EffectsStore.GetEffectResults(storedId)), + store.EffectsStore, + DefaultSerializer.Instance + ); + var effect = new Effect(effectResults); + + var id = await effect.CreateOrGet("EffectId", Guid.NewGuid(), flush: false); + var id2 = await effect.CreateOrGet("EffectId", Guid.NewGuid(), flush: false); + id2.ShouldBe(id); + + effectResults.HasPendingChanges.ShouldBeTrue(); + await store.EffectsStore.GetEffectResults(storedId).SelectAsync(e => e.Any()).ShouldBeFalseAsync(); + await effect.Flush(); + + effectResults.HasPendingChanges.ShouldBeFalse(); + var storedEffects = await store.EffectsStore.GetEffectResults(storedId); + storedEffects.Count.ShouldBe(1); + var storedEffect = storedEffects.Single(); + var storedGuid = storedEffect.Result!.ToStringFromUtf8Bytes().DeserializeFromJsonTo(); + storedGuid.ShouldBe(id); + } + public abstract Task ExistingEffectsFuncIsOnlyInvokedAfterGettingValue(); public async Task ExistingEffectsFuncIsOnlyInvokedAfterGettingValue(Task storeTask) { diff --git a/Core/Cleipnir.ResilientFunctions.Tests/TestTemplates/FunctionTests/MessagingTests.cs b/Core/Cleipnir.ResilientFunctions.Tests/TestTemplates/FunctionTests/MessagingTests.cs index 8614d2e7..24229e89 100644 --- a/Core/Cleipnir.ResilientFunctions.Tests/TestTemplates/FunctionTests/MessagingTests.cs +++ b/Core/Cleipnir.ResilientFunctions.Tests/TestTemplates/FunctionTests/MessagingTests.cs @@ -105,7 +105,7 @@ public async Task TimeoutEventCausesSuspendedFunctionToBeReInvoked(Task(() => + await Should.ThrowAsync(() => rFunc.Invoke(functionId.Instance.Value, "") ); diff --git a/Core/Cleipnir.ResilientFunctions.Tests/TestTemplates/FunctionTests/PostponedTests.cs b/Core/Cleipnir.ResilientFunctions.Tests/TestTemplates/FunctionTests/PostponedTests.cs index 55f601fd..603b9de6 100644 --- a/Core/Cleipnir.ResilientFunctions.Tests/TestTemplates/FunctionTests/PostponedTests.cs +++ b/Core/Cleipnir.ResilientFunctions.Tests/TestTemplates/FunctionTests/PostponedTests.cs @@ -692,7 +692,6 @@ await store.PostponeFunction( ignoreInterrupted: false, expectedEpoch: 0, effects: null, - messages: null, complimentaryState: new ComplimentaryState(storedParameter.ToUtf8Bytes().ToFunc(), LeaseLength: 0) ).ShouldBeTrueAsync(); diff --git a/Core/Cleipnir.ResilientFunctions.Tests/TestTemplates/FunctionTests/SunshineTests.cs b/Core/Cleipnir.ResilientFunctions.Tests/TestTemplates/FunctionTests/SunshineTests.cs index 90dbe0be..825d313c 100644 --- a/Core/Cleipnir.ResilientFunctions.Tests/TestTemplates/FunctionTests/SunshineTests.cs +++ b/Core/Cleipnir.ResilientFunctions.Tests/TestTemplates/FunctionTests/SunshineTests.cs @@ -4,6 +4,7 @@ using Cleipnir.ResilientFunctions.CoreRuntime; using Cleipnir.ResilientFunctions.CoreRuntime.Invocation; using Cleipnir.ResilientFunctions.Domain; +using Cleipnir.ResilientFunctions.Domain.Exceptions.Commands; using Cleipnir.ResilientFunctions.Helpers; using Cleipnir.ResilientFunctions.Messaging; using Cleipnir.ResilientFunctions.Reactive.Extensions; @@ -716,4 +717,125 @@ public async Task FunctionCanAcceptAndReturnOptionType(Task stor unhandledExceptionHandler.ShouldNotHaveExceptions(); } + + public abstract Task PendingEffectChangesArePersistedWithSucceedFunctionResult(); + public async Task PendingEffectChangesArePersistedWithSucceedFunctionResult(Task storeTask) + { + var store = await storeTask; + var flowId = TestFlowId.Create(); + + var unhandledExceptionHandler = new UnhandledExceptionCatcher(); + using var functionsRegistry = new FunctionsRegistry(store, new Settings(unhandledExceptionHandler.Catch)); + var registration = functionsRegistry + .RegisterParamless( + flowId.Type, + async Task (workflow) => + { + await workflow + .Effect + .Capture("SomeEffect", () => "SomeEffectValue", ResiliencyLevel.AtLeastOnceDelayFlush); + } + ); + + await registration.Invoke(flowId.Instance); + var cp = await registration.ControlPanel(flowId.Instance).ShouldNotBeNullAsync(); + cp.Status.ShouldBe(Status.Succeeded); + var effectValue = await cp.Effects.GetValue("SomeEffect"); + effectValue.ShouldBe(effectValue); + + unhandledExceptionHandler.ShouldNotHaveExceptions(); + } + + public abstract Task PendingEffectChangesArePersistedWithPostponedFunctionResult(); + public async Task PendingEffectChangesArePersistedWithPostponedFunctionResult(Task storeTask) + { + var store = await storeTask; + var flowId = TestFlowId.Create(); + + var unhandledExceptionHandler = new UnhandledExceptionCatcher(); + using var functionsRegistry = new FunctionsRegistry(store, new Settings(unhandledExceptionHandler.Catch)); + var registration = functionsRegistry + .RegisterParamless( + flowId.Type, + async Task (workflow) => + { + await workflow + .Effect + .Capture("SomeEffect", () => "SomeEffectValue", ResiliencyLevel.AtLeastOnceDelayFlush); + + await workflow.Delay(TimeSpan.FromHours(1)); + } + ); + + await Safe.Try(() => registration.Invoke(flowId.Instance)); + + var cp = await registration.ControlPanel(flowId.Instance).ShouldNotBeNullAsync(); + cp.Status.ShouldBe(Status.Postponed); + var effectValue = await cp.Effects.GetValue("SomeEffect"); + effectValue.ShouldBe(effectValue); + + unhandledExceptionHandler.ShouldNotHaveExceptions(); + } + + public abstract Task PendingEffectChangesArePersistedWithSuspendedFunctionResult(); + public async Task PendingEffectChangesArePersistedWithSuspendedFunctionResult(Task storeTask) + { + var store = await storeTask; + var flowId = TestFlowId.Create(); + + var unhandledExceptionHandler = new UnhandledExceptionCatcher(); + using var functionsRegistry = new FunctionsRegistry(store, new Settings(unhandledExceptionHandler.Catch)); + var registration = functionsRegistry + .RegisterParamless( + flowId.Type, + async Task (workflow) => + { + await workflow + .Effect + .Capture("SomeEffect", () => "SomeEffectValue", ResiliencyLevel.AtLeastOnceDelayFlush); + + throw new SuspendInvocationException(); + } + ); + + await Safe.Try(() => registration.Invoke(flowId.Instance)); + + var cp = await registration.ControlPanel(flowId.Instance).ShouldNotBeNullAsync(); + cp.Status.ShouldBe(Status.Suspended); + var effectValue = await cp.Effects.GetValue("SomeEffect"); + effectValue.ShouldBe(effectValue); + + unhandledExceptionHandler.ShouldNotHaveExceptions(); + } + + public abstract Task PendingEffectChangesArePersistedWithFailedFunctionResult(); + public async Task PendingEffectChangesArePersistedWithFailedFunctionResult(Task storeTask) + { + var store = await storeTask; + var flowId = TestFlowId.Create(); + + var unhandledExceptionHandler = new UnhandledExceptionCatcher(); + using var functionsRegistry = new FunctionsRegistry(store, new Settings(unhandledExceptionHandler.Catch)); + var registration = functionsRegistry + .RegisterParamless( + flowId.Type, + async Task (workflow) => + { + await workflow + .Effect + .Capture("SomeEffect", () => "SomeEffectValue", ResiliencyLevel.AtLeastOnceDelayFlush); + + throw new TimeoutException(); + } + ); + + await Safe.Try(() => registration.Invoke(flowId.Instance)); + + var cp = await registration.ControlPanel(flowId.Instance).ShouldNotBeNullAsync(); + cp.Status.ShouldBe(Status.Failed); + var effectValue = await cp.Effects.GetValue("SomeEffect"); + effectValue.ShouldBe(effectValue); + + unhandledExceptionHandler.ShouldNotHaveExceptions(); + } } \ No newline at end of file diff --git a/Core/Cleipnir.ResilientFunctions.Tests/TestTemplates/FunctionTests/TimeoutTests.cs b/Core/Cleipnir.ResilientFunctions.Tests/TestTemplates/FunctionTests/TimeoutTests.cs index 720ff242..9bed207c 100644 --- a/Core/Cleipnir.ResilientFunctions.Tests/TestTemplates/FunctionTests/TimeoutTests.cs +++ b/Core/Cleipnir.ResilientFunctions.Tests/TestTemplates/FunctionTests/TimeoutTests.cs @@ -35,11 +35,12 @@ protected async Task ExpiredTimeoutIsAddedToMessages(Task storeT inner: async Task (string _, Workflow workflow) => { var messages = workflow.Messages; - var timeoutTask = messages.OfType().First(); - await messages.RegisteredTimeouts.RegisterTimeout("test", expiresIn: TimeSpan.FromMilliseconds(500)); + + var timeoutTask = messages.OfType().First(maxWait: TimeSpan.FromSeconds(10)); timeoutTask.IsCompleted.ShouldBeFalse(); + await messages.TakeUntilTimeout("TimeoutId", TimeSpan.FromMilliseconds(500)).Completion(); var timeout = await timeoutTask; - timeout.TimeoutId.Id.ShouldBe("test"); + timeout.TimeoutId.Id.ShouldBe("TimeoutId"); } ).Invoke; @@ -69,7 +70,7 @@ protected async Task ExpiredTimeoutMakesReactiveChainThrowTimeoutException(Task< var messages = workflow.Messages; await messages .TakeUntilTimeout("TimeoutId#21", expiresIn: TimeSpan.FromMilliseconds(500)) - .FirstOfType(); + .FirstOfType(maxWait: TimeSpan.FromSeconds(5)); } ).Invoke; @@ -80,160 +81,6 @@ await Should.ThrowAsync>( unhandledExceptionHandler.ThrownExceptions.Count.ShouldBe(0); } - public abstract Task RegisteredTimeoutIsCancelledAfterReactiveChainCompletes(); - protected async Task RegisteredTimeoutIsCancelledAfterReactiveChainCompletes(Task storeTask) - { - var store = await storeTask; - var flowId = TestFlowId.Create(); - var unhandledExceptionHandler = new UnhandledExceptionCatcher(); - using var functionsRegistry = new FunctionsRegistry - ( - store, - new Settings( - unhandledExceptionHandler.Catch, - messagesDefaultMaxWaitForCompletion: TimeSpan.MaxValue - ) - ); - var registration = functionsRegistry.RegisterAction( - flowId.Type, - inner: Task (string _, Workflow workflow) => - workflow - .Messages - .TakeUntilTimeout("TimeoutId4321", expiresIn: TimeSpan.FromMilliseconds(5_000)) - .First() - ); - - await registration.Schedule("someInstanceId", "someParam"); - - var messageWriter = registration.MessageWriters.For(new FlowInstance("someInstanceId")); - await messageWriter.AppendMessage("someMessage"); - - var controlPanel = await registration.ControlPanel("someInstanceId"); - controlPanel.ShouldNotBeNull(); - - await controlPanel.WaitForCompletion(allowPostponeAndSuspended: true); - - await controlPanel.Refresh(); - - await controlPanel - .RegisteredTimeouts - .All - .SelectAsync(ts => ts.Count == 0) - .ShouldBeTrueAsync(); - - unhandledExceptionHandler.ThrownExceptions.Count.ShouldBe(0); - } - - public abstract Task PendingTimeoutCanBeRemovedFromControlPanel(); - protected async Task PendingTimeoutCanBeRemovedFromControlPanel(Task storeTask) - { - var store = await storeTask; - var flowId = TestFlowId.Create(); - var unhandledExceptionHandler = new UnhandledExceptionCatcher(); - using var functionsRegistry = new FunctionsRegistry - ( - store, - new Settings( - unhandledExceptionHandler.Catch, - messagesDefaultMaxWaitForCompletion: TimeSpan.Zero - ) - ); - var registration = functionsRegistry.RegisterParamless( - flowId.Type, - inner: Task (workflow) => - workflow - .Messages - .TakeUntilTimeout("TimeoutId4321", expiresIn: TimeSpan.FromMinutes(10)) - .First() - ); - - await registration.Schedule("someInstanceId"); - - var controlPanel = await registration.ControlPanel("someInstanceId"); - controlPanel.ShouldNotBeNull(); - await controlPanel.BusyWaitUntil(cp => cp.Status == Status.Suspended); - - var registeredTimeouts = await controlPanel.RegisteredTimeouts.All; - registeredTimeouts.Count.ShouldBe(1); - var registeredTimeout = registeredTimeouts.First(); - - var id = registeredTimeout.TimeoutId; - id.Id.ShouldBe("TimeoutId4321"); - id.Type.ShouldBe(EffectType.Timeout); - - var timeouts = (await store.TimeoutStore.GetTimeouts(controlPanel.StoredId)).ToList(); - timeouts.Count.ShouldBe(1); - var timeout = timeouts.First(); - timeout.TimeoutId.ShouldBe(id); - - await controlPanel.RegisteredTimeouts.Remove(id); - - await controlPanel.Refresh(); - - await controlPanel.RegisteredTimeouts.All.ShouldBeEmptyAsync(); - await store.TimeoutStore.GetTimeouts(controlPanel.StoredId).ShouldBeEmptyAsync(); - - unhandledExceptionHandler.ThrownExceptions.Count.ShouldBe(0); - } - - public abstract Task PendingTimeoutCanBeUpdatedFromControlPanel(); - protected async Task PendingTimeoutCanBeUpdatedFromControlPanel(Task storeTask) - { - var store = await storeTask; - var flowId = TestFlowId.Create(); - var unhandledExceptionHandler = new UnhandledExceptionCatcher(); - using var functionsRegistry = new FunctionsRegistry - ( - store, - new Settings( - unhandledExceptionHandler.Catch, - messagesDefaultMaxWaitForCompletion: TimeSpan.Zero - ) - ); - var registration = functionsRegistry.RegisterParamless( - flowId.Type, - inner: Task (workflow) => - workflow - .Messages - .TakeUntilTimeout("TimeoutId4321", expiresIn: TimeSpan.FromMinutes(10)) - .First() - ); - - await registration.Schedule("someInstanceId"); - - var controlPanel = await registration.ControlPanel("someInstanceId"); - controlPanel.ShouldNotBeNull(); - await controlPanel.BusyWaitUntil(cp => cp.Status == Status.Suspended); - - var registeredTimeouts = await controlPanel.RegisteredTimeouts.All; - registeredTimeouts.Count.ShouldBe(1); - var registeredTimeout = registeredTimeouts.First(); - - var id = registeredTimeout.TimeoutId; - id.Id.ShouldBe("TimeoutId4321"); - id.Type.ShouldBe(EffectType.Timeout); - - var timeouts = (await store.TimeoutStore.GetTimeouts(controlPanel.StoredId)).ToList(); - timeouts.Count.ShouldBe(1); - var timeout = timeouts.First(); - timeout.TimeoutId.ShouldBe(id); - - await controlPanel.RegisteredTimeouts.Upsert(id, new DateTime(2100, 1, 1, 0, 0, 0, DateTimeKind.Utc)); - - await controlPanel.Refresh(); - - registeredTimeout = (await controlPanel.RegisteredTimeouts.All).Single(); - timeout = (await store.TimeoutStore.GetTimeouts(controlPanel.StoredId)).Single(); - - registeredTimeout.TimeoutId.ShouldBe(id); - timeout.TimeoutId.ShouldBe(id); - - registeredTimeout.Expiry.ShouldBe(new DateTime(2100, 1, 1, 0, 0, 0, DateTimeKind.Utc)); - new DateTime(timeout.Expiry).ShouldBe(new DateTime(2100, 1, 1, 0, 0, 0, DateTimeKind.Utc)); - - unhandledExceptionHandler.ThrownExceptions.Count.ShouldBe(0); - } - public abstract Task ExpiredImplicitTimeoutsAreAddedToMessages(); protected async Task ExpiredImplicitTimeoutsAreAddedToMessages(Task storeTask) { @@ -247,7 +94,7 @@ protected async Task ExpiredImplicitTimeoutsAreAddedToMessages(Task storedParameter.ToUtf8Bytes(), LeaseLength: 0) ).ShouldBeTrueAsync(); @@ -296,7 +296,6 @@ await store.PostponeFunction( ignoreInterrupted: false, expectedEpoch: 0, effects: null, - messages: null, complimentaryState: new ComplimentaryState(storedParameter.ToUtf8Bytes().ToFunc(), LeaseLength: 0) ).ShouldBeTrueAsync(); @@ -333,7 +332,6 @@ await store.PostponeFunction( ignoreInterrupted: false, expectedEpoch: 0, effects: null, - messages: null, complimentaryState: new ComplimentaryState(storedParameter.ToUtf8Bytes().ToFunc(), LeaseLength: 0) ).ShouldBeTrueAsync(); @@ -370,7 +368,6 @@ await store.PostponeFunction( ignoreInterrupted: false, expectedEpoch: 1, effects: null, - messages: null, complimentaryState: new ComplimentaryState(storedParameter.ToUtf8Bytes().ToFunc(), LeaseLength: 0) ).ShouldBeFalseAsync(); @@ -560,7 +557,6 @@ await store.FailFunction( timestamp: DateTime.UtcNow.Ticks, expectedEpoch: 0, effects: null, - messages: null, complimentaryState: new ComplimentaryState(storedParameter.ToUtf8Bytes().ToFunc(), LeaseLength: 0) ); @@ -684,7 +680,6 @@ await store.SuspendFunction( timestamp: DateTime.UtcNow.Ticks, expectedEpoch: 0, effects: null, - messages: null, complimentaryState: new ComplimentaryState(storedParameter.ToUtf8Bytes().ToFunc(), LeaseLength: 0) ).ShouldBeAsync(true); @@ -860,7 +855,6 @@ await store.SucceedFunction( DateTime.UtcNow.Ticks, expectedEpoch: 0, effects: null, - messages: null, complimentaryState: new ComplimentaryState(Test.SimpleStoredParameter.ToFunc(), LeaseLength: 0) ); @@ -892,7 +886,6 @@ await store.PostponeFunction( ignoreInterrupted: false, expectedEpoch: 0, effects: null, - messages: null, complimentaryState: new ComplimentaryState(Test.SimpleStoredParameter.ToFunc(), LeaseLength: 0) ); @@ -923,7 +916,6 @@ await store.FailFunction( timestamp: DateTime.UtcNow.Ticks, expectedEpoch: 0, effects: null, - messages: null, complimentaryState: new ComplimentaryState(Test.SimpleStoredParameter.ToFunc(), LeaseLength: 0) ); @@ -953,7 +945,6 @@ await store.SuspendFunction( timestamp: DateTime.UtcNow.Ticks, expectedEpoch: 0, effects: null, - messages: null, complimentaryState: new ComplimentaryState(Test.SimpleStoredParameter.ToFunc(), LeaseLength: 0) ); @@ -990,7 +981,6 @@ await store.SuspendFunction( timestamp: DateTime.UtcNow.Ticks, expectedEpoch: 0, effects: null, - messages: null, complimentaryState: new ComplimentaryState(Test.SimpleStoredParameter.ToFunc(), LeaseLength: 0) ).ShouldBeFalseAsync(); @@ -1027,7 +1017,6 @@ await store.MessageStore.AppendMessage( timestamp: DateTime.UtcNow.Ticks, expectedEpoch: 0, effects: null, - messages: null, complimentaryState: new ComplimentaryState(Test.SimpleStoredParameter.ToFunc(), LeaseLength: 0) ); @@ -1088,7 +1077,6 @@ await store.SuspendFunction( timestamp: DateTime.UtcNow.Ticks, expectedEpoch: 0, effects: null, - messages: null, new ComplimentaryState(() => Test.SimpleStoredParameter, LeaseLength: 0) ).ShouldBeTrueAsync(); @@ -1168,7 +1156,6 @@ await store.SucceedFunction( timestamp: timestamp, expectedEpoch: 0, effects: null, - messages: null, new ComplimentaryState(() => Test.SimpleStoredParameter, LeaseLength: 0) ).ShouldBeTrueAsync(); } @@ -1309,7 +1296,6 @@ await store.SucceedFunction( timestamp, expectedEpoch: 0, effects: null, - messages: null, new ComplimentaryState(StoredParameterFunc: () => null, LeaseLength: 0) ); @@ -1467,7 +1453,6 @@ await store.SucceedFunction( timestamp: timestamp, expectedEpoch: 0, effects: null, - messages: null, new ComplimentaryState(() => Test.SimpleStoredParameter, LeaseLength: 0) ).ShouldBeTrueAsync(); } @@ -1511,7 +1496,6 @@ await store.CreateFunction( ignoreInterrupted: false, expectedEpoch: 0, effects: null, - messages: null, new ComplimentaryState(() => Test.SimpleStoredParameter, LeaseLength: 0) ); success.ShouldBeFalse(); @@ -1553,7 +1537,6 @@ await store.CreateFunction( ignoreInterrupted: true, expectedEpoch: 0, effects: null, - messages: null, new ComplimentaryState(() => Test.SimpleStoredParameter, LeaseLength: 0) ); success.ShouldBeTrue(); @@ -1843,4 +1826,148 @@ await store.CreateFunction( effects.Count.ShouldBe(0); messages.Count.ShouldBe(0); } + + public abstract Task EffectsArePersistedOnSuspendFunction(); + protected async Task EffectsArePersistedOnSuspendFunction(Task storeTask) + { + await SharedEffectsArePersistedOnFunctionPersist( + storeTask, + storeFunc: (store, id, effects, complimentaryState) => + store.SuspendFunction( + id, + DateTime.UtcNow.Ticks, + expectedEpoch: 0, + effects, + complimentaryState + ) + ); + } + + public abstract Task EffectsArePersistedOnSucceededFunction(); + protected async Task EffectsArePersistedOnSucceededFunction(Task storeTask) + { + await SharedEffectsArePersistedOnFunctionPersist( + storeTask, + storeFunc: (store, id, effects, complimentaryState) => + store.SucceedFunction( + id, + result: null, + DateTime.UtcNow.Ticks, + expectedEpoch: 0, + effects, + complimentaryState + ) + ); + } + + public abstract Task EffectsArePersistedOnPostponeFunction(); + protected async Task EffectsArePersistedOnPostponeFunction(Task storeTask) + { + await SharedEffectsArePersistedOnFunctionPersist( + storeTask, + storeFunc: (store, id, effects, complimentaryState) => + store.PostponeFunction( + id, + postponeUntil: 0, + timestamp: DateTime.UtcNow.Ticks, + ignoreInterrupted: false, + expectedEpoch: 0, + effects, + complimentaryState + ) + ); + } + + public abstract Task EffectsArePersistedOnFailFunction(); + protected async Task EffectsArePersistedOnFailFunction(Task storeTask) + { + await SharedEffectsArePersistedOnFunctionPersist( + storeTask, + storeFunc: (store, id, effects, complimentaryState) => + store.FailFunction( + id, + new StoredException("SomeMessage", "SomeStackTrace", "SomeExceptionType"), + timestamp: DateTime.UtcNow.Ticks, + expectedEpoch: 0, + effects, + complimentaryState + ) + ); + } + + private async Task SharedEffectsArePersistedOnFunctionPersist( + Task storeTask, + Func?, ComplimentaryState, Task> storeFunc + ) + { + var storedId = TestStoredId.Create(); + + var store = await storeTask; + var paramJson = PARAM.ToJson(); + + await store.CreateFunction( + storedId, + "humanInstanceId", + paramJson.ToUtf8Bytes(), + leaseExpiration: DateTime.UtcNow.Ticks, + postponeUntil: null, + timestamp: DateTime.UtcNow.Ticks, + parent: null + ).ShouldBeTrueAsync(); + + var effectId1 = new EffectId("EffectId#1", EffectType.Effect, Context: ""); + var effectId2 = new EffectId("EffectId#2", EffectType.Effect, Context: ""); + var storedEffects = new List + { + new(storedId, effectId1.ToStoredEffectId(), CrudOperation.Insert, new StoredEffect(effectId1, effectId1.ToStoredEffectId(), WorkStatus.Completed, Result: "Hallo".ToUtf8Bytes(), StoredException: null)), + new(storedId, effectId2.ToStoredEffectId(), CrudOperation.Insert, new StoredEffect(effectId2, effectId2.ToStoredEffectId(), WorkStatus.Completed, Result: "World".ToUtf8Bytes(), StoredException: null)), + }; + + await storeFunc( + store, + storedId, + storedEffects, + new ComplimentaryState(() => null, LeaseLength: 0) + ).ShouldBeTrueAsync(); + + var fetchedEffects = await store.EffectsStore.GetEffectResults(storedId); + fetchedEffects.Count.ShouldBe(2); + var fetchedEffect1 = fetchedEffects.Single(s => s.EffectId == effectId1); + fetchedEffect1.Result!.ToStringFromUtf8Bytes().ShouldBe("Hallo"); + var fetchedEffect2 = fetchedEffects.Single(s => s.EffectId == effectId2); + fetchedEffect2.Result!.ToStringFromUtf8Bytes().ShouldBe("World"); + } + + public abstract Task AppendMessageNoStatusAndInterruptWorks(); + protected async Task AppendMessageNoStatusAndInterruptWorks(Task storeTask) + { + var storedId = TestStoredId.Create(); + + var store = await storeTask; + var paramJson = PARAM.ToJson(); + + await store.CreateFunction( + storedId, + "humanInstanceId", + paramJson.ToUtf8Bytes(), + leaseExpiration: DateTime.UtcNow.Ticks, + postponeUntil: null, + timestamp: DateTime.UtcNow.Ticks, + parent: null + ).ShouldBeTrueAsync(); + + var msg = new StoredMessage("HelloWorld".ToUtf8Bytes(), typeof(string).SimpleQualifiedName().ToUtf8Bytes(), IdempotencyKey: "SomeIdempotencyKey"); + await store.MessageStore.AppendMessageNoStatusAndInterrupt(storedId, msg); + + var sf = await store.GetFunction(storedId); + sf.ShouldNotBeNull(); + sf.Interrupted.ShouldBeFalse(); + + var messages = await store.MessageStore.GetMessages(storedId, skip: 0); + messages.Count.ShouldBe(1); + var fetchedMessage = messages.Single(); + fetchedMessage.MessageContent.ToStringFromUtf8Bytes().ShouldBe("HelloWorld"); + fetchedMessage.MessageType.ToStringFromUtf8Bytes().ShouldBe(typeof(string).SimpleQualifiedName()); + fetchedMessage.IdempotencyKey.ShouldBe("SomeIdempotencyKey"); + } } \ No newline at end of file diff --git a/Core/Cleipnir.ResilientFunctions.Tests/TestTemplates/TimeoutStoreTests.cs b/Core/Cleipnir.ResilientFunctions.Tests/TestTemplates/TimeoutStoreTests.cs index bf070b6c..c4b0a8ad 100644 --- a/Core/Cleipnir.ResilientFunctions.Tests/TestTemplates/TimeoutStoreTests.cs +++ b/Core/Cleipnir.ResilientFunctions.Tests/TestTemplates/TimeoutStoreTests.cs @@ -91,26 +91,6 @@ await BusyWait.Until( timeouts[0].Expiry.ShouldBe(expiry); } - public abstract Task RegisteredTimeoutIsReturnedFromRegisteredTimeouts(); - protected async Task RegisteredTimeoutIsReturnedFromRegisteredTimeouts(Task storeTask) - { - var store = await storeTask; - var flowId = TestFlowId.Create(); - var storedId = flowId.ToStoredId(new StoredType(1)); - - var registeredTimeouts = new RegisteredTimeouts(storedId, store.TimeoutStore, CreateEffect(flowId, storedId, store)); - - await registeredTimeouts.RegisterTimeout("timeoutId1".ToEffectId(), expiresIn: TimeSpan.FromHours(1)); - await registeredTimeouts.RegisterTimeout("timeoutId2".ToEffectId(), expiresIn: TimeSpan.FromHours(2)); - - await BusyWait.Until(() => registeredTimeouts.PendingTimeouts().SelectAsync(t => t.Count == 2)); - - var timeouts = await registeredTimeouts.PendingTimeouts(); - timeouts.Count.ShouldBe(2); - timeouts.Any(t => t.TimeoutId == "timeoutId1".ToEffectId()).ShouldBe(true); - timeouts.Any(t => t.TimeoutId == "timeoutId2".ToEffectId()).ShouldBe(true); - } - public abstract Task TimeoutStoreCanBeInitializedMultipleTimes(); protected async Task TimeoutStoreCanBeInitializedMultipleTimes(Task storeTask) { @@ -119,55 +99,6 @@ protected async Task TimeoutStoreCanBeInitializedMultipleTimes(Task storeTask) - { - var store = await storeTask; - var flowId = TestFlowId.Create(); - var storedId = flowId.ToStoredId(new StoredType(1)); - - var effect = CreateEffect(flowId, storedId, store); - var registeredTimeouts = new RegisteredTimeouts(storedId, store.TimeoutStore, effect); - - var otherInstanceRegisteredTimeouts = new RegisteredTimeouts( - storedId with { Instance = (storedId.Instance + "2").ToStoredInstance() }, - store.TimeoutStore, - effect - ); - - await registeredTimeouts.RegisterTimeout("timeoutId1".ToEffectId(), expiresIn: TimeSpan.FromHours(1)); - await registeredTimeouts.RegisterTimeout("timeoutId2".ToEffectId(), expiresIn: TimeSpan.FromHours(2)); - await otherInstanceRegisteredTimeouts.RegisterTimeout("timeoutId3".ToEffectId(), expiresIn: TimeSpan.FromHours(3)); - - await BusyWait.Until(() => registeredTimeouts.PendingTimeouts().SelectAsync(t => t.Count == 2)); - - var timeouts = await registeredTimeouts.PendingTimeouts(); - timeouts.Count.ShouldBe(2); - timeouts.Any(t => t.TimeoutId == "timeoutId1".ToEffectId()).ShouldBe(true); - timeouts.Any(t => t.TimeoutId == "timeoutId2".ToEffectId()).ShouldBe(true); - } - - public abstract Task TimeoutIsNotRegisteredAgainWhenProviderAlreadyContainsTimeout(); - protected async Task TimeoutIsNotRegisteredAgainWhenProviderAlreadyContainsTimeout(Task storeTask) - { - var upsertCount = 0; - var store = new TimeoutStoreDecorator((await storeTask).TimeoutStore, () => upsertCount++); - var flowId = TestFlowId.Create(); - var storedId = flowId.ToStoredId(new StoredType(1)); - - var registeredTimeouts = new RegisteredTimeouts(storedId, store, CreateEffect(flowId, storedId, await storeTask)); - - await registeredTimeouts.RegisterTimeout("timeoutId1".ToEffectId(), expiresIn: TimeSpan.FromHours(1)); - upsertCount.ShouldBe(1); - - var pendingTimeouts = await registeredTimeouts.PendingTimeouts(); - pendingTimeouts.Count.ShouldBe(1); - pendingTimeouts.Single().TimeoutId.ShouldBe("timeoutId1".ToEffectId()); - - await registeredTimeouts.RegisterTimeout("timeoutId1".ToEffectId(), expiresIn: TimeSpan.FromHours(1)); - upsertCount.ShouldBe(1); - } - public abstract Task TimeoutsForDifferentTypesCanBeCreatedFetchedSuccessfully(); protected async Task TimeoutsForDifferentTypesCanBeCreatedFetchedSuccessfully(Task storeTask) { @@ -198,26 +129,6 @@ protected async Task TimeoutsForDifferentTypesCanBeCreatedFetchedSuccessfully(Ta timeouts.ShouldBeEmpty(); } - public abstract Task CancellingNonExistingTimeoutDoesResultInIO(); - protected async Task CancellingNonExistingTimeoutDoesResultInIO(Task storeTask) - { - var removeCount = 0; - var store = new TimeoutStoreDecorator((await storeTask).TimeoutStore, removeTimeoutCallback: () => removeCount++); - var flowId = TestFlowId.Create(); - var storedId = flowId.ToStoredId(new StoredType(1)); - - var registeredTimeouts = new RegisteredTimeouts(storedId, store, CreateEffect(flowId, storedId, await storeTask)); - - var pendingTimeouts = await registeredTimeouts.PendingTimeouts(); - pendingTimeouts.ShouldBeEmpty(); - - await registeredTimeouts.RegisterTimeout("SomeOtherTimeoutId".ToEffectId(), expiresIn: TimeSpan.FromHours(1)); - - await registeredTimeouts.CancelTimeout("SomeTimeoutId".ToEffectId()); - - removeCount.ShouldBe(1); - } - private Effect CreateEffect(FlowId flowId, StoredId storedId, IFunctionStore functionStore) { var lazyExistingEffects = new Lazy>>(() => Task.FromResult((IReadOnlyList) new List())); diff --git a/Core/Cleipnir.ResilientFunctions.Tests/TestTemplates/WatchDogsTests/CrashableFunctionStore.cs b/Core/Cleipnir.ResilientFunctions.Tests/TestTemplates/WatchDogsTests/CrashableFunctionStore.cs index 28a9164a..459459b3 100644 --- a/Core/Cleipnir.ResilientFunctions.Tests/TestTemplates/WatchDogsTests/CrashableFunctionStore.cs +++ b/Core/Cleipnir.ResilientFunctions.Tests/TestTemplates/WatchDogsTests/CrashableFunctionStore.cs @@ -112,12 +112,11 @@ public Task SucceedFunction( byte[]? result, long timestamp, int expectedEpoch, - IReadOnlyList? effects, - IReadOnlyList? messages, + IReadOnlyList? effects, ComplimentaryState complimentaryState ) => _crashed ? Task.FromException(new TimeoutException()) - : _inner.SucceedFunction(storedId, result, timestamp, expectedEpoch, effects, messages, complimentaryState); + : _inner.SucceedFunction(storedId, result, timestamp, expectedEpoch, effects, complimentaryState); public async Task PostponeFunction( StoredId storedId, @@ -125,15 +124,14 @@ public async Task PostponeFunction( long timestamp, bool ignoreInterrupted, int expectedEpoch, - IReadOnlyList? effects, - IReadOnlyList? messages, + IReadOnlyList? effects, ComplimentaryState complimentaryState ) { if (_crashed) throw new TimeoutException(); - var result = await _inner.PostponeFunction(storedId, postponeUntil, timestamp, ignoreInterrupted, expectedEpoch, effects, messages, complimentaryState); + var result = await _inner.PostponeFunction(storedId, postponeUntil, timestamp, ignoreInterrupted, expectedEpoch, effects, complimentaryState); AfterPostponeFunctionFlag.Raise(); return result; @@ -144,23 +142,21 @@ public Task FailFunction( StoredException storedException, long timestamp, int expectedEpoch, - IReadOnlyList? effects, - IReadOnlyList? messages, + IReadOnlyList? effects, ComplimentaryState complimentaryState ) => _crashed ? Task.FromException(new TimeoutException()) - : _inner.FailFunction(storedId, storedException, timestamp, expectedEpoch, effects, messages, complimentaryState); + : _inner.FailFunction(storedId, storedException, timestamp, expectedEpoch, effects, complimentaryState); public Task SuspendFunction( StoredId storedId, long timestamp, int expectedEpoch, - IReadOnlyList? effects, - IReadOnlyList? messages, + IReadOnlyList? effects, ComplimentaryState complimentaryState) => _crashed ? Task.FromException(new TimeoutException()) - : _inner.SuspendFunction(storedId, timestamp, expectedEpoch, effects, messages, complimentaryState); + : _inner.SuspendFunction(storedId, timestamp, expectedEpoch, effects, complimentaryState); public Task Interrupt(StoredId storedId, bool onlyIfExecuting) => _crashed diff --git a/Core/Cleipnir.ResilientFunctions/CoreRuntime/Invocation/InvocationHelper.cs b/Core/Cleipnir.ResilientFunctions/CoreRuntime/Invocation/InvocationHelper.cs index 2a135094..d8b3769d 100644 --- a/Core/Cleipnir.ResilientFunctions/CoreRuntime/Invocation/InvocationHelper.cs +++ b/Core/Cleipnir.ResilientFunctions/CoreRuntime/Invocation/InvocationHelper.cs @@ -98,7 +98,6 @@ public async Task PersistFailure(StoredId storedId, FlowId flowId, FatalWorkflow timestamp: DateTime.UtcNow.Ticks, expectedEpoch, effects: null, - messages: null, complimentaryState: new ComplimentaryState( () => SerializeParameter(param), _settings.LeaseLength.Ticks @@ -110,12 +109,24 @@ public async Task PersistFailure(StoredId storedId, FlowId flowId, FatalWorkflow public async Task PersistResult( StoredId storedId, - FlowId flowId, Result result, TParam param, - StoredId? parent, + Workflow workflow, int expectedEpoch) { + var pendingEffectChanges = workflow.Effect.EffectResults.PendingChanges; + var storedEffectChanges = workflow.Effect.EffectResults.HasPendingChanges + ? workflow.Effect.EffectResults.PendingChanges.Values + .Where(pc => !pc.Existing) + .Select(pc => new StoredEffectChange( + storedId, + pc.Id, + pc.Operation!.Value, + pc.StoredEffect + )) + .ToList() + : null; + var complementaryState = new ComplimentaryState( () => SerializeParameter(param), _settings.LeaseLength.Ticks @@ -128,8 +139,7 @@ public async Task PersistResult( result: SerializeResult(result.SucceedWithValue), timestamp: DateTime.UtcNow.Ticks, expectedEpoch, - effects: null, - messages: null, + effects: storedEffectChanges, complementaryState ) ? PersistResultOutcome.Success : PersistResultOutcome.Failed; case Outcome.Postpone: @@ -139,8 +149,7 @@ public async Task PersistResult( timestamp: DateTime.UtcNow.Ticks, ignoreInterrupted: false, expectedEpoch, - effects: null, - messages: null, + effects: storedEffectChanges, complementaryState ) ? PersistResultOutcome.Success : PersistResultOutcome.Reschedule; case Outcome.Fail: @@ -149,8 +158,7 @@ public async Task PersistResult( storedException: Serializer.SerializeException(result.Fail!), timestamp: DateTime.UtcNow.Ticks, expectedEpoch, - effects: null, - messages: null, + effects: storedEffectChanges, complementaryState ) ? PersistResultOutcome.Success : PersistResultOutcome.Failed; case Outcome.Suspend: @@ -158,8 +166,7 @@ public async Task PersistResult( storedId, timestamp: DateTime.UtcNow.Ticks, expectedEpoch, - effects: null, - messages: null, + effects: storedEffectChanges, complementaryState ) ? PersistResultOutcome.Success : PersistResultOutcome.Reschedule; default: @@ -276,7 +283,6 @@ await _functionStore.FailFunction( timestamp: DateTime.UtcNow.Ticks, expectedEpoch, effects: null, - messages: null, complimentaryState: new ComplimentaryState( () => sf.Parameter, _settings.LeaseLength.Ticks @@ -409,10 +415,9 @@ public Messages CreateMessages( ScheduleReInvocation scheduleReInvocation, Func isWorkflowRunning, Effect effect, - IReadOnlyList initialMessages) + IReadOnlyList? initialMessages) { var messageWriter = new MessageWriter(storedId, _functionStore, Serializer, scheduleReInvocation); - var registeredTimeouts = new RegisteredTimeouts(storedId, _functionStore.TimeoutStore, effect); var messagesPullerAndEmitter = new MessagesPullerAndEmitter( storedId, defaultDelay: _settings.MessagesPullFrequency, @@ -420,11 +425,10 @@ public Messages CreateMessages( isWorkflowRunning, _functionStore, Serializer, - registeredTimeouts, initialMessages ); - return new Messages(messageWriter, registeredTimeouts, messagesPullerAndEmitter); + return new Messages(messageWriter, messagesPullerAndEmitter); } public Tuple CreateEffectAndStates(StoredId storedId, FlowId flowId, IReadOnlyList storedEffects) @@ -467,7 +471,6 @@ public ExistingStates CreateExistingStates(FlowId flowId) public ExistingEffects CreateExistingEffects(FlowId flowId) => new(MapToStoredId(flowId), flowId, _functionStore.EffectsStore, Serializer); public ExistingMessages CreateExistingMessages(FlowId flowId) => new(MapToStoredId(flowId), _functionStore.MessageStore, Serializer); - public ExistingRegisteredTimeouts CreateExistingTimeouts(FlowId flowId, ExistingEffects existingEffects) => new(MapToStoredId(flowId), _functionStore.TimeoutStore, existingEffects); public ExistingSemaphores CreateExistingSemaphores(FlowId flowId) => new(MapToStoredId(flowId), _functionStore, CreateExistingEffects(flowId)); public DistributedSemaphores CreateSemaphores(StoredId storedId, Effect effect) diff --git a/Core/Cleipnir.ResilientFunctions/CoreRuntime/Invocation/Invoker.cs b/Core/Cleipnir.ResilientFunctions/CoreRuntime/Invocation/Invoker.cs index 56314200..8609f525 100644 --- a/Core/Cleipnir.ResilientFunctions/CoreRuntime/Invocation/Invoker.cs +++ b/Core/Cleipnir.ResilientFunctions/CoreRuntime/Invocation/Invoker.cs @@ -55,7 +55,7 @@ public async Task Invoke(FlowInstance instance, TParam param, InitialSt } finally{ disposables.Dispose(); } - await PersistResultAndEnsureSuccess(storedId, flowId, result, param, parent: null); + await PersistResultAndEnsureSuccess(storedId, flowId, result, param, parent: null, workflow); return result.SucceedWithValue!; } @@ -90,7 +90,7 @@ public async Task> ScheduleInvoke(FlowInstance flowInsta catch (Exception exception) { var fwe = FatalWorkflowException.CreateNonGeneric(flowId, exception); await PersistFailure(storedId, flowId, fwe, param, parent?.StoredId); throw fwe; } finally{ disposables.Dispose(); } - await PersistResultAndEnsureSuccess(storedId, flowId, result, param, parent?.StoredId, allowPostponedOrSuspended: true); + await PersistResultAndEnsureSuccess(storedId, flowId, result, param, parent?.StoredId, workflow, allowPostponedOrSuspended: true); } catch (Exception exception) { _unhandledExceptionHandler.Invoke(_flowType, exception); } }); @@ -141,7 +141,7 @@ public async Task Restart(StoredInstance instanceId, int expectedEpoch) catch (Exception exception) { var fwe = FatalWorkflowException.CreateNonGeneric(flowId, exception); await PersistFailure(storedId, flowId, fwe, param, parent, epoch); throw fwe; } finally{ disposables.Dispose(); } - await PersistResultAndEnsureSuccess(storedId, flowId, result, param, parent, epoch); + await PersistResultAndEnsureSuccess(storedId, flowId, result, param, parent, workflow, epoch); return result.SucceedWithValue!; } @@ -167,7 +167,7 @@ public async Task ScheduleRestart(StoredInstance instance, int expectedEpoch) catch (Exception exception) { var fwe = FatalWorkflowException.CreateNonGeneric(flowId, exception); await PersistFailure(storedId, flowId, fwe, param, parent, epoch); throw fwe; } finally{ disposables.Dispose(); } - await PersistResultAndEnsureSuccess(storedId, flowId, result, param, parent, epoch, allowPostponedOrSuspended: true); + await PersistResultAndEnsureSuccess(storedId, flowId, result, param, parent, workflow, epoch, allowPostponedOrSuspended: true); } catch (Exception exception) { _unhandledExceptionHandler.Invoke(_flowType, exception); } }); @@ -198,7 +198,7 @@ internal async Task ScheduleRestart(StoredInstance instance, RestartedFunction r catch (Exception exception) { var fwe = FatalWorkflowException.CreateNonGeneric(flowId, exception); await PersistFailure(storedId, flowId, fwe, param, parent, epoch); throw fwe; } finally { disposables.Dispose(); } - await PersistResultAndEnsureSuccess(storedId, flowId, result, param, parent, epoch, allowPostponedOrSuspended: true); + await PersistResultAndEnsureSuccess(storedId, flowId, result, param, parent, workflow, epoch, allowPostponedOrSuspended: true); } catch (Exception exception) { _unhandledExceptionHandler.Invoke(_flowType, exception); } finally{ onCompletion(); } @@ -238,7 +238,7 @@ await _invocationHelper.PersistFunctionInStore( ScheduleRestart, isWorkflowRunning: () => !isWorkflowRunningDisposable.Disposed, effect, - initialState == null ? [] : _invocationHelper.MapInitialMessages(initialState.Messages) + initialState == null ? null : _invocationHelper.MapInitialMessages(initialState.Messages) ); var correlations = _invocationHelper.CreateCorrelations(flowId); @@ -341,11 +341,11 @@ private async Task PersistFailure(StoredId storedId, FlowId flowId, FatalWorkflo await _invocationHelper.PersistFailure(storedId, flowId, exception, param, parent, expectedEpoch); } - private async Task PersistResultAndEnsureSuccess(StoredId storedId, FlowId flowId, Result result, TParam param, StoredId? parent, int expectedEpoch = 0, bool allowPostponedOrSuspended = false) + private async Task PersistResultAndEnsureSuccess(StoredId storedId, FlowId flowId, Result result, TParam param, StoredId? parent, Workflow workflow, int expectedEpoch = 0, bool allowPostponedOrSuspended = false) { await _invocationHelper.PublishCompletionMessageToParent(parent, childId: flowId, result); - var outcome = await _invocationHelper.PersistResult(storedId, flowId, result, param, parent, expectedEpoch); + var outcome = await _invocationHelper.PersistResult(storedId, result, param, workflow, expectedEpoch); switch (outcome) { case PersistResultOutcome.Failed: diff --git a/Core/Cleipnir.ResilientFunctions/CoreRuntime/RegisteredTimeouts.cs b/Core/Cleipnir.ResilientFunctions/CoreRuntime/RegisteredTimeouts.cs deleted file mode 100644 index cacf8b66..00000000 --- a/Core/Cleipnir.ResilientFunctions/CoreRuntime/RegisteredTimeouts.cs +++ /dev/null @@ -1,73 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Threading.Tasks; -using Cleipnir.ResilientFunctions.Domain; -using Cleipnir.ResilientFunctions.Storage; - -namespace Cleipnir.ResilientFunctions.CoreRuntime; - -public interface IRegisteredTimeouts -{ - Task RegisterTimeout(EffectId timeoutId, DateTime expiresAt); - Task RegisterTimeout(EffectId timeoutId, TimeSpan expiresIn); - Task CancelTimeout(EffectId timeoutId); - Task> PendingTimeouts(); -} - -public enum TimeoutStatus -{ - Created, - Registered, - Cancelled -} - -public class RegisteredTimeouts(StoredId storedId, ITimeoutStore timeoutStore, Effect effect) : IRegisteredTimeouts -{ - public string GetNextImplicitId() => EffectContext.CurrentContext.NextImplicitId(); - - public async Task RegisterTimeout(EffectId timeoutId, DateTime expiresAt) - { - if (await effect.Contains(timeoutId)) - return; - - expiresAt = expiresAt.ToUniversalTime(); - await timeoutStore.UpsertTimeout( - new StoredTimeout(storedId, timeoutId, expiresAt.Ticks), - overwrite: true - ); - - await effect.Upsert(timeoutId, TimeoutStatus.Registered); - } - - public Task RegisterTimeout(string timeoutId, TimeSpan expiresIn) - => RegisterTimeout(EffectId.CreateWithCurrentContext(timeoutId, EffectType.Timeout), expiresAt: DateTime.UtcNow.Add(expiresIn)); - public Task RegisterTimeout(string timeoutId, DateTime expiresAt) - => RegisterTimeout(EffectId.CreateWithCurrentContext(timeoutId, EffectType.Timeout), expiresAt); - public Task RegisterTimeout(EffectId timeoutId, TimeSpan expiresIn) - => RegisterTimeout(timeoutId, expiresAt: DateTime.UtcNow.Add(expiresIn)); - - public async Task CancelTimeout(EffectId timeoutId) - { - if (!await effect.Contains(timeoutId)) - { - await timeoutStore.RemoveTimeout(storedId, timeoutId); - return; - } - - var timeoutStatus = await effect.Get(timeoutId); - if (timeoutStatus == TimeoutStatus.Cancelled) - return; - - await timeoutStore.RemoveTimeout(storedId, timeoutId); - await effect.Upsert(timeoutId, TimeoutStatus.Cancelled); - } - - public async Task> PendingTimeouts() - { - var timeouts = await timeoutStore.GetTimeouts(storedId); - return timeouts - .Select(t => new RegisteredTimeout(t.TimeoutId, new DateTime(t.Expiry).ToUniversalTime())) - .ToList(); - } -} \ No newline at end of file diff --git a/Core/Cleipnir.ResilientFunctions/Domain/ControlPanel.cs b/Core/Cleipnir.ResilientFunctions/Domain/ControlPanel.cs index 27d3deca..90b60f53 100644 --- a/Core/Cleipnir.ResilientFunctions/Domain/ControlPanel.cs +++ b/Core/Cleipnir.ResilientFunctions/Domain/ControlPanel.cs @@ -17,20 +17,22 @@ internal ControlPanel( Status status, int epoch, long expires, ExistingEffects effects, ExistingStates states, ExistingMessages messages, ExistingSemaphores semaphores, - ExistingRegisteredTimeouts registeredTimeouts, Correlations correlations, + Correlations correlations, FatalWorkflowException? fatalWorkflowException ) : base( invoker, invocationHelper, flowId, storedId, status, epoch, expires, innerParam: Unit.Instance, innerResult: Unit.Instance, effects, - states, messages, registeredTimeouts, semaphores, correlations, fatalWorkflowException + states, messages, semaphores, correlations, fatalWorkflowException ) { } public Task Succeed() => InnerSucceed(result: Unit.Instance); - public async Task BusyWaitUntil(Func predicate, TimeSpan? maxWait = null, TimeSpan? checkFrequency = null) + public Task BusyWaitUntil(Func predicate, TimeSpan? maxWait = null, TimeSpan? checkFrequency = null) + => BusyWaitUntil(predicate: cp => predicate(cp).ToTask(), maxWait, checkFrequency); + public async Task BusyWaitUntil(Func> predicate, TimeSpan? maxWait = null, TimeSpan? checkFrequency = null) { - if (predicate(this)) + if (await predicate(this)) return; maxWait ??= TimeSpan.FromSeconds(10); @@ -41,7 +43,7 @@ public async Task BusyWaitUntil(Func predicate, TimeSpan? ma { await Task.Delay(checkFrequency.Value); await Refresh(); - if (predicate(this)) + if (await predicate(this)) return; } while (stopWatch.Elapsed < maxWait); @@ -58,13 +60,13 @@ internal ControlPanel( Status status, int epoch, long expires, TParam innerParam, ExistingEffects effects, ExistingStates states, ExistingMessages messages, ExistingSemaphores semaphores, - ExistingRegisteredTimeouts registeredTimeouts, Correlations correlations, + Correlations correlations, FatalWorkflowException? fatalWorkflowException ) : base( invoker, invocationHelper, flowId, storedId, status, epoch, expires, innerParam, innerResult: Unit.Instance, effects, - states, messages, registeredTimeouts, semaphores, correlations, fatalWorkflowException + states, messages, semaphores, correlations, fatalWorkflowException ) { } public TParam Param @@ -75,9 +77,12 @@ public TParam Param public Task Succeed() => InnerSucceed(result: Unit.Instance); - public async Task BusyWaitUntil(Func, bool> predicate, TimeSpan? maxWait = null, TimeSpan? checkFrequency = null) + public Task BusyWaitUntil(Func, bool> predicate, TimeSpan? maxWait = null, TimeSpan? checkFrequency = null) + => BusyWaitUntil(predicate: cp => predicate(cp).ToTask(), maxWait, checkFrequency); + + public async Task BusyWaitUntil(Func, Task> predicate, TimeSpan? maxWait = null, TimeSpan? checkFrequency = null) { - if (predicate(this)) + if (await predicate(this)) return; maxWait ??= TimeSpan.FromSeconds(10); @@ -88,7 +93,7 @@ public async Task BusyWaitUntil(Func, bool> predicate, Time { await Task.Delay(checkFrequency.Value); await Refresh(); - if (predicate(this)) + if (await predicate(this)) return; } while (stopWatch.Elapsed < maxWait); @@ -105,12 +110,12 @@ internal ControlPanel( long expires, TParam innerParam, TReturn? innerResult, ExistingEffects effects, ExistingStates states, ExistingMessages messages, ExistingSemaphores semaphores, - ExistingRegisteredTimeouts registeredTimeouts, Correlations correlations, FatalWorkflowException? fatalWorkflowException + Correlations correlations, FatalWorkflowException? fatalWorkflowException ) : base( invoker, invocationHelper, flowId, storedId, status, epoch, expires, innerParam, innerResult, effects, states, messages, - registeredTimeouts, semaphores, correlations, fatalWorkflowException + semaphores, correlations, fatalWorkflowException ) { } public Task Succeed(TReturn result) => InnerSucceed(result); @@ -121,10 +126,13 @@ public TParam Param get => InnerParam; set => InnerParam = value; } + + public Task BusyWaitUntil(Func, bool> predicate, TimeSpan? maxWait = null, TimeSpan? checkFrequency = null) + => BusyWaitUntil(predicate: cp => predicate(cp).ToTask(), maxWait, checkFrequency); - public async Task BusyWaitUntil(Func, bool> predicate, TimeSpan? maxWait = null, TimeSpan? checkFrequency = null) + public async Task BusyWaitUntil(Func, Task> predicate, TimeSpan? maxWait = null, TimeSpan? checkFrequency = null) { - if (predicate(this)) + if (await predicate(this)) return; maxWait ??= TimeSpan.FromSeconds(10); @@ -135,7 +143,7 @@ public async Task BusyWaitUntil(Func, bool> predic { await Task.Delay(checkFrequency.Value); await Refresh(); - if (predicate(this)) + if (await predicate(this)) return; } while (stopWatch.Elapsed < maxWait); @@ -162,7 +170,6 @@ internal BaseControlPanel( ExistingEffects effects, ExistingStates states, ExistingMessages messages, - ExistingRegisteredTimeouts registeredTimeouts, ExistingSemaphores semaphores, Correlations correlations, FatalWorkflowException? fatalWorkflowException) @@ -183,7 +190,6 @@ internal BaseControlPanel( Effects = effects; States = states; Messages = messages; - RegisteredTimeouts = registeredTimeouts; Semaphores = semaphores; Correlations = correlations; FatalWorkflowException = fatalWorkflowException; @@ -204,8 +210,6 @@ internal BaseControlPanel( public ExistingSemaphores Semaphores { get; private set; } public Correlations Correlations { get; private set; } - public ExistingRegisteredTimeouts RegisteredTimeouts { get; private set; } - private TParam _innerParam; protected TParam InnerParam { @@ -330,7 +334,6 @@ public async Task Refresh() Effects = _invocationHelper.CreateExistingEffects(FlowId); Messages = _invocationHelper.CreateExistingMessages(FlowId); States = _invocationHelper.CreateExistingStates(FlowId); - RegisteredTimeouts = _invocationHelper.CreateExistingTimeouts(FlowId, Effects); Correlations = _invocationHelper.CreateCorrelations(FlowId); _innerParamChanged = false; diff --git a/Core/Cleipnir.ResilientFunctions/Domain/ControlPanelFactory.cs b/Core/Cleipnir.ResilientFunctions/Domain/ControlPanelFactory.cs index 372babc5..814b6777 100644 --- a/Core/Cleipnir.ResilientFunctions/Domain/ControlPanelFactory.cs +++ b/Core/Cleipnir.ResilientFunctions/Domain/ControlPanelFactory.cs @@ -41,7 +41,6 @@ internal ControlPanelFactory(FlowType flowType, StoredType storedType, Invoker Contains(string id) => await Contains(CreateEffectId(id, EffectType.Effect)); internal Task Contains(EffectId effectId) => effectResults.Contains(effectId); + internal EffectResults EffectResults => effectResults; public async Task GetStatus(string id) { @@ -36,8 +38,8 @@ public async Task Mark(string id) return true; } - public Task CreateOrGet(string id, T value) => CreateOrGet(CreateEffectId(id), value); - internal Task CreateOrGet(EffectId effectId, T value) => effectResults.CreateOrGet(effectId, value, flush: true); + public Task CreateOrGet(string id, T value, bool flush = true) => CreateOrGet(CreateEffectId(id), value, flush); + internal Task CreateOrGet(EffectId effectId, T value, bool flush = true) => effectResults.CreateOrGet(effectId, value, flush); public async Task Upsert(string id, T value) => await Upsert(CreateEffectId(id, EffectType.Effect), value); internal Task Upsert(EffectId effectId, T value) => effectResults.Upsert(effectId, value, flush: true); @@ -83,11 +85,45 @@ private Task InnerCapture(string id, EffectType effectType, Func> => effectResults.InnerCapture(id, effectType, work, resiliency, effectContext); public Task Clear(string id) => effectResults.Clear(CreateEffectId(id), flush: true); + + public Task Flush() => effectResults.Flush(); public Task WhenAny(string id, params Task[] tasks) => Capture(id, work: async () => await await Task.WhenAny(tasks)); public Task WhenAll(string id, params Task[] tasks) - => Capture(id, work: () => Task.WhenAll(tasks)); + => Capture(id, work: async () => + { + var results = new T[tasks.Length]; + var minPostponeException = default(PostponeInvocationException); + var suspendException = default(SuspendInvocationException); + for (var i = 0; i < tasks.Length; i++) + { + var task = tasks[i]; + try + { + var result = await task; + results[i] = result; + } + catch (PostponeInvocationException e) + { + minPostponeException ??= e; + minPostponeException = minPostponeException.PostponeUntil < e.PostponeUntil + ? minPostponeException + : e; + } + catch (SuspendInvocationException e) + { + suspendException ??= e; + } + } + + if (minPostponeException != null) + throw new PostponeInvocationException(minPostponeException.PostponeUntil, minPostponeException); + if (suspendException != null) + throw new SuspendInvocationException(suspendException); + + return results; + }); public Task WhenAny(params Task[] tasks) => WhenAny(EffectContext.CurrentContext.NextImplicitId(), tasks); diff --git a/Core/Cleipnir.ResilientFunctions/Domain/EffectResults.cs b/Core/Cleipnir.ResilientFunctions/Domain/EffectResults.cs index 19030e90..598a4489 100644 --- a/Core/Cleipnir.ResilientFunctions/Domain/EffectResults.cs +++ b/Core/Cleipnir.ResilientFunctions/Domain/EffectResults.cs @@ -20,7 +20,14 @@ public class EffectResults private readonly ISerializer _serializer; private volatile bool _initialized; - private readonly Dictionary _effectResults = new(); + public Dictionary PendingChanges { get; } = new(); + + private volatile bool _hasPendingChanges; + public bool HasPendingChanges + { + get => _hasPendingChanges; + private set => _hasPendingChanges = value; + } public EffectResults( FlowId flowId, @@ -48,7 +55,7 @@ private async Task InitializeIfRequired() return; foreach (var existingEffect in existingEffects) - _effectResults[existingEffect.EffectId] = + PendingChanges[existingEffect.EffectId] = new PendingEffectChange( existingEffect.StoredEffectId, existingEffect, @@ -64,14 +71,14 @@ public async Task Contains(EffectId effectId) { await InitializeIfRequired(); lock (_sync) - return _effectResults.ContainsKey(effectId); + return PendingChanges.ContainsKey(effectId); } public async Task GetOrValueDefault(EffectId effectId) { await InitializeIfRequired(); lock (_sync) - return _effectResults.GetValueOrDefault(effectId)?.StoredEffect; + return PendingChanges.GetValueOrDefault(effectId)?.StoredEffect; } public async Task Set(StoredEffect storedEffect, bool flush) @@ -91,7 +98,7 @@ public async Task CreateOrGet(EffectId effectId, T value, bool flush) await InitializeIfRequired(); lock (_sync) { - if (_effectResults.TryGetValue(effectId, out var existing) && existing.StoredEffect?.WorkStatus == WorkStatus.Completed) + if (PendingChanges.TryGetValue(effectId, out var existing) && existing.StoredEffect?.WorkStatus == WorkStatus.Completed) return _serializer.Deserialize(existing.StoredEffect.Result!); if (existing?.StoredEffect?.StoredException != null) @@ -130,7 +137,7 @@ public async Task> TryGet(EffectId effectId) lock (_sync) { - if (_effectResults.TryGetValue(effectId, out var change)) + if (PendingChanges.TryGetValue(effectId, out var change)) { var storedEffect = change.StoredEffect; if (storedEffect?.WorkStatus == WorkStatus.Completed) @@ -156,7 +163,7 @@ public async Task InnerCapture(string id, EffectType effectType, Func work lock (_sync) { - var success = _effectResults.TryGetValue(effectId, out var pendingChange); + var success = PendingChanges.TryGetValue(effectId, out var pendingChange); var storedEffect = pendingChange?.StoredEffect; if (success && storedEffect?.WorkStatus == WorkStatus.Completed) return; @@ -236,7 +243,7 @@ public async Task InnerCapture(string id, EffectType effectType, Func(storedEffect.StoredEffect?.Result!))!; if (success && storedEffect!.StoredEffect?.WorkStatus == WorkStatus.Failed) @@ -320,7 +327,7 @@ public async Task Clear(EffectId effectId, bool flush) await InitializeIfRequired(); lock (_sync) - if (!_effectResults.ContainsKey(effectId)) + if (!PendingChanges.ContainsKey(effectId)) return; await FlushOrAddToPending( @@ -335,10 +342,10 @@ await FlushOrAddToPending( private async Task FlushOrAddToPending(EffectId effectId, StoredEffectId storedEffectId, StoredEffect? storedEffect, bool flush, bool delete) { lock (_sync) - if (_effectResults.ContainsKey(effectId)) + if (PendingChanges.ContainsKey(effectId)) { - var existing = _effectResults[effectId]; - _effectResults[effectId] = existing with + var existing = PendingChanges[effectId]; + PendingChanges[effectId] = existing with { StoredEffect = storedEffect, Operation = delete @@ -348,7 +355,7 @@ private async Task FlushOrAddToPending(EffectId effectId, StoredEffectId storedE } else { - _effectResults[effectId] = new PendingEffectChange( + PendingChanges[effectId] = new PendingEffectChange( storedEffectId, storedEffect, CrudOperation.Insert, @@ -356,20 +363,21 @@ private async Task FlushOrAddToPending(EffectId effectId, StoredEffectId storedE ); } - if (flush) - await Flush(); + if (flush) + await Flush(); + + HasPendingChanges = !flush; } private readonly SemaphoreSlim _flushSync = new(initialCount: 1, maxCount: 1); - private async Task Flush() + public async Task Flush() { await _flushSync.WaitAsync(); - try { IReadOnlyList pendingChanges; lock (_sync) - pendingChanges = _effectResults.Values.Where(r => r.Operation != null).ToList(); + pendingChanges = PendingChanges.Values.Where(r => r.Operation != null).ToList(); if (pendingChanges.Count == 0) return; @@ -387,19 +395,20 @@ private async Task Flush() await _effectsStore.SetEffectResults(_storedId, changes); lock (_sync) - foreach (var (key, value) in _effectResults.ToList()) + foreach (var (key, value) in PendingChanges.ToList()) { if (value.Operation == CrudOperation.Delete) - _effectResults.Remove(key); + PendingChanges.Remove(key); else - _effectResults[key] = value with + PendingChanges[key] = value with { Existing = true, Operation = null }; } + + HasPendingChanges = false; } - finally { _flushSync.Release(); diff --git a/Core/Cleipnir.ResilientFunctions/Domain/Exceptions/Commands/PostponeInvocationException.cs b/Core/Cleipnir.ResilientFunctions/Domain/Exceptions/Commands/PostponeInvocationException.cs index 1aee2d40..60aaf9a8 100644 --- a/Core/Cleipnir.ResilientFunctions/Domain/Exceptions/Commands/PostponeInvocationException.cs +++ b/Core/Cleipnir.ResilientFunctions/Domain/Exceptions/Commands/PostponeInvocationException.cs @@ -12,4 +12,6 @@ public PostponeInvocationException(int postponeForMs) => PostponeUntil = DateTime.UtcNow.AddMilliseconds(postponeForMs); public PostponeInvocationException(DateTime postponeUntil) => PostponeUntil = postponeUntil.ToUniversalTime(); + public PostponeInvocationException(DateTime postponeUntil, PostponeInvocationException innerException) : base(message: null, innerException) + => PostponeUntil = postponeUntil.ToUniversalTime(); } \ No newline at end of file diff --git a/Core/Cleipnir.ResilientFunctions/Domain/Exceptions/Commands/SuspendInvocationException.cs b/Core/Cleipnir.ResilientFunctions/Domain/Exceptions/Commands/SuspendInvocationException.cs index 40c57bff..f118e006 100644 --- a/Core/Cleipnir.ResilientFunctions/Domain/Exceptions/Commands/SuspendInvocationException.cs +++ b/Core/Cleipnir.ResilientFunctions/Domain/Exceptions/Commands/SuspendInvocationException.cs @@ -2,4 +2,9 @@ namespace Cleipnir.ResilientFunctions.Domain.Exceptions.Commands; -public class SuspendInvocationException : Exception; \ No newline at end of file +public class SuspendInvocationException : Exception +{ + public SuspendInvocationException() {} + public SuspendInvocationException(SuspendInvocationException innerException) + : base(message: null, innerException) { } +} \ No newline at end of file diff --git a/Core/Cleipnir.ResilientFunctions/Domain/ExistingRegisteredTimeouts.cs b/Core/Cleipnir.ResilientFunctions/Domain/ExistingRegisteredTimeouts.cs deleted file mode 100644 index 954bf9ad..00000000 --- a/Core/Cleipnir.ResilientFunctions/Domain/ExistingRegisteredTimeouts.cs +++ /dev/null @@ -1,64 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Threading.Tasks; -using Cleipnir.ResilientFunctions.CoreRuntime; -using Cleipnir.ResilientFunctions.Helpers; -using Cleipnir.ResilientFunctions.Storage; - -namespace Cleipnir.ResilientFunctions.Domain; - -public class ExistingRegisteredTimeouts(StoredId storedId, ITimeoutStore timeoutStore, ExistingEffects effects) -{ - private Dictionary? _timeouts; - - private async Task> GetTimeouts() - { - if (_timeouts is not null) - return _timeouts; - - var storedTimeouts = await timeoutStore.GetTimeouts(storedId); - return _timeouts = storedTimeouts.ToDictionary( - s => s.TimeoutId, - s => new DateTime(s.Expiry, DateTimeKind.Utc) - ); - } - - public Task this[TimeoutId timeoutId] => this[new EffectId(timeoutId.Value, EffectType.Timeout, Context: "")]; - public Task this[EffectId timeoutId] => GetTimeouts().ContinueWith(t => t.Result[timeoutId]); - - public Task> All - => GetTimeouts().ContinueWith( - t => t.Result - .Select(kv => new RegisteredTimeout(kv.Key, kv.Value)) - .ToList() - .CastTo>() - ); - - public Task Remove(TimeoutId timeoutId) => Remove(new EffectId(timeoutId.Value, EffectType.Timeout, Context: "")); - public async Task Remove(EffectId timeoutId) - { - var timeouts = await GetTimeouts(); - await effects.Remove(timeoutId); - await timeoutStore.RemoveTimeout(storedId, timeoutId); - timeouts.Remove(timeoutId); - } - - public Task Upsert(TimeoutId timeoutId, DateTime expiresAt) - => Upsert(new EffectId(timeoutId.Value, EffectType.Timeout, Context: ""), expiresAt); - public async Task Upsert(EffectId timeoutId, DateTime expiresAt) - { - var timeouts = await GetTimeouts(); - await timeoutStore.UpsertTimeout( - new StoredTimeout(storedId, timeoutId, expiresAt.ToUniversalTime().Ticks), - overwrite: true - ); - - await effects.SetValue(timeoutId, TimeoutStatus.Registered); - timeouts[timeoutId] = expiresAt; - } - public Task Upsert(TimeoutId timeoutId, TimeSpan expiresIn) - => Upsert(timeoutId, expiresAt: DateTime.UtcNow.Add(expiresIn)); - public Task Upsert(EffectId timeoutId, TimeSpan expiresIn) - => Upsert(timeoutId, expiresAt: DateTime.UtcNow.Add(expiresIn)); -} \ No newline at end of file diff --git a/Core/Cleipnir.ResilientFunctions/Messaging/IMessageStore.cs b/Core/Cleipnir.ResilientFunctions/Messaging/IMessageStore.cs index 3780d976..d3393bd3 100644 --- a/Core/Cleipnir.ResilientFunctions/Messaging/IMessageStore.cs +++ b/Core/Cleipnir.ResilientFunctions/Messaging/IMessageStore.cs @@ -9,6 +9,7 @@ public interface IMessageStore Task Initialize(); Task AppendMessage(StoredId storedId, StoredMessage storedMessage); + Task AppendMessageNoStatusAndInterrupt(StoredId storedId, StoredMessage storedMessage); Task AppendMessages(IReadOnlyList messages, bool interrupt = true); Task AppendMessages(IReadOnlyList messages, bool interrupt); diff --git a/Core/Cleipnir.ResilientFunctions/Messaging/MessageWriter.cs b/Core/Cleipnir.ResilientFunctions/Messaging/MessageWriter.cs index 062c6014..e6370c8f 100644 --- a/Core/Cleipnir.ResilientFunctions/Messaging/MessageWriter.cs +++ b/Core/Cleipnir.ResilientFunctions/Messaging/MessageWriter.cs @@ -63,4 +63,13 @@ public async Task AppendMessage(TMessage message, string? ide return Finding.Found; } + + internal async Task AppendMessageNoSync(TMessage message, string? idempotencyKey = null) where TMessage : notnull + { + var (eventJson, eventType) = _serializer.SerializeMessage(message, typeof(TMessage)); + await _messageStore.AppendMessageNoStatusAndInterrupt( + _storedId, + new StoredMessage(eventJson, eventType, idempotencyKey) + ); + } } \ No newline at end of file diff --git a/Core/Cleipnir.ResilientFunctions/Messaging/Messages.cs b/Core/Cleipnir.ResilientFunctions/Messaging/Messages.cs index 557e84cb..ef0dd776 100644 --- a/Core/Cleipnir.ResilientFunctions/Messaging/Messages.cs +++ b/Core/Cleipnir.ResilientFunctions/Messaging/Messages.cs @@ -2,14 +2,12 @@ using System.Collections.Generic; using System.Threading; using System.Threading.Tasks; -using Cleipnir.ResilientFunctions.CoreRuntime; using Cleipnir.ResilientFunctions.Reactive; namespace Cleipnir.ResilientFunctions.Messaging; public class Messages : IReactiveChain { - public RegisteredTimeouts RegisteredTimeouts { get; } public IReactiveChain Source => _messagePullerAndEmitter.Source; private readonly MessageWriter _messageWriter; @@ -17,12 +15,10 @@ public class Messages : IReactiveChain public Messages( MessageWriter messageWriter, - RegisteredTimeouts registeredTimeouts, MessagesPullerAndEmitter messagePullerAndEmitter ) { _messageWriter = messageWriter; - RegisteredTimeouts = registeredTimeouts; _messagePullerAndEmitter = messagePullerAndEmitter; } @@ -31,6 +27,11 @@ public async Task AppendMessage(object @event, string? idempotencyKey = null) await _messageWriter.AppendMessage(@event, idempotencyKey); await Sync(); } + + internal async Task AppendMessageNoSync(object @event, string? idempotencyKey = null) + { + await _messageWriter.AppendMessage(@event, idempotencyKey); + } public Task Sync() => _messagePullerAndEmitter.PullEvents(maxSinceLastSynced: TimeSpan.Zero); diff --git a/Core/Cleipnir.ResilientFunctions/Messaging/MessagesPullerAndEmitter.cs b/Core/Cleipnir.ResilientFunctions/Messaging/MessagesPullerAndEmitter.cs index a78bfbcc..a21a89f3 100644 --- a/Core/Cleipnir.ResilientFunctions/Messaging/MessagesPullerAndEmitter.cs +++ b/Core/Cleipnir.ResilientFunctions/Messaging/MessagesPullerAndEmitter.cs @@ -45,7 +45,7 @@ public MessagesPullerAndEmitter( TimeSpan defaultDelay, TimeSpan defaultMaxWait, Func isWorkflowRunning, - IFunctionStore functionStore, ISerializer serializer, IRegisteredTimeouts registeredTimeouts, + IFunctionStore functionStore, ISerializer serializer, IReadOnlyList? initialMessages) { _storedId = storedId; @@ -54,7 +54,6 @@ public MessagesPullerAndEmitter( _serializer = serializer; Source = new Source( - registeredTimeouts, syncStore: PullEvents, defaultDelay, defaultMaxWait, diff --git a/Core/Cleipnir.ResilientFunctions/Reactive/Extensions/InnerOperators.cs b/Core/Cleipnir.ResilientFunctions/Reactive/Extensions/InnerOperators.cs index 1fc74ccc..9b306530 100644 --- a/Core/Cleipnir.ResilientFunctions/Reactive/Extensions/InnerOperators.cs +++ b/Core/Cleipnir.ResilientFunctions/Reactive/Extensions/InnerOperators.cs @@ -107,9 +107,9 @@ public static IReactiveChain TakeUntilTimeout(this Messages s, string ti public static IReactiveChain TakeUntilTimeout(this Messages s, string timeoutEventId, DateTime expiresAt) => new TimeoutOperator(s.Source, EffectId.CreateWithCurrentContext(timeoutEventId, EffectType.Timeout), expiresAt); public static IReactiveChain TakeUntilTimeout(this Messages s, TimeSpan expiresIn) - => s.TakeUntilTimeout(s.RegisteredTimeouts.GetNextImplicitId(), expiresIn); + => s.TakeUntilTimeout(EffectContext.CurrentContext.NextImplicitId(), expiresIn); public static IReactiveChain TakeUntilTimeout(this Messages s, DateTime expiresAt) - => s.TakeUntilTimeout(s.RegisteredTimeouts.GetNextImplicitId(), expiresAt); + => s.TakeUntilTimeout(EffectContext.CurrentContext.NextImplicitId(), expiresAt); public static IReactiveChain Skip(this IReactiveChain s, int toSkip) { diff --git a/Core/Cleipnir.ResilientFunctions/Reactive/Extensions/LeafOperators.cs b/Core/Cleipnir.ResilientFunctions/Reactive/Extensions/LeafOperators.cs index 7dbd8a85..b2639013 100644 --- a/Core/Cleipnir.ResilientFunctions/Reactive/Extensions/LeafOperators.cs +++ b/Core/Cleipnir.ResilientFunctions/Reactive/Extensions/LeafOperators.cs @@ -37,11 +37,21 @@ public static async Task> ToList(this IReactiveChain s, TimeSpan? return await tcs.Task; } - await subscription.RegisterTimeout(); + var registerTimeoutResult = await subscription.RegisterTimeout(); + if (registerTimeoutResult?.AppendedTimeoutToMessages == true) + { + await subscription.SyncStore(maxSinceLastSynced: TimeSpan.Zero); + subscription.PushMessages(); + if (tcs.Task.IsCompleted) + return await tcs.Task; + } + var timeout = registerTimeoutResult?.TimeoutExpiry; maxWait ??= subscription.DefaultMessageMaxWait; if (maxWait == TimeSpan.Zero) - throw new SuspendInvocationException(); + throw timeout == null + ? new SuspendInvocationException() + : new PostponeInvocationException(timeout.Value); var stopWatch = new Stopwatch(); stopWatch.Start(); @@ -59,7 +69,9 @@ public static async Task> ToList(this IReactiveChain s, TimeSpan? return await tcs.Task; } - throw new SuspendInvocationException(); + throw timeout == null + ? new SuspendInvocationException() + : new PostponeInvocationException(timeout.Value); } public static Task> Completion(this IReactiveChain s, TimeSpan? maxWait = null) @@ -182,29 +194,8 @@ public static Task> LastOfType(this Messages messages, string timeo #region Suspend - public static async Task SuspendUntil(this Messages s, string timeoutEventId, DateTime resumeAt) - { - var timeoutEmitted = false; - var effectId = EffectId.CreateWithCurrentContext(timeoutEventId, EffectType.Timeout); - var subscription = s - .OfType() - .Where(t => t.TimeoutId == effectId) - .Take(1) - .Subscribe( - onNext: _ => timeoutEmitted = true, - onCompletion: () => { }, - onError: _ => { } - ); - await subscription.Initialize(); - - subscription.PushMessages(); - - if (timeoutEmitted) - return; - - await subscription.RegisteredTimeouts.RegisterTimeout(effectId, resumeAt); - throw new SuspendInvocationException(); - } + public static Task SuspendUntil(this Messages s, string timeoutEventId, DateTime resumeAt) + => s.TakeUntilTimeout(timeoutEventId, resumeAt).Completion(); public static Task SuspendFor(this Messages s, string timeoutEventId, TimeSpan resumeAfter) => s.SuspendUntil(timeoutEventId, DateTime.UtcNow.Add(resumeAfter)); diff --git a/Core/Cleipnir.ResilientFunctions/Reactive/ISubscription.cs b/Core/Cleipnir.ResilientFunctions/Reactive/ISubscription.cs index 9565123f..a9398431 100644 --- a/Core/Cleipnir.ResilientFunctions/Reactive/ISubscription.cs +++ b/Core/Cleipnir.ResilientFunctions/Reactive/ISubscription.cs @@ -1,7 +1,6 @@ using System; using System.Threading.Tasks; -using Cleipnir.ResilientFunctions.CoreRuntime; -using Cleipnir.ResilientFunctions.Domain; +using Cleipnir.ResilientFunctions.Reactive.Operators; namespace Cleipnir.ResilientFunctions.Reactive; @@ -9,7 +8,6 @@ public interface ISubscription { bool IsWorkflowRunning { get; } IReactiveChain Source { get; } - IRegisteredTimeouts RegisteredTimeouts { get; } TimeSpan DefaultMessageSyncDelay { get; } TimeSpan DefaultMessageMaxWait { get; } @@ -18,6 +16,6 @@ public interface ISubscription Task SyncStore(TimeSpan maxSinceLastSynced); void PushMessages(); - Task RegisterTimeout(); + Task RegisterTimeout(); Task CancelTimeout(); } \ No newline at end of file diff --git a/Core/Cleipnir.ResilientFunctions/Reactive/Operators/BufferOperator.cs b/Core/Cleipnir.ResilientFunctions/Reactive/Operators/BufferOperator.cs index 05e7d42c..ee6d6ba7 100644 --- a/Core/Cleipnir.ResilientFunctions/Reactive/Operators/BufferOperator.cs +++ b/Core/Cleipnir.ResilientFunctions/Reactive/Operators/BufferOperator.cs @@ -1,7 +1,6 @@ using System; using System.Collections.Generic; using System.Threading.Tasks; -using Cleipnir.ResilientFunctions.CoreRuntime; namespace Cleipnir.ResilientFunctions.Reactive.Operators; @@ -54,13 +53,12 @@ public Subscription( public bool IsWorkflowRunning => _subscription.IsWorkflowRunning; public IReactiveChain Source => _subscription.Source; - public IRegisteredTimeouts RegisteredTimeouts => _subscription.RegisteredTimeouts; public Task Initialize() => _subscription.Initialize(); public Task SyncStore(TimeSpan maxSinceLastSynced) => _subscription.SyncStore(maxSinceLastSynced); public void PushMessages() => _subscription.PushMessages(); - public Task RegisterTimeout() => _subscription.RegisterTimeout(); + public Task RegisterTimeout() => _subscription.RegisterTimeout(); public Task CancelTimeout() => _subscription.CancelTimeout(); private void OnNext(T next) diff --git a/Core/Cleipnir.ResilientFunctions/Reactive/Operators/CustomOperator.cs b/Core/Cleipnir.ResilientFunctions/Reactive/Operators/CustomOperator.cs index 314c146d..472348d7 100644 --- a/Core/Cleipnir.ResilientFunctions/Reactive/Operators/CustomOperator.cs +++ b/Core/Cleipnir.ResilientFunctions/Reactive/Operators/CustomOperator.cs @@ -1,6 +1,5 @@ using System; using System.Threading.Tasks; -using Cleipnir.ResilientFunctions.CoreRuntime; namespace Cleipnir.ResilientFunctions.Reactive.Operators; @@ -67,14 +66,12 @@ public Subscription( _innerSubscription = inner.Subscribe(OnNext, OnCompletion, OnError); } - public IRegisteredTimeouts RegisteredTimeouts => _innerSubscription.RegisteredTimeouts; - public Task Initialize() => _innerSubscription.Initialize(); public Task SyncStore(TimeSpan maxSinceLastSynced) => _innerSubscription.SyncStore(maxSinceLastSynced); public void PushMessages() => _innerSubscription.PushMessages(); - public Task RegisterTimeout() => _innerSubscription.RegisterTimeout(); + public Task RegisterTimeout() => _innerSubscription.RegisterTimeout(); public Task CancelTimeout() => _innerSubscription.CancelTimeout(); private void OnNext(TIn next) diff --git a/Core/Cleipnir.ResilientFunctions/Reactive/Operators/RegisterTimeoutResult.cs b/Core/Cleipnir.ResilientFunctions/Reactive/Operators/RegisterTimeoutResult.cs new file mode 100644 index 00000000..8fc92708 --- /dev/null +++ b/Core/Cleipnir.ResilientFunctions/Reactive/Operators/RegisterTimeoutResult.cs @@ -0,0 +1,5 @@ +using System; + +namespace Cleipnir.ResilientFunctions.Reactive.Operators; + +public record RegisterTimeoutResult(DateTime? TimeoutExpiry, bool AppendedTimeoutToMessages); \ No newline at end of file diff --git a/Core/Cleipnir.ResilientFunctions/Reactive/Operators/TimeoutOperator.cs b/Core/Cleipnir.ResilientFunctions/Reactive/Operators/TimeoutOperator.cs index 80ab0901..c838db21 100644 --- a/Core/Cleipnir.ResilientFunctions/Reactive/Operators/TimeoutOperator.cs +++ b/Core/Cleipnir.ResilientFunctions/Reactive/Operators/TimeoutOperator.cs @@ -64,30 +64,66 @@ public Subscription( public bool IsWorkflowRunning => _innerSubscription.IsWorkflowRunning; public IReactiveChain Source => _innerSubscription.Source; - public IRegisteredTimeouts RegisteredTimeouts => _innerSubscription.RegisteredTimeouts; - + + private bool _createdTimeout; + public Task Initialize() => _innerSubscription.Initialize(); - public Task SyncStore(TimeSpan maxSinceLastSynced) => _innerSubscription.SyncStore(maxSinceLastSynced); + public async Task SyncStore(TimeSpan maxSinceLastSynced) + { + if (!TimeoutExists && !_createdTimeout) + { + //append timeout event to messages if it has expired + var workflow = CurrentFlow.Workflow; + if (workflow == null) + throw new InvalidOperationException("Reactive operator must be invoked by the Cleipnir framework"); + + var timeout = await workflow.Effect.CreateOrGet(_timeoutId, _expiresAt); + if (DateTime.UtcNow >= timeout) + { + var timeoutEvent = new TimeoutEvent(_timeoutId, _expiresAt); + var idempotencyKey = $"Timeout¤{_timeoutId}"; + await workflow.Messages.AppendMessageNoSync(timeoutEvent, idempotencyKey); + await _innerSubscription.SyncStore(maxSinceLastSynced: TimeSpan.Zero); + return; + } + } + + await _innerSubscription.SyncStore(maxSinceLastSynced); + } public void PushMessages() => _innerSubscription.PushMessages(); - public Task RegisterTimeout() => _innerSubscription.RegisteredTimeouts.RegisterTimeout(_timeoutId, _expiresAt); - public Task CancelTimeout() + private bool TimeoutExists => _innerSubscription + .Source + .OfType() + .Where(t => t.TimeoutId == _timeoutId) + .Take(1) + .Existing(out _) + .Any(); + + public async Task RegisterTimeout() { - var timeoutExists = _innerSubscription - .Source - .OfType() - .Where(t => t.TimeoutId == _timeoutId) - .Take(1) - .Existing(out _) - .Any(); - - if (timeoutExists) - return Task.CompletedTask; + if (TimeoutExists || _createdTimeout) + return null; + + var workflow = CurrentFlow.Workflow; + if (workflow == null) + throw new InvalidOperationException("Reactive operator must be invoked by the Cleipnir framework"); + + var timeout = await workflow.Effect.CreateOrGet(_timeoutId with {Id = _timeoutId.Id + "_Expires"}, _expiresAt, flush: false); + if (timeout > DateTime.UtcNow) + return new RegisterTimeoutResult(timeout, AppendedTimeoutToMessages: false); - return _innerSubscription.RegisteredTimeouts.CancelTimeout(_timeoutId); - } + //append timeout event to messages + var timeoutEvent = new TimeoutEvent(_timeoutId, _expiresAt); + var idempotencyKey = $"Timeout¤{_timeoutId}"; + await workflow.Messages.AppendMessageNoSync(timeoutEvent, idempotencyKey); + _createdTimeout = true; + return new RegisterTimeoutResult(TimeoutExpiry: null, AppendedTimeoutToMessages: true); + } + + public Task CancelTimeout() => Task.CompletedTask; private void OnNext(T next) { diff --git a/Core/Cleipnir.ResilientFunctions/Reactive/Operators/TimeoutRegistration.cs b/Core/Cleipnir.ResilientFunctions/Reactive/Operators/TimeoutRegistration.cs new file mode 100644 index 00000000..3dc18e74 --- /dev/null +++ b/Core/Cleipnir.ResilientFunctions/Reactive/Operators/TimeoutRegistration.cs @@ -0,0 +1,6 @@ +using System; +using Cleipnir.ResilientFunctions.Domain; + +namespace Cleipnir.ResilientFunctions.Reactive.Operators; + +public record TimeoutRegistration(EffectId EffectId, DateTime ExpiresAt); \ No newline at end of file diff --git a/Core/Cleipnir.ResilientFunctions/Reactive/Origin/Source.cs b/Core/Cleipnir.ResilientFunctions/Reactive/Origin/Source.cs index 8eb86901..b79babf2 100644 --- a/Core/Cleipnir.ResilientFunctions/Reactive/Origin/Source.cs +++ b/Core/Cleipnir.ResilientFunctions/Reactive/Origin/Source.cs @@ -1,8 +1,6 @@ using System; using System.Collections.Generic; using System.Linq; -using Cleipnir.ResilientFunctions.CoreRuntime; -using Cleipnir.ResilientFunctions.Domain; using Cleipnir.ResilientFunctions.Reactive.Utilities; namespace Cleipnir.ResilientFunctions.Reactive.Origin; @@ -10,8 +8,7 @@ namespace Cleipnir.ResilientFunctions.Reactive.Origin; public class Source : IReactiveChain { private bool _completed; - - private readonly IRegisteredTimeouts _registeredTimeouts; + private readonly SyncStore _syncStore; private readonly TimeSpan _defaultDelay; private readonly TimeSpan _defaultMaxWait; @@ -34,7 +31,6 @@ public IEnumerable Existing } public Source( - IRegisteredTimeouts registeredTimeouts, SyncStore syncStore, TimeSpan defaultDelay, TimeSpan defaultMaxWait, @@ -42,7 +38,6 @@ public Source( Func initialSyncPerformed ) { - _registeredTimeouts = registeredTimeouts; _syncStore = syncStore; _defaultDelay = defaultDelay; @@ -56,7 +51,7 @@ public ISubscription Subscribe(Action onNext, Action onCompletion, Actio var subscription = new SourceSubscription( onNext, onCompletion, onError, source: this, - _emittedEvents, _syncStore, _initialSyncPerformed, _isWorkflowRunning, _registeredTimeouts, + _emittedEvents, _syncStore, _initialSyncPerformed, _isWorkflowRunning, _defaultDelay, _defaultMaxWait ); diff --git a/Core/Cleipnir.ResilientFunctions/Reactive/Origin/SourceSubscription.cs b/Core/Cleipnir.ResilientFunctions/Reactive/Origin/SourceSubscription.cs index 7082fce4..4b2d5f12 100644 --- a/Core/Cleipnir.ResilientFunctions/Reactive/Origin/SourceSubscription.cs +++ b/Core/Cleipnir.ResilientFunctions/Reactive/Origin/SourceSubscription.cs @@ -1,7 +1,6 @@ using System; using System.Threading.Tasks; -using Cleipnir.ResilientFunctions.CoreRuntime; -using Cleipnir.ResilientFunctions.Domain; +using Cleipnir.ResilientFunctions.Reactive.Operators; namespace Cleipnir.ResilientFunctions.Reactive.Origin; @@ -19,7 +18,6 @@ internal class SourceSubscription : ISubscription private readonly Func _isWorkflowRunning; public bool IsWorkflowRunning => _isWorkflowRunning(); public IReactiveChain Source { get; } - public IRegisteredTimeouts RegisteredTimeouts { get; } public TimeSpan DefaultMessageSyncDelay { get; } public TimeSpan DefaultMessageMaxWait { get; } @@ -30,7 +28,6 @@ public SourceSubscription( SyncStore syncStore, Func initialSyncPerformed, Func isWorkflowRunning, - IRegisteredTimeouts registeredTimeouts, TimeSpan defaultDelay, TimeSpan defaultMessageMaxWait ) @@ -43,7 +40,6 @@ TimeSpan defaultMessageMaxWait _syncStore = syncStore; _initialSyncPerformed = initialSyncPerformed; _isWorkflowRunning = isWorkflowRunning; - RegisteredTimeouts = registeredTimeouts; DefaultMessageSyncDelay = defaultDelay; DefaultMessageMaxWait = defaultMessageMaxWait; } @@ -70,6 +66,6 @@ public void PushMessages() _onNext(toEmit.Event!); } - public Task RegisterTimeout() => Task.CompletedTask; + public Task RegisterTimeout() => Task.FromResult(default(RegisterTimeoutResult?)); public Task CancelTimeout() => Task.CompletedTask; } \ No newline at end of file diff --git a/Core/Cleipnir.ResilientFunctions/Storage/IFunctionStore.cs b/Core/Cleipnir.ResilientFunctions/Storage/IFunctionStore.cs index 3024bfbf..a895258a 100644 --- a/Core/Cleipnir.ResilientFunctions/Storage/IFunctionStore.cs +++ b/Core/Cleipnir.ResilientFunctions/Storage/IFunctionStore.cs @@ -64,8 +64,7 @@ Task SucceedFunction( byte[]? result, long timestamp, int expectedEpoch, - IReadOnlyList? effects, - IReadOnlyList? messages, + IReadOnlyList? effects, ComplimentaryState complimentaryState ); @@ -75,8 +74,7 @@ Task PostponeFunction( long timestamp, bool ignoreInterrupted, int expectedEpoch, - IReadOnlyList? effects, - IReadOnlyList? messages, + IReadOnlyList? effects, ComplimentaryState complimentaryState ); @@ -85,8 +83,7 @@ Task FailFunction( StoredException storedException, long timestamp, int expectedEpoch, - IReadOnlyList? effects, - IReadOnlyList? messages, + IReadOnlyList? effects, ComplimentaryState complimentaryState ); @@ -94,8 +91,7 @@ Task SuspendFunction( StoredId storedId, long timestamp, int expectedEpoch, - IReadOnlyList? effects, - IReadOnlyList? messages, + IReadOnlyList? effects, ComplimentaryState complimentaryState ); diff --git a/Core/Cleipnir.ResilientFunctions/Storage/InMemoryFunctionStore.cs b/Core/Cleipnir.ResilientFunctions/Storage/InMemoryFunctionStore.cs index 74b4ec5e..b59a0a0a 100644 --- a/Core/Cleipnir.ResilientFunctions/Storage/InMemoryFunctionStore.cs +++ b/Core/Cleipnir.ResilientFunctions/Storage/InMemoryFunctionStore.cs @@ -231,8 +231,7 @@ public Task SucceedFunction( byte[]? result, long timestamp, int expectedEpoch, - IReadOnlyList? effects, - IReadOnlyList? messages, + IReadOnlyList? effects, ComplimentaryState complimentaryState) { lock (_sync) @@ -242,6 +241,9 @@ public Task SucceedFunction( var state = _states[storedId]; if (state.Epoch != expectedEpoch) return false.ToTask(); + if (effects != null) + _effectsStore.SetEffectResults(storedId, effects).Wait(); + state.Status = Status.Succeeded; state.Result = result; state.Timestamp = timestamp; @@ -256,8 +258,7 @@ public Task PostponeFunction( long timestamp, bool ignoreInterrupted, int expectedEpoch, - IReadOnlyList? effects, - IReadOnlyList? messages, + IReadOnlyList? effects, ComplimentaryState complimentaryState) { lock (_sync) @@ -266,10 +267,13 @@ public Task PostponeFunction( var state = _states[storedId]; if (state.Epoch != expectedEpoch) return false.ToTask(); - + if (!ignoreInterrupted && state.Interrupted) return false.ToTask(); + if (effects != null) + _effectsStore.SetEffectResults(storedId, effects).Wait(); + state.Status = Status.Postponed; state.Expires = postponeUntil; state.Timestamp = timestamp; @@ -283,8 +287,7 @@ public Task FailFunction( StoredException storedException, long timestamp, int expectedEpoch, - IReadOnlyList? effects, - IReadOnlyList? messages, + IReadOnlyList? effects, ComplimentaryState complimentaryState) { lock (_sync) @@ -294,6 +297,9 @@ public Task FailFunction( var state = _states[storedId]; if (state.Epoch != expectedEpoch) return false.ToTask(); + if (effects != null) + _effectsStore.SetEffectResults(storedId, effects).Wait(); + state.Status = Status.Failed; state.Exception = storedException; state.Timestamp = timestamp; @@ -306,15 +312,17 @@ public Task SuspendFunction( StoredId storedId, long timestamp, int expectedEpoch, - IReadOnlyList? effects, - IReadOnlyList? messages, + IReadOnlyList? effects, ComplimentaryState complimentaryState) { lock (_sync) { if (!_states.ContainsKey(storedId)) return false.ToTask(); - + + if (effects != null) + _effectsStore.SetEffectResults(storedId, effects).Wait(); + var state = _states[storedId]; if (state.Epoch != expectedEpoch) return false.ToTask(); @@ -524,6 +532,9 @@ private class InnerState } } + public Task AppendMessageNoStatusAndInterrupt(StoredId storedId, StoredMessage storedMessage) + => AppendMessage(storedId, storedMessage); + public async Task AppendMessages(IReadOnlyList messages, bool interrupt = true) { foreach (var (storedId, storedMessage) in messages) diff --git a/Samples/Sample.ConsoleApp/Utils/CrashableFunctionStore.cs b/Samples/Sample.ConsoleApp/Utils/CrashableFunctionStore.cs index 22a43a06..71c96f29 100644 --- a/Samples/Sample.ConsoleApp/Utils/CrashableFunctionStore.cs +++ b/Samples/Sample.ConsoleApp/Utils/CrashableFunctionStore.cs @@ -94,12 +94,11 @@ public Task SucceedFunction( byte[]? result, long timestamp, int expectedEpoch, - IReadOnlyList? effects, - IReadOnlyList? messages, + IReadOnlyList? effects, ComplimentaryState complimentaryState ) => _crashed ? Task.FromException(new TimeoutException()) - : _inner.SucceedFunction(storedId, result, timestamp, expectedEpoch, effects, messages, complimentaryState); + : _inner.SucceedFunction(storedId, result, timestamp, expectedEpoch, effects, complimentaryState); public Task PostponeFunction( StoredId storedId, @@ -107,34 +106,31 @@ public Task PostponeFunction( long timestamp, bool ignoreInterrupted, int expectedEpoch, - IReadOnlyList? effects, - IReadOnlyList? messages, + IReadOnlyList? effects, ComplimentaryState complimentaryState ) => _crashed ? Task.FromException(new TimeoutException()) - : _inner.PostponeFunction(storedId, postponeUntil, timestamp, ignoreInterrupted, expectedEpoch, effects, messages, complimentaryState); + : _inner.PostponeFunction(storedId, postponeUntil, timestamp, ignoreInterrupted, expectedEpoch, effects, complimentaryState); public Task FailFunction( StoredId storedId, StoredException storedException, long timestamp, int expectedEpoch, - IReadOnlyList? effects, - IReadOnlyList? messages, + IReadOnlyList? effects, ComplimentaryState complimentaryState ) => _crashed ? Task.FromException(new TimeoutException()) - : _inner.FailFunction(storedId, storedException, timestamp, expectedEpoch, effects, messages, complimentaryState); + : _inner.FailFunction(storedId, storedException, timestamp, expectedEpoch, effects, complimentaryState); public Task SuspendFunction( StoredId storedId, long timestamp, int expectedEpoch, - IReadOnlyList? effects, - IReadOnlyList? messages, + IReadOnlyList? effects, ComplimentaryState complimentaryState ) => _crashed ? Task.FromException(new TimeoutException()) - : _inner.SuspendFunction(storedId, timestamp, expectedEpoch, effects, messages, complimentaryState); + : _inner.SuspendFunction(storedId, timestamp, expectedEpoch, effects, complimentaryState); public Task Interrupt(StoredId storedId, bool onlyIfExecuting) => _crashed diff --git a/Stores/MariaDB/Cleipnir.ResilientFunctions.MariaDB.Tests/Messaging/MessagesTests.cs b/Stores/MariaDB/Cleipnir.ResilientFunctions.MariaDB.Tests/Messaging/MessagesTests.cs index 112c55af..cf660870 100644 --- a/Stores/MariaDB/Cleipnir.ResilientFunctions.MariaDB.Tests/Messaging/MessagesTests.cs +++ b/Stores/MariaDB/Cleipnir.ResilientFunctions.MariaDB.Tests/Messaging/MessagesTests.cs @@ -21,6 +21,10 @@ public override Task MessagesFirstOfTypesReturnsFirstForFirstOfTypesOnFirst() public override Task MessagesFirstOfTypesReturnsSecondForFirstOfTypesOnSecond() => MessagesFirstOfTypesReturnsSecondForFirstOfTypesOnSecond(FunctionStoreFactory.Create()); + [TestMethod] + public override Task MessagesFirstOfTypesReturnsNoneForTimeout() + => MessagesFirstOfTypesReturnsNoneForTimeout(FunctionStoreFactory.Create()); + [TestMethod] public override Task ExistingEventsShouldBeSameAsAllAfterEmit() => ExistingEventsShouldBeSameAsAllAfterEmit(FunctionStoreFactory.Create()); diff --git a/Stores/MariaDB/Cleipnir.ResilientFunctions.MariaDB.Tests/RFunctionTests/ControlPanelTests.cs b/Stores/MariaDB/Cleipnir.ResilientFunctions.MariaDB.Tests/RFunctionTests/ControlPanelTests.cs index 887c38d6..4e6f7b34 100644 --- a/Stores/MariaDB/Cleipnir.ResilientFunctions.MariaDB.Tests/RFunctionTests/ControlPanelTests.cs +++ b/Stores/MariaDB/Cleipnir.ResilientFunctions.MariaDB.Tests/RFunctionTests/ControlPanelTests.cs @@ -164,14 +164,6 @@ public override Task ExistingEffectCanBeRemoved() [TestMethod] public override Task EffectsAreOnlyFetchedOnPropertyInvocation() => EffectsAreOnlyFetchedOnPropertyInvocation(FunctionStoreFactory.Create()); - - [TestMethod] - public override Task ExistingTimeoutCanBeUpdatedForAction() - => ExistingTimeoutCanBeUpdatedForAction(FunctionStoreFactory.Create()); - - [TestMethod] - public override Task ExistingTimeoutCanBeUpdatedForFunc() - => ExistingTimeoutCanBeUpdatedForFunc(FunctionStoreFactory.Create()); [TestMethod] public override Task CorrelationsCanBeChanged() diff --git a/Stores/MariaDB/Cleipnir.ResilientFunctions.MariaDB.Tests/RFunctionTests/EffectTests.cs b/Stores/MariaDB/Cleipnir.ResilientFunctions.MariaDB.Tests/RFunctionTests/EffectTests.cs index fe7d70fe..3edc21f1 100644 --- a/Stores/MariaDB/Cleipnir.ResilientFunctions.MariaDB.Tests/RFunctionTests/EffectTests.cs +++ b/Stores/MariaDB/Cleipnir.ResilientFunctions.MariaDB.Tests/RFunctionTests/EffectTests.cs @@ -28,7 +28,15 @@ public override Task ExceptionThrowingActionTest() [TestMethod] public override Task TaskWhenAnyFuncTest() => TaskWhenAnyFuncTest(FunctionStoreFactory.Create()); - + + [TestMethod] + public override Task TaskWhenAnyPostponeTest() + => TaskWhenAnyPostponeTest(FunctionStoreFactory.Create()); + + [TestMethod] + public override Task TaskWhenAllPostponeTest() + => TaskWhenAllPostponeTest(FunctionStoreFactory.Create()); + [TestMethod] public override Task ClearEffectsTest() => ClearEffectsTest(FunctionStoreFactory.Create()); @@ -36,7 +44,11 @@ public override Task ClearEffectsTest() [TestMethod] public override Task EffectsCrudTest() => EffectsCrudTest(FunctionStoreFactory.Create()); - + + [TestMethod] + public override Task EffectsCreateOrGetWithoutFlushTest() + => EffectsCreateOrGetWithoutFlushTest(FunctionStoreFactory.Create()); + [TestMethod] public override Task ExistingEffectsFuncIsOnlyInvokedAfterGettingValue() => ExistingEffectsFuncIsOnlyInvokedAfterGettingValue(FunctionStoreFactory.Create()); diff --git a/Stores/MariaDB/Cleipnir.ResilientFunctions.MariaDB.Tests/RFunctionTests/SunshineTests.cs b/Stores/MariaDB/Cleipnir.ResilientFunctions.MariaDB.Tests/RFunctionTests/SunshineTests.cs index 17ccdbfa..3c234ff0 100644 --- a/Stores/MariaDB/Cleipnir.ResilientFunctions.MariaDB.Tests/RFunctionTests/SunshineTests.cs +++ b/Stores/MariaDB/Cleipnir.ResilientFunctions.MariaDB.Tests/RFunctionTests/SunshineTests.cs @@ -88,4 +88,20 @@ public override Task ParamlessCanBeCreatedWithInitialFailedEffect() [TestMethod] public override Task FunctionCanAcceptAndReturnOptionType() => FunctionCanAcceptAndReturnOptionType(FunctionStoreFactory.Create()); + + [TestMethod] + public override Task PendingEffectChangesArePersistedWithSucceedFunctionResult() + => PendingEffectChangesArePersistedWithSucceedFunctionResult(FunctionStoreFactory.Create()); + + [TestMethod] + public override Task PendingEffectChangesArePersistedWithPostponedFunctionResult() + => PendingEffectChangesArePersistedWithPostponedFunctionResult(FunctionStoreFactory.Create()); + + [TestMethod] + public override Task PendingEffectChangesArePersistedWithSuspendedFunctionResult() + => PendingEffectChangesArePersistedWithSuspendedFunctionResult(FunctionStoreFactory.Create()); + + [TestMethod] + public override Task PendingEffectChangesArePersistedWithFailedFunctionResult() + => PendingEffectChangesArePersistedWithFailedFunctionResult(FunctionStoreFactory.Create()); } \ No newline at end of file diff --git a/Stores/MariaDB/Cleipnir.ResilientFunctions.MariaDB.Tests/RFunctionTests/TimeoutTests.cs b/Stores/MariaDB/Cleipnir.ResilientFunctions.MariaDB.Tests/RFunctionTests/TimeoutTests.cs index 3d92df69..e43fb399 100644 --- a/Stores/MariaDB/Cleipnir.ResilientFunctions.MariaDB.Tests/RFunctionTests/TimeoutTests.cs +++ b/Stores/MariaDB/Cleipnir.ResilientFunctions.MariaDB.Tests/RFunctionTests/TimeoutTests.cs @@ -13,18 +13,6 @@ public override Task ExpiredTimeoutIsAddedToMessages() public override Task ExpiredTimeoutMakesReactiveChainThrowTimeoutException() => ExpiredTimeoutMakesReactiveChainThrowTimeoutException(FunctionStoreFactory.Create()); - [TestMethod] - public override Task RegisteredTimeoutIsCancelledAfterReactiveChainCompletes() - => RegisteredTimeoutIsCancelledAfterReactiveChainCompletes(FunctionStoreFactory.Create()); - - [TestMethod] - public override Task PendingTimeoutCanBeRemovedFromControlPanel() - => PendingTimeoutCanBeRemovedFromControlPanel(FunctionStoreFactory.Create()); - - [TestMethod] - public override Task PendingTimeoutCanBeUpdatedFromControlPanel() - => PendingTimeoutCanBeUpdatedFromControlPanel(FunctionStoreFactory.Create()); - [TestMethod] public override Task ExpiredImplicitTimeoutsAreAddedToMessages() => ExpiredImplicitTimeoutsAreAddedToMessages(FunctionStoreFactory.Create()); diff --git a/Stores/MariaDB/Cleipnir.ResilientFunctions.MariaDB.Tests/StoreTests.cs b/Stores/MariaDB/Cleipnir.ResilientFunctions.MariaDB.Tests/StoreTests.cs index d5d3a2f6..b1b2658c 100644 --- a/Stores/MariaDB/Cleipnir.ResilientFunctions.MariaDB.Tests/StoreTests.cs +++ b/Stores/MariaDB/Cleipnir.ResilientFunctions.MariaDB.Tests/StoreTests.cs @@ -218,5 +218,24 @@ public override Task RestartExecutionReturnsEffectsAndMessages() [TestMethod] public override Task RestartExecutionWorksWithEmptyEffectsAndMessages() => RestartExecutionWorksWithEmptyEffectsAndMessages(FunctionStoreFactory.Create()); + + [TestMethod] + public override Task EffectsArePersistedOnSuspendFunction() + => EffectsArePersistedOnSuspendFunction(FunctionStoreFactory.Create()); + [TestMethod] + public override Task EffectsArePersistedOnSucceededFunction() + => EffectsArePersistedOnSucceededFunction(FunctionStoreFactory.Create()); + + [TestMethod] + public override Task EffectsArePersistedOnPostponeFunction() + => EffectsArePersistedOnPostponeFunction(FunctionStoreFactory.Create()); + + [TestMethod] + public override Task EffectsArePersistedOnFailFunction() + => EffectsArePersistedOnFailFunction(FunctionStoreFactory.Create()); + + [TestMethod] + public override Task AppendMessageNoStatusAndInterruptWorks() + => AppendMessageNoStatusAndInterruptWorks(FunctionStoreFactory.Create()); } \ No newline at end of file diff --git a/Stores/MariaDB/Cleipnir.ResilientFunctions.MariaDB.Tests/TimeoutStoreTests.cs b/Stores/MariaDB/Cleipnir.ResilientFunctions.MariaDB.Tests/TimeoutStoreTests.cs index efa13b3e..f5ffb138 100644 --- a/Stores/MariaDB/Cleipnir.ResilientFunctions.MariaDB.Tests/TimeoutStoreTests.cs +++ b/Stores/MariaDB/Cleipnir.ResilientFunctions.MariaDB.Tests/TimeoutStoreTests.cs @@ -17,28 +17,12 @@ public override Task ExistingTimeoutCanUpdatedSuccessfully() [TestMethod] public override Task OverwriteFalseDoesNotAffectExistingTimeout() => OverwriteFalseDoesNotAffectExistingTimeout(FunctionStoreFactory.Create().SelectAsync(s => s.TimeoutStore)); - - [TestMethod] - public override Task RegisteredTimeoutIsReturnedFromRegisteredTimeouts() - => RegisteredTimeoutIsReturnedFromRegisteredTimeouts(FunctionStoreFactory.Create()); - + [TestMethod] public override Task TimeoutStoreCanBeInitializedMultipleTimes() => TimeoutStoreCanBeInitializedMultipleTimes(FunctionStoreFactory.Create().SelectAsync(s => s.TimeoutStore)); - [TestMethod] - public override Task RegisteredTimeoutIsReturnedFromRegisteredTimeoutsForFunctionId() - => RegisteredTimeoutIsReturnedFromRegisteredTimeoutsForFunctionId(FunctionStoreFactory.Create()); - - [TestMethod] - public override Task TimeoutIsNotRegisteredAgainWhenProviderAlreadyContainsTimeout() - => TimeoutIsNotRegisteredAgainWhenProviderAlreadyContainsTimeout(FunctionStoreFactory.Create()); - [TestMethod] public override Task TimeoutsForDifferentTypesCanBeCreatedFetchedSuccessfully() => TimeoutsForDifferentTypesCanBeCreatedFetchedSuccessfully(FunctionStoreFactory.Create().SelectAsync(fs => fs.TimeoutStore)); - - [TestMethod] - public override Task CancellingNonExistingTimeoutDoesResultInIO() - => CancellingNonExistingTimeoutDoesResultInIO(FunctionStoreFactory.Create()); } \ No newline at end of file diff --git a/Stores/MariaDB/Cleipnir.ResilientFunctions.MariaDB/MariaDbEffectsStore.cs b/Stores/MariaDB/Cleipnir.ResilientFunctions.MariaDB/MariaDbEffectsStore.cs index 1b22292d..201d3e85 100644 --- a/Stores/MariaDB/Cleipnir.ResilientFunctions.MariaDB/MariaDbEffectsStore.cs +++ b/Stores/MariaDB/Cleipnir.ResilientFunctions.MariaDB/MariaDbEffectsStore.cs @@ -71,7 +71,7 @@ public async Task SetEffectResults(StoredId storedId, IReadOnlyList CreateFunction( if (effects?.Any() ?? false) { - var effectsCommand = _sqlGenerator.UpdateEffects( + var effectsCommand = _sqlGenerator.UpsertEffects( effects.Select(e => new StoredEffectChange(storedId, e.StoredEffectId, CrudOperation.Insert, e)).ToList() ); storeCommand = storeCommand.Merge(effectsCommand); @@ -345,17 +345,27 @@ public async Task SucceedFunction( byte[]? result, long timestamp, int expectedEpoch, - IReadOnlyList? effects, - IReadOnlyList? messages, + IReadOnlyList? effects, ComplimentaryState complimentaryState) { - await using var conn = await CreateOpenConnection(_connectionString); - await using var command = _sqlGenerator - .SucceedFunction(storedId, result, timestamp, expectedEpoch) - .ToSqlCommand(conn); + var succeedCommand = _sqlGenerator.SucceedFunction(storedId, result, timestamp, expectedEpoch); + var effectsCommand = effects == null + ? null + : _sqlGenerator.UpsertEffects(effects); - var affectedRows = await command.ExecuteNonQueryAsync(); - return affectedRows == 1; + await using var conn = await CreateOpenConnection(_connectionString); + if (effects == null) + { + await using var command = succeedCommand.ToSqlCommand(conn); + var affectedRows = await command.ExecuteNonQueryAsync(); + return affectedRows == 1; + } + else + { + await using var command = StoreCommand.Merge(effectsCommand!, succeedCommand).ToSqlCommand(conn); + var affectedRows = await command.ExecuteNonQueryAsync(); + return affectedRows == 1 + (effects?.Count ?? 0); + } } public async Task PostponeFunction( @@ -364,17 +374,28 @@ public async Task PostponeFunction( long timestamp, bool ignoreInterrupted, int expectedEpoch, - IReadOnlyList? effects, - IReadOnlyList? messages, + IReadOnlyList? effects, ComplimentaryState complimentaryState) { - await using var conn = await CreateOpenConnection(_connectionString); - await using var command = _sqlGenerator - .PostponeFunction(storedId, postponeUntil, timestamp, ignoreInterrupted, expectedEpoch) - .ToSqlCommand(conn); + var postponeCommand = _sqlGenerator + .PostponeFunction(storedId, postponeUntil, timestamp, ignoreInterrupted, expectedEpoch); + var effectsCommand = effects == null + ? null + : _sqlGenerator.UpsertEffects(effects); - var affectedRows = await command.ExecuteNonQueryAsync(); - return affectedRows == 1; + await using var conn = await CreateOpenConnection(_connectionString); + if (effects == null) + { + await using var command = postponeCommand.ToSqlCommand(conn); + var affectedRows = await command.ExecuteNonQueryAsync(); + return affectedRows == 1; + } + else + { + await using var command = StoreCommand.Merge(effectsCommand!, postponeCommand).ToSqlCommand(conn); + var affectedRows = await command.ExecuteNonQueryAsync(); + return affectedRows == 1 + (effects?.Count ?? 0); + } } public async Task FailFunction( @@ -382,34 +403,54 @@ public async Task FailFunction( StoredException storedException, long timestamp, int expectedEpoch, - IReadOnlyList? effects, - IReadOnlyList? messages, + IReadOnlyList? effects, ComplimentaryState complimentaryState) { - await using var conn = await CreateOpenConnection(_connectionString); - await using var command = _sqlGenerator - .FailFunction(storedId, storedException, timestamp, expectedEpoch) - .ToSqlCommand(conn); + var failCommand = _sqlGenerator.FailFunction(storedId, storedException, timestamp, expectedEpoch); + var effectsCommand = effects == null + ? null + : _sqlGenerator.UpsertEffects(effects); - var affectedRows = await command.ExecuteNonQueryAsync(); - return affectedRows == 1; + await using var conn = await CreateOpenConnection(_connectionString); + if (effects == null) + { + await using var command = failCommand.ToSqlCommand(conn); + var affectedRows = await command.ExecuteNonQueryAsync(); + return affectedRows == 1; + } + else + { + await using var command = StoreCommand.Merge(effectsCommand!, failCommand).ToSqlCommand(conn); + var affectedRows = await command.ExecuteNonQueryAsync(); + return affectedRows == 1 + (effects?.Count ?? 0); + } } public async Task SuspendFunction( StoredId storedId, long timestamp, int expectedEpoch, - IReadOnlyList? effects, - IReadOnlyList? messages, + IReadOnlyList? effects, ComplimentaryState complimentaryState) { + var suspendCommand = _sqlGenerator.SuspendFunction(storedId, timestamp, expectedEpoch); + var effectsCommand = effects == null + ? null + : _sqlGenerator.UpsertEffects(effects); + await using var conn = await CreateOpenConnection(_connectionString); - await using var command = _sqlGenerator - .SuspendFunction(storedId, timestamp, expectedEpoch) - .ToSqlCommand(conn); - - var affectedRows = await command.ExecuteNonQueryAsync(); - return affectedRows == 1; + if (effects == null) + { + await using var command = suspendCommand.ToSqlCommand(conn); + var affectedRows = await command.ExecuteNonQueryAsync(); + return affectedRows == 1; + } + else + { + await using var command = StoreCommand.Merge(effectsCommand!, suspendCommand).ToSqlCommand(conn); + var affectedRows = await command.ExecuteNonQueryAsync(); + return affectedRows == 1 + effects.Count; + } } private string? _interruptSql; diff --git a/Stores/MariaDB/Cleipnir.ResilientFunctions.MariaDB/MariaDbMessageStore.cs b/Stores/MariaDB/Cleipnir.ResilientFunctions.MariaDB/MariaDbMessageStore.cs index 2589aa80..e2daf362 100644 --- a/Stores/MariaDB/Cleipnir.ResilientFunctions.MariaDB/MariaDbMessageStore.cs +++ b/Stores/MariaDB/Cleipnir.ResilientFunctions.MariaDB/MariaDbMessageStore.cs @@ -108,6 +108,15 @@ INSERT INTO {_tablePrefix}_messages return null; } + public async Task AppendMessageNoStatusAndInterrupt(StoredId storedId, StoredMessage storedMessage) + { + await using var conn = await DatabaseHelper.CreateOpenConnection(_connectionString); + await using var command = _sqlGenerator + .AppendMessage(storedId, storedMessage) + .ToSqlCommand(conn); + await command.ExecuteNonQueryAsync(); + } + public async Task AppendMessages(IReadOnlyList messages, bool interrupt = true) { if (messages.Count == 0) diff --git a/Stores/MariaDB/Cleipnir.ResilientFunctions.MariaDB/SqlGenerator.cs b/Stores/MariaDB/Cleipnir.ResilientFunctions.MariaDB/SqlGenerator.cs index 02239e7d..72cd1717 100644 --- a/Stores/MariaDB/Cleipnir.ResilientFunctions.MariaDB/SqlGenerator.cs +++ b/Stores/MariaDB/Cleipnir.ResilientFunctions.MariaDB/SqlGenerator.cs @@ -79,9 +79,8 @@ public async Task> ReadEffects(MySqlDataReader reade return functions; } - - public StoreCommand UpdateEffects(IReadOnlyList changes) + public StoreCommand UpsertEffects(IReadOnlyList changes) { var upsertCommand = default(StoreCommand); @@ -315,7 +314,7 @@ public StoreCommand SuspendFunction(StoredId storedId, long timestamp, int expec WHERE type = ? AND instance = ? AND epoch = ? AND - NOT interrupted"; + NOT interrupted;"; return StoreCommand.Create( _suspendFunctionSql, @@ -363,45 +362,32 @@ public StoreCommand RestartExecution(StoredId storedId, int expectedEpoch, long return command; } - public async Task ReadToStoredFunction(StoredId storedId, MySqlDataReader reader) + private string? _appendMessageSql; + public StoreCommand AppendMessage(StoredId storedId, StoredMessage storedMessage) { - const int paramIndex = 0; - const int statusIndex = 1; - const int resultIndex = 2; - const int exceptionIndex = 3; - const int epochIndex = 4; - const int expiresIndex = 5; - const int interruptedIndex = 6; - const int timestampIndex = 7; - const int humanInstanceIdIndex = 8; - const int parentIndex = 9; - - while (await reader.ReadAsync()) - { - var hasParam = !await reader.IsDBNullAsync(paramIndex); - var hasResult = !await reader.IsDBNullAsync(resultIndex); - var hasError = !await reader.IsDBNullAsync(exceptionIndex); - var hasParent = !await reader.IsDBNullAsync(parentIndex); - var storedException = hasError - ? JsonSerializer.Deserialize(reader.GetString(exceptionIndex)) - : null; - return new StoredFlow( - storedId, - HumanInstanceId: reader.GetString(humanInstanceIdIndex), - hasParam ? (byte[]) reader.GetValue(paramIndex) : null, - Status: (Status) reader.GetInt32(statusIndex), - Result: hasResult ? (byte[]) reader.GetValue(resultIndex) : null, - storedException, Epoch: reader.GetInt32(epochIndex), - Expires: reader.GetInt64(expiresIndex), - Interrupted: reader.GetBoolean(interruptedIndex), - Timestamp: reader.GetInt64(timestampIndex), - ParentId: hasParent ? StoredId.Deserialize(reader.GetString(parentIndex)) : null - ); - } + _appendMessageSql ??= @$" + INSERT INTO {tablePrefix}_messages + (type, instance, position, message_json, message_type, idempotency_key) + SELECT ?, ?, COALESCE(MAX(position), -1) + 1, ?, ?, ? + FROM {tablePrefix}_messages + WHERE type = ? AND instance = ?;"; - return null; + var command = StoreCommand.Create( + _appendMessageSql, + values: + [ + storedId.Type.Value, + storedId.Instance.Value.ToString("N"), + storedMessage.MessageContent, + storedMessage.MessageType, + storedMessage.IdempotencyKey ?? (object)DBNull.Value, + storedId.Type.Value, + storedId.Instance.Value.ToString("N") + ] + ); + return command; } - + public StoreCommand AppendMessages(IReadOnlyList messages) { var sql = @$" diff --git a/Stores/PostgreSQL/Cleipnir.ResilientFunctions.PostgreSQL.Tests/Messaging/MessagesTests.cs b/Stores/PostgreSQL/Cleipnir.ResilientFunctions.PostgreSQL.Tests/Messaging/MessagesTests.cs index a7089572..6a9d993e 100644 --- a/Stores/PostgreSQL/Cleipnir.ResilientFunctions.PostgreSQL.Tests/Messaging/MessagesTests.cs +++ b/Stores/PostgreSQL/Cleipnir.ResilientFunctions.PostgreSQL.Tests/Messaging/MessagesTests.cs @@ -22,6 +22,10 @@ public override Task MessagesFirstOfTypesReturnsFirstForFirstOfTypesOnFirst() public override Task MessagesFirstOfTypesReturnsSecondForFirstOfTypesOnSecond() => MessagesFirstOfTypesReturnsSecondForFirstOfTypesOnSecond(FunctionStoreFactory.Create()); + [TestMethod] + public override Task MessagesFirstOfTypesReturnsNoneForTimeout() + => MessagesFirstOfTypesReturnsNoneForTimeout(FunctionStoreFactory.Create()); + [TestMethod] public override Task ExistingEventsShouldBeSameAsAllAfterEmit() => ExistingEventsShouldBeSameAsAllAfterEmit(FunctionStoreFactory.Create()); diff --git a/Stores/PostgreSQL/Cleipnir.ResilientFunctions.PostgreSQL.Tests/RFunctionTests/ControlPanelTests.cs b/Stores/PostgreSQL/Cleipnir.ResilientFunctions.PostgreSQL.Tests/RFunctionTests/ControlPanelTests.cs index b22cf134..344d59d4 100644 --- a/Stores/PostgreSQL/Cleipnir.ResilientFunctions.PostgreSQL.Tests/RFunctionTests/ControlPanelTests.cs +++ b/Stores/PostgreSQL/Cleipnir.ResilientFunctions.PostgreSQL.Tests/RFunctionTests/ControlPanelTests.cs @@ -133,15 +133,7 @@ public override Task ExistingStateCanBeReplacedRemovedAndAdded() [TestMethod] public override Task SaveChangesPersistsChangedResult() => SaveChangesPersistsChangedResult(FunctionStoreFactory.Create()); - - [TestMethod] - public override Task ExistingTimeoutCanBeUpdatedForAction() - => ExistingTimeoutCanBeUpdatedForAction(FunctionStoreFactory.Create()); - - [TestMethod] - public override Task ExistingTimeoutCanBeUpdatedForFunc() - => ExistingTimeoutCanBeUpdatedForFunc(FunctionStoreFactory.Create()); - + [TestMethod] public override Task ExistingEffectCanBeRemoved() => ExistingEffectCanBeRemoved(FunctionStoreFactory.Create()); diff --git a/Stores/PostgreSQL/Cleipnir.ResilientFunctions.PostgreSQL.Tests/RFunctionTests/EffectTests.cs b/Stores/PostgreSQL/Cleipnir.ResilientFunctions.PostgreSQL.Tests/RFunctionTests/EffectTests.cs index 3a4b4338..9001ad9b 100644 --- a/Stores/PostgreSQL/Cleipnir.ResilientFunctions.PostgreSQL.Tests/RFunctionTests/EffectTests.cs +++ b/Stores/PostgreSQL/Cleipnir.ResilientFunctions.PostgreSQL.Tests/RFunctionTests/EffectTests.cs @@ -34,10 +34,22 @@ public override Task TaskWhenAnyFuncTest() public override Task TaskWhenAllFuncTest() => TaskWhenAllFuncTest(FunctionStoreFactory.Create()); + [TestMethod] + public override Task TaskWhenAnyPostponeTest() + => TaskWhenAnyPostponeTest(FunctionStoreFactory.Create()); + + [TestMethod] + public override Task TaskWhenAllPostponeTest() + => TaskWhenAllPostponeTest(FunctionStoreFactory.Create()); + [TestMethod] public override Task ClearEffectsTest() => ClearEffectsTest(FunctionStoreFactory.Create()); + [TestMethod] + public override Task EffectsCreateOrGetWithoutFlushTest() + => EffectsCreateOrGetWithoutFlushTest(FunctionStoreFactory.Create()); + [TestMethod] public override Task ExistingEffectsFuncIsOnlyInvokedAfterGettingValue() => ExistingEffectsFuncIsOnlyInvokedAfterGettingValue(FunctionStoreFactory.Create()); diff --git a/Stores/PostgreSQL/Cleipnir.ResilientFunctions.PostgreSQL.Tests/RFunctionTests/SunshineTests.cs b/Stores/PostgreSQL/Cleipnir.ResilientFunctions.PostgreSQL.Tests/RFunctionTests/SunshineTests.cs index 7c3704c2..39cb443b 100644 --- a/Stores/PostgreSQL/Cleipnir.ResilientFunctions.PostgreSQL.Tests/RFunctionTests/SunshineTests.cs +++ b/Stores/PostgreSQL/Cleipnir.ResilientFunctions.PostgreSQL.Tests/RFunctionTests/SunshineTests.cs @@ -89,4 +89,20 @@ public override Task ParamlessCanBeCreatedWithInitialFailedEffect() [TestMethod] public override Task FunctionCanAcceptAndReturnOptionType() => FunctionCanAcceptAndReturnOptionType(FunctionStoreFactory.Create()); + + [TestMethod] + public override Task PendingEffectChangesArePersistedWithSucceedFunctionResult() + => PendingEffectChangesArePersistedWithSucceedFunctionResult(FunctionStoreFactory.Create()); + + [TestMethod] + public override Task PendingEffectChangesArePersistedWithPostponedFunctionResult() + => PendingEffectChangesArePersistedWithPostponedFunctionResult(FunctionStoreFactory.Create()); + + [TestMethod] + public override Task PendingEffectChangesArePersistedWithSuspendedFunctionResult() + => PendingEffectChangesArePersistedWithSuspendedFunctionResult(FunctionStoreFactory.Create()); + + [TestMethod] + public override Task PendingEffectChangesArePersistedWithFailedFunctionResult() + => PendingEffectChangesArePersistedWithFailedFunctionResult(FunctionStoreFactory.Create()); } \ No newline at end of file diff --git a/Stores/PostgreSQL/Cleipnir.ResilientFunctions.PostgreSQL.Tests/RFunctionTests/TimeoutTests.cs b/Stores/PostgreSQL/Cleipnir.ResilientFunctions.PostgreSQL.Tests/RFunctionTests/TimeoutTests.cs index 4295cde5..87566c98 100644 --- a/Stores/PostgreSQL/Cleipnir.ResilientFunctions.PostgreSQL.Tests/RFunctionTests/TimeoutTests.cs +++ b/Stores/PostgreSQL/Cleipnir.ResilientFunctions.PostgreSQL.Tests/RFunctionTests/TimeoutTests.cs @@ -14,18 +14,6 @@ public override Task ExpiredTimeoutIsAddedToMessages() public override Task ExpiredTimeoutMakesReactiveChainThrowTimeoutException() => ExpiredTimeoutMakesReactiveChainThrowTimeoutException(FunctionStoreFactory.Create()); - [TestMethod] - public override Task RegisteredTimeoutIsCancelledAfterReactiveChainCompletes() - => RegisteredTimeoutIsCancelledAfterReactiveChainCompletes(FunctionStoreFactory.Create()); - - [TestMethod] - public override Task PendingTimeoutCanBeRemovedFromControlPanel() - => PendingTimeoutCanBeRemovedFromControlPanel(FunctionStoreFactory.Create()); - - [TestMethod] - public override Task PendingTimeoutCanBeUpdatedFromControlPanel() - => PendingTimeoutCanBeUpdatedFromControlPanel(FunctionStoreFactory.Create()); - [TestMethod] public override Task ExpiredImplicitTimeoutsAreAddedToMessages() => ExpiredImplicitTimeoutsAreAddedToMessages(FunctionStoreFactory.Create()); diff --git a/Stores/PostgreSQL/Cleipnir.ResilientFunctions.PostgreSQL.Tests/StoreTests.cs b/Stores/PostgreSQL/Cleipnir.ResilientFunctions.PostgreSQL.Tests/StoreTests.cs index 1af7219e..5efa2ee3 100644 --- a/Stores/PostgreSQL/Cleipnir.ResilientFunctions.PostgreSQL.Tests/StoreTests.cs +++ b/Stores/PostgreSQL/Cleipnir.ResilientFunctions.PostgreSQL.Tests/StoreTests.cs @@ -221,4 +221,23 @@ public override Task RestartExecutionReturnsEffectsAndMessages() public override Task RestartExecutionWorksWithEmptyEffectsAndMessages() => RestartExecutionWorksWithEmptyEffectsAndMessages(FunctionStoreFactory.Create()); + [TestMethod] + public override Task EffectsArePersistedOnSuspendFunction() + => EffectsArePersistedOnSuspendFunction(FunctionStoreFactory.Create()); + + [TestMethod] + public override Task EffectsArePersistedOnSucceededFunction() + => EffectsArePersistedOnSucceededFunction(FunctionStoreFactory.Create()); + + [TestMethod] + public override Task EffectsArePersistedOnPostponeFunction() + => EffectsArePersistedOnPostponeFunction(FunctionStoreFactory.Create()); + + [TestMethod] + public override Task EffectsArePersistedOnFailFunction() + => EffectsArePersistedOnFailFunction(FunctionStoreFactory.Create()); + + [TestMethod] + public override Task AppendMessageNoStatusAndInterruptWorks() + => AppendMessageNoStatusAndInterruptWorks(FunctionStoreFactory.Create()); } \ No newline at end of file diff --git a/Stores/PostgreSQL/Cleipnir.ResilientFunctions.PostgreSQL.Tests/TimeoutStoreTests.cs b/Stores/PostgreSQL/Cleipnir.ResilientFunctions.PostgreSQL.Tests/TimeoutStoreTests.cs index e2759d8a..fa601b20 100644 --- a/Stores/PostgreSQL/Cleipnir.ResilientFunctions.PostgreSQL.Tests/TimeoutStoreTests.cs +++ b/Stores/PostgreSQL/Cleipnir.ResilientFunctions.PostgreSQL.Tests/TimeoutStoreTests.cs @@ -18,26 +18,10 @@ public override Task ExistingTimeoutCanUpdatedSuccessfully() [TestMethod] public override Task TimeoutStoreCanBeInitializedMultipleTimes() => TimeoutStoreCanBeInitializedMultipleTimes(FunctionStoreFactory.Create().SelectAsync(s => s.TimeoutStore)); - - [TestMethod] - public override Task RegisteredTimeoutIsReturnedFromRegisteredTimeoutsForFunctionId() - => RegisteredTimeoutIsReturnedFromRegisteredTimeoutsForFunctionId(FunctionStoreFactory.Create()); - - [TestMethod] - public override Task TimeoutIsNotRegisteredAgainWhenProviderAlreadyContainsTimeout() - => TimeoutIsNotRegisteredAgainWhenProviderAlreadyContainsTimeout(FunctionStoreFactory.Create()); - + [TestMethod] public override Task TimeoutsForDifferentTypesCanBeCreatedFetchedSuccessfully() => TimeoutsForDifferentTypesCanBeCreatedFetchedSuccessfully(FunctionStoreFactory.Create().SelectAsync(fs => fs.TimeoutStore)); - - [TestMethod] - public override Task CancellingNonExistingTimeoutDoesResultInIO() - => CancellingNonExistingTimeoutDoesResultInIO(FunctionStoreFactory.Create()); - - [TestMethod] - public override Task RegisteredTimeoutIsReturnedFromRegisteredTimeouts() - => RegisteredTimeoutIsReturnedFromRegisteredTimeouts(FunctionStoreFactory.Create()); [TestMethod] public override Task OverwriteFalseDoesNotAffectExistingTimeout() diff --git a/Stores/PostgreSQL/Cleipnir.ResilientFunctions.PostgreSQL/PostgreSqlFunctionStore.cs b/Stores/PostgreSQL/Cleipnir.ResilientFunctions.PostgreSQL/PostgreSqlFunctionStore.cs index 8d9709ef..a790add6 100644 --- a/Stores/PostgreSQL/Cleipnir.ResilientFunctions.PostgreSQL/PostgreSqlFunctionStore.cs +++ b/Stores/PostgreSQL/Cleipnir.ResilientFunctions.PostgreSQL/PostgreSqlFunctionStore.cs @@ -378,20 +378,31 @@ public async Task SucceedFunction( byte[]? result, long timestamp, int expectedEpoch, - IReadOnlyList? effects, - IReadOnlyList? messages, + IReadOnlyList? effects, ComplimentaryState complimentaryState) { - await using var conn = await CreateConnection(); - await using var command = _sqlGenerator.SucceedFunction( - storedId, - result, - timestamp, - expectedEpoch - ).ToNpgsqlCommand(conn); - - var affectedRows = await command.ExecuteNonQueryAsync(); - return affectedRows == 1; + var succeedCommand = _sqlGenerator.SucceedFunction(storedId, result, timestamp, expectedEpoch); + var effectsCommand = effects == null + ? [] + : _sqlGenerator.UpdateEffects(effects); + + if (effects == null) + { + await using var conn = await CreateConnection(); + await using var command = succeedCommand.ToNpgsqlCommand(conn); + var affectedRows = await command.ExecuteNonQueryAsync(); + return affectedRows == 1; + } + else + { + await using var conn = await CreateConnection(); + await using var command = effectsCommand + .Append(succeedCommand) + .CreateBatch(conn); + + var affectedRows = await command.ExecuteNonQueryAsync(); + return affectedRows == 1 + effects.Count; + } } public async Task PostponeFunction( @@ -400,21 +411,37 @@ public async Task PostponeFunction( long timestamp, bool ignoreInterrupted, int expectedEpoch, - IReadOnlyList? effects, - IReadOnlyList? messages, + IReadOnlyList? effects, ComplimentaryState complimentaryState) { - await using var conn = await CreateConnection(); - await using var command = _sqlGenerator.PostponeFunction( + var postponeCommand = _sqlGenerator.PostponeFunction( storedId, postponeUntil, timestamp, ignoreInterrupted, expectedEpoch - ).ToNpgsqlCommand(conn); - - var affectedRows = await command.ExecuteNonQueryAsync(); - return affectedRows == 1; + ); + var effectsCommand = effects == null + ? [] + : _sqlGenerator.UpdateEffects(effects); + + if (effects == null) + { + await using var conn = await CreateConnection(); + await using var command = postponeCommand.ToNpgsqlCommand(conn); + var affectedRows = await command.ExecuteNonQueryAsync(); + return affectedRows == 1; + } + else + { + await using var conn = await CreateConnection(); + await using var command = effectsCommand + .Append(postponeCommand) + .CreateBatch(conn); + + var affectedRows = await command.ExecuteNonQueryAsync(); + return affectedRows == 1 + effects.Count; + } } public async Task FailFunction( @@ -422,37 +449,67 @@ public async Task FailFunction( StoredException storedException, long timestamp, int expectedEpoch, - IReadOnlyList? effects, - IReadOnlyList? messages, + IReadOnlyList? effects, ComplimentaryState complimentaryState) { - await using var conn = await CreateConnection(); - await using var command = _sqlGenerator.FailFunction( + var failCommand = _sqlGenerator.FailFunction( storedId, storedException, timestamp, expectedEpoch - ).ToNpgsqlCommand(conn); + ); + var effectsCommand = effects == null + ? [] + : _sqlGenerator.UpdateEffects(effects); - var affectedRows = await command.ExecuteNonQueryAsync(); - return affectedRows == 1; + if (effects == null) + { + await using var conn = await CreateConnection(); + await using var command = failCommand.ToNpgsqlCommand(conn); + var affectedRows = await command.ExecuteNonQueryAsync(); + return affectedRows == 1; + } + else + { + await using var conn = await CreateConnection(); + await using var command = effectsCommand + .Append(failCommand) + .CreateBatch(conn); + + var affectedRows = await command.ExecuteNonQueryAsync(); + return affectedRows == 1 + effects.Count; + } } public async Task SuspendFunction( StoredId storedId, long timestamp, int expectedEpoch, - IReadOnlyList? effects, - IReadOnlyList? messages, + IReadOnlyList? effects, ComplimentaryState complimentaryState) { - await using var conn = await CreateConnection(); - - await using var command = _sqlGenerator - .SuspendFunction(storedId, timestamp, expectedEpoch) - .ToNpgsqlCommand(conn); - var affectedRows = await command.ExecuteNonQueryAsync(); - return affectedRows == 1; + var suspendCommand = _sqlGenerator.SuspendFunction(storedId, timestamp, expectedEpoch); + var effectsCommand = effects == null + ? [] + : _sqlGenerator.UpdateEffects(effects); + + if (effects == null) + { + await using var conn = await CreateConnection(); + await using var command = suspendCommand.ToNpgsqlCommand(conn); + var affectedRows = await command.ExecuteNonQueryAsync(); + return affectedRows == 1; + } + else + { + await using var conn = await CreateConnection(); + await using var command = effectsCommand + .Append(suspendCommand) + .CreateBatch(conn); + + var affectedRows = await command.ExecuteNonQueryAsync(); + return affectedRows == 1 + effects.Count; + } } private string? _setParametersSql; diff --git a/Stores/PostgreSQL/Cleipnir.ResilientFunctions.PostgreSQL/PostgreSqlMessageStore.cs b/Stores/PostgreSQL/Cleipnir.ResilientFunctions.PostgreSQL/PostgreSqlMessageStore.cs index 0e92a08d..ab22375b 100644 --- a/Stores/PostgreSQL/Cleipnir.ResilientFunctions.PostgreSQL/PostgreSqlMessageStore.cs +++ b/Stores/PostgreSQL/Cleipnir.ResilientFunctions.PostgreSQL/PostgreSqlMessageStore.cs @@ -122,6 +122,15 @@ INSERT INTO {_tablePrefix}_messages return null; } + public async Task AppendMessageNoStatusAndInterrupt(StoredId storedId, StoredMessage storedMessage) + { + await using var conn = await CreateConnection(); + await using var command = sqlGenerator + .AppendMessage(storedId, storedMessage) + .ToNpgsqlCommand(conn); + await command.ExecuteNonQueryAsync(); + } + public async Task AppendMessages(IReadOnlyList messages, bool interrupt = true) { var maxPositions = await GetMaxPositions( diff --git a/Stores/PostgreSQL/Cleipnir.ResilientFunctions.PostgreSQL/SqlGenerator.cs b/Stores/PostgreSQL/Cleipnir.ResilientFunctions.PostgreSQL/SqlGenerator.cs index 4eb33068..e488897d 100644 --- a/Stores/PostgreSQL/Cleipnir.ResilientFunctions.PostgreSQL/SqlGenerator.cs +++ b/Stores/PostgreSQL/Cleipnir.ResilientFunctions.PostgreSQL/SqlGenerator.cs @@ -75,7 +75,7 @@ public async Task> ReadEffects(NpgsqlDataReader read return functions; } - public IEnumerable UpdateEffects(IReadOnlyList changes) + public IReadOnlyList UpdateEffects(IReadOnlyList changes) { var commands = new List(changes.Count); @@ -360,6 +360,29 @@ 9 parent return null; } + private string? _appendMessageSql; + public StoreCommand AppendMessage(StoredId storedId, StoredMessage storedMessage) + { + var (messageJson, messageType, idempotencyKey) = storedMessage; + _appendMessageSql ??= @$" + INSERT INTO {tablePrefix}_messages + (type, instance, position, message_json, message_type, idempotency_key) + VALUES ( + $1, $2, + (SELECT COALESCE(MAX(position), -1) + 1 FROM {tablePrefix}_messages WHERE type = $1 AND instance = $2), + $3, $4, $5 + );"; + + var command = StoreCommand.Create(_appendMessageSql); + command.AddParameter(storedId.Type.Value); + command.AddParameter(storedId.Instance.Value); + command.AddParameter(messageJson); + command.AddParameter(messageType); + command.AddParameter(idempotencyKey ?? (object)DBNull.Value); + + return command; + } + public StoreCommand AppendMessages(IReadOnlyList messages) { var sql = @$" diff --git a/Stores/PostgreSQL/Cleipnir.ResilientFunctions.PostgreSQL/StoreCommandExtensions.cs b/Stores/PostgreSQL/Cleipnir.ResilientFunctions.PostgreSQL/StoreCommandExtensions.cs index 98624c5b..7e68534f 100644 --- a/Stores/PostgreSQL/Cleipnir.ResilientFunctions.PostgreSQL/StoreCommandExtensions.cs +++ b/Stores/PostgreSQL/Cleipnir.ResilientFunctions.PostgreSQL/StoreCommandExtensions.cs @@ -40,6 +40,15 @@ public static NpgsqlBatch CreateBatch(this IEnumerable commands) return batch; } + public static NpgsqlBatch CreateBatch(this IEnumerable commands, NpgsqlConnection conn) + { + var batch = new NpgsqlBatch(conn); + foreach (var command in commands) + batch.BatchCommands.Add(command.ToNpgsqlBatchCommand()); + + return batch; + } + public static NpgsqlBatch WithConnection(this NpgsqlBatch batch, NpgsqlConnection conn) { batch.Connection = conn; diff --git a/Stores/SqlServer/Cleipnir.ResilientFunctions.SqlServer.Tests/EffectTests.cs b/Stores/SqlServer/Cleipnir.ResilientFunctions.SqlServer.Tests/EffectTests.cs index 9b14003f..f8e79768 100644 --- a/Stores/SqlServer/Cleipnir.ResilientFunctions.SqlServer.Tests/EffectTests.cs +++ b/Stores/SqlServer/Cleipnir.ResilientFunctions.SqlServer.Tests/EffectTests.cs @@ -30,6 +30,14 @@ public override Task ExceptionThrowingActionTest() public override Task TaskWhenAnyFuncTest() => TaskWhenAnyFuncTest(FunctionStoreFactory.Create()); + [TestMethod] + public override Task TaskWhenAnyPostponeTest() + => TaskWhenAnyPostponeTest(FunctionStoreFactory.Create()); + + [TestMethod] + public override Task TaskWhenAllPostponeTest() + => TaskWhenAllPostponeTest(FunctionStoreFactory.Create()); + [TestMethod] public override Task ClearEffectsTest() => ClearEffectsTest(FunctionStoreFactory.Create()); @@ -38,6 +46,10 @@ public override Task ClearEffectsTest() public override Task EffectsCrudTest() => EffectsCrudTest(FunctionStoreFactory.Create()); + [TestMethod] + public override Task EffectsCreateOrGetWithoutFlushTest() + => EffectsCreateOrGetWithoutFlushTest(FunctionStoreFactory.Create()); + [TestMethod] public override Task ExistingEffectsFuncIsOnlyInvokedAfterGettingValue() => ExistingEffectsFuncIsOnlyInvokedAfterGettingValue(FunctionStoreFactory.Create()); diff --git a/Stores/SqlServer/Cleipnir.ResilientFunctions.SqlServer.Tests/Messaging/MessagesTests.cs b/Stores/SqlServer/Cleipnir.ResilientFunctions.SqlServer.Tests/Messaging/MessagesTests.cs index 26925bf4..b9cd48bd 100644 --- a/Stores/SqlServer/Cleipnir.ResilientFunctions.SqlServer.Tests/Messaging/MessagesTests.cs +++ b/Stores/SqlServer/Cleipnir.ResilientFunctions.SqlServer.Tests/Messaging/MessagesTests.cs @@ -21,6 +21,10 @@ public override Task MessagesFirstOfTypesReturnsFirstForFirstOfTypesOnFirst() [TestMethod] public override Task MessagesFirstOfTypesReturnsSecondForFirstOfTypesOnSecond() => MessagesFirstOfTypesReturnsSecondForFirstOfTypesOnSecond(FunctionStoreFactory.Create()); + + [TestMethod] + public override Task MessagesFirstOfTypesReturnsNoneForTimeout() + => MessagesFirstOfTypesReturnsNoneForTimeout(FunctionStoreFactory.Create()); [TestMethod] public override Task ExistingEventsShouldBeSameAsAllAfterEmit() diff --git a/Stores/SqlServer/Cleipnir.ResilientFunctions.SqlServer.Tests/RFunctionTests/ControlPanelTests.cs b/Stores/SqlServer/Cleipnir.ResilientFunctions.SqlServer.Tests/RFunctionTests/ControlPanelTests.cs index 4132b0f5..b289337b 100644 --- a/Stores/SqlServer/Cleipnir.ResilientFunctions.SqlServer.Tests/RFunctionTests/ControlPanelTests.cs +++ b/Stores/SqlServer/Cleipnir.ResilientFunctions.SqlServer.Tests/RFunctionTests/ControlPanelTests.cs @@ -166,15 +166,6 @@ public override Task ExistingEffectCanBeReplaced() public override Task EffectCanBeStarted() => EffectCanBeStarted(FunctionStoreFactory.Create()); - - [TestMethod] - public override Task ExistingTimeoutCanBeUpdatedForAction() - => ExistingTimeoutCanBeUpdatedForAction(FunctionStoreFactory.Create()); - - [TestMethod] - public override Task ExistingTimeoutCanBeUpdatedForFunc() - => ExistingTimeoutCanBeUpdatedForFunc(FunctionStoreFactory.Create()); - [TestMethod] public override Task CorrelationsCanBeChanged() => CorrelationsCanBeChanged(FunctionStoreFactory.Create()); diff --git a/Stores/SqlServer/Cleipnir.ResilientFunctions.SqlServer.Tests/RFunctionTests/SunshineTests.cs b/Stores/SqlServer/Cleipnir.ResilientFunctions.SqlServer.Tests/RFunctionTests/SunshineTests.cs index cea50c1f..0d76d8fa 100644 --- a/Stores/SqlServer/Cleipnir.ResilientFunctions.SqlServer.Tests/RFunctionTests/SunshineTests.cs +++ b/Stores/SqlServer/Cleipnir.ResilientFunctions.SqlServer.Tests/RFunctionTests/SunshineTests.cs @@ -89,4 +89,20 @@ public override Task ParamlessCanBeCreatedWithInitialFailedEffect() [TestMethod] public override Task FunctionCanAcceptAndReturnOptionType() => FunctionCanAcceptAndReturnOptionType(FunctionStoreFactory.Create()); + + [TestMethod] + public override Task PendingEffectChangesArePersistedWithSucceedFunctionResult() + => PendingEffectChangesArePersistedWithSucceedFunctionResult(FunctionStoreFactory.Create()); + + [TestMethod] + public override Task PendingEffectChangesArePersistedWithPostponedFunctionResult() + => PendingEffectChangesArePersistedWithPostponedFunctionResult(FunctionStoreFactory.Create()); + + [TestMethod] + public override Task PendingEffectChangesArePersistedWithSuspendedFunctionResult() + => PendingEffectChangesArePersistedWithSuspendedFunctionResult(FunctionStoreFactory.Create()); + + [TestMethod] + public override Task PendingEffectChangesArePersistedWithFailedFunctionResult() + => PendingEffectChangesArePersistedWithFailedFunctionResult(FunctionStoreFactory.Create()); } \ No newline at end of file diff --git a/Stores/SqlServer/Cleipnir.ResilientFunctions.SqlServer.Tests/RFunctionTests/TimeoutTests.cs b/Stores/SqlServer/Cleipnir.ResilientFunctions.SqlServer.Tests/RFunctionTests/TimeoutTests.cs index 19250d0a..562adb0c 100644 --- a/Stores/SqlServer/Cleipnir.ResilientFunctions.SqlServer.Tests/RFunctionTests/TimeoutTests.cs +++ b/Stores/SqlServer/Cleipnir.ResilientFunctions.SqlServer.Tests/RFunctionTests/TimeoutTests.cs @@ -14,18 +14,6 @@ public override Task ExpiredTimeoutIsAddedToMessages() public override Task ExpiredTimeoutMakesReactiveChainThrowTimeoutException() => ExpiredTimeoutMakesReactiveChainThrowTimeoutException(FunctionStoreFactory.Create()); - [TestMethod] - public override Task RegisteredTimeoutIsCancelledAfterReactiveChainCompletes() - => RegisteredTimeoutIsCancelledAfterReactiveChainCompletes(FunctionStoreFactory.Create()); - - [TestMethod] - public override Task PendingTimeoutCanBeRemovedFromControlPanel() - => PendingTimeoutCanBeRemovedFromControlPanel(FunctionStoreFactory.Create()); - - [TestMethod] - public override Task PendingTimeoutCanBeUpdatedFromControlPanel() - => PendingTimeoutCanBeUpdatedFromControlPanel(FunctionStoreFactory.Create()); - [TestMethod] public override Task ExpiredImplicitTimeoutsAreAddedToMessages() => ExpiredImplicitTimeoutsAreAddedToMessages(FunctionStoreFactory.Create()); diff --git a/Stores/SqlServer/Cleipnir.ResilientFunctions.SqlServer.Tests/StoreTests.cs b/Stores/SqlServer/Cleipnir.ResilientFunctions.SqlServer.Tests/StoreTests.cs index 8da94739..70e915d5 100644 --- a/Stores/SqlServer/Cleipnir.ResilientFunctions.SqlServer.Tests/StoreTests.cs +++ b/Stores/SqlServer/Cleipnir.ResilientFunctions.SqlServer.Tests/StoreTests.cs @@ -220,4 +220,23 @@ public override Task RestartExecutionReturnsEffectsAndMessages() public override Task RestartExecutionWorksWithEmptyEffectsAndMessages() => RestartExecutionWorksWithEmptyEffectsAndMessages(FunctionStoreFactory.Create()); + [TestMethod] + public override Task EffectsArePersistedOnSuspendFunction() + => EffectsArePersistedOnSuspendFunction(FunctionStoreFactory.Create()); + + [TestMethod] + public override Task EffectsArePersistedOnSucceededFunction() + => EffectsArePersistedOnSucceededFunction(FunctionStoreFactory.Create()); + + [TestMethod] + public override Task EffectsArePersistedOnPostponeFunction() + => EffectsArePersistedOnPostponeFunction(FunctionStoreFactory.Create()); + + [TestMethod] + public override Task EffectsArePersistedOnFailFunction() + => EffectsArePersistedOnFailFunction(FunctionStoreFactory.Create()); + + [TestMethod] + public override Task AppendMessageNoStatusAndInterruptWorks() + => AppendMessageNoStatusAndInterruptWorks(FunctionStoreFactory.Create()); } \ No newline at end of file diff --git a/Stores/SqlServer/Cleipnir.ResilientFunctions.SqlServer.Tests/TimeoutStoreTests.cs b/Stores/SqlServer/Cleipnir.ResilientFunctions.SqlServer.Tests/TimeoutStoreTests.cs index fb193025..3b4f122f 100644 --- a/Stores/SqlServer/Cleipnir.ResilientFunctions.SqlServer.Tests/TimeoutStoreTests.cs +++ b/Stores/SqlServer/Cleipnir.ResilientFunctions.SqlServer.Tests/TimeoutStoreTests.cs @@ -19,27 +19,11 @@ public override Task ExistingTimeoutCanUpdatedSuccessfully() public override Task TimeoutStoreCanBeInitializedMultipleTimes() => TimeoutStoreCanBeInitializedMultipleTimes(FunctionStoreFactory.Create().SelectAsync(s => s.TimeoutStore)); - [TestMethod] - public override Task RegisteredTimeoutIsReturnedFromRegisteredTimeouts() - => RegisteredTimeoutIsReturnedFromRegisteredTimeouts(FunctionStoreFactory.Create()); - [TestMethod] public override Task OverwriteFalseDoesNotAffectExistingTimeout() => OverwriteFalseDoesNotAffectExistingTimeout(FunctionStoreFactory.Create().SelectAsync(s => s.TimeoutStore)); - [TestMethod] - public override Task RegisteredTimeoutIsReturnedFromRegisteredTimeoutsForFunctionId() - => RegisteredTimeoutIsReturnedFromRegisteredTimeoutsForFunctionId(FunctionStoreFactory.Create()); - - [TestMethod] - public override Task TimeoutIsNotRegisteredAgainWhenProviderAlreadyContainsTimeout() - => TimeoutIsNotRegisteredAgainWhenProviderAlreadyContainsTimeout(FunctionStoreFactory.Create()); - [TestMethod] public override Task TimeoutsForDifferentTypesCanBeCreatedFetchedSuccessfully() => TimeoutsForDifferentTypesCanBeCreatedFetchedSuccessfully(FunctionStoreFactory.Create().SelectAsync(fs => fs.TimeoutStore)); - - [TestMethod] - public override Task CancellingNonExistingTimeoutDoesResultInIO() - => CancellingNonExistingTimeoutDoesResultInIO(FunctionStoreFactory.Create()); } \ No newline at end of file diff --git a/Stores/SqlServer/Cleipnir.ResilientFunctions.SqlServer/SqlGenerator.cs b/Stores/SqlServer/Cleipnir.ResilientFunctions.SqlServer/SqlGenerator.cs index 4c871c6b..8c8353c8 100644 --- a/Stores/SqlServer/Cleipnir.ResilientFunctions.SqlServer/SqlGenerator.cs +++ b/Stores/SqlServer/Cleipnir.ResilientFunctions.SqlServer/SqlGenerator.cs @@ -426,6 +426,33 @@ public StoreCommand RestartExecution(StoredId storedId, int expectedEpoch, long return default; } + + private string? _appendMessageSql; + public StoreCommand AppendMessage(StoredId storedId, StoredMessage storedMessage, string paramPrefix) + { + _appendMessageSql ??= @$" + INSERT INTO {tablePrefix}_Messages + (FlowType, FlowInstance, Position, MessageJson, MessageType, IdempotencyKey) + VALUES ( + @FlowType, + @FlowInstance, + (SELECT COALESCE(MAX(position), -1) + 1 FROM {tablePrefix}_Messages WHERE FlowType = @FlowType AND FlowInstance = @FlowInstance), + @MessageJson, @MessageType, @IdempotencyKey + );"; + + var sql = _appendMessageSql; + if (paramPrefix != "") + sql = sql.Replace("@", $"@{paramPrefix}"); + + var command = StoreCommand.Create(sql); + command.AddParameter($"@{paramPrefix}FlowType", storedId.Type.Value); + command.AddParameter($"@{paramPrefix}FlowInstance", storedId.Instance.Value); + command.AddParameter($"@{paramPrefix}MessageJson", storedMessage.MessageContent); + command.AddParameter($"@{paramPrefix}MessageType", storedMessage.MessageType); + command.AddParameter($"@{paramPrefix}IdempotencyKey", storedMessage.IdempotencyKey ?? (object)DBNull.Value); + + return command; + } public StoreCommand? AppendMessages(IReadOnlyList messages, bool interrupt, string prefix = "") { diff --git a/Stores/SqlServer/Cleipnir.ResilientFunctions.SqlServer/SqlServerFunctionStore.cs b/Stores/SqlServer/Cleipnir.ResilientFunctions.SqlServer/SqlServerFunctionStore.cs index 4b379323..6cba393c 100644 --- a/Stores/SqlServer/Cleipnir.ResilientFunctions.SqlServer/SqlServerFunctionStore.cs +++ b/Stores/SqlServer/Cleipnir.ResilientFunctions.SqlServer/SqlServerFunctionStore.cs @@ -395,22 +395,25 @@ public async Task SucceedFunction( byte[]? result, long timestamp, int expectedEpoch, - IReadOnlyList? effects, - IReadOnlyList? messages, + IReadOnlyList? effects, ComplimentaryState complimentaryState) { + var succeedCommand = _sqlGenerator.SucceedFunction(storedId, result, timestamp, expectedEpoch, paramPrefix: "Succeed"); + var effectCommand = effects == null + ? null + : _sqlGenerator.UpdateEffects(effects, paramPrefix: "Effect"); + + var messageCommands = Array.Empty(); + await using var conn = await _connFunc(); - await using var command = _sqlGenerator - .SucceedFunction( - storedId, - result, - timestamp, - expectedEpoch, - paramPrefix: "" + await using var command = (effectCommand == null && messageCommands.Length == 0 + ? succeedCommand + : StoreCommand.Merge(messageCommands.Append(effectCommand).Append(succeedCommand))! ).ToSqlCommand(conn); - + + var expectedAffectedRows = 1 + (effects?.Count ?? 0); var affectedRows = await command.ExecuteNonQueryAsync(); - return affectedRows > 0; + return affectedRows == expectedAffectedRows; } public async Task PostponeFunction( @@ -419,22 +422,32 @@ public async Task PostponeFunction( long timestamp, bool ignoreInterrupted, int expectedEpoch, - IReadOnlyList? effects, - IReadOnlyList? messages, + IReadOnlyList? effects, ComplimentaryState complimentaryState) { - await using var conn = await _connFunc(); - await using var command = _sqlGenerator.PostponeFunction( + var postponeCommand = _sqlGenerator.PostponeFunction( storedId, postponeUntil, timestamp, ignoreInterrupted, expectedEpoch, - paramPrefix: "" - ).ToSqlCommand(conn); + paramPrefix: "Postpone" + ); + var effectCommand = effects == null + ? null + : _sqlGenerator.UpdateEffects(effects, paramPrefix: "Effect"); + + var messageCommands = Array.Empty(); + await using var conn = await _connFunc(); + await using var command = (effectCommand == null && messageCommands.Length == 0 + ? postponeCommand + : StoreCommand.Merge(messageCommands.Append(effectCommand).Append(postponeCommand))! + ).ToSqlCommand(conn); + + var expectedAffectedRows = 1 + (effects?.Count ?? 0); var affectedRows = await command.ExecuteNonQueryAsync(); - return affectedRows > 0; + return affectedRows == expectedAffectedRows; } public async Task FailFunction( @@ -442,44 +455,57 @@ public async Task FailFunction( StoredException storedException, long timestamp, int expectedEpoch, - IReadOnlyList? effects, - IReadOnlyList? messages, + IReadOnlyList? effects, ComplimentaryState complimentaryState) { - await using var conn = await _connFunc(); - await using var command = _sqlGenerator + var failCommand = _sqlGenerator .FailFunction( storedId, storedException, timestamp, expectedEpoch, paramPrefix: "" - ).ToSqlCommand(conn); + ); + var effectCommand = effects == null + ? null + : _sqlGenerator.UpdateEffects(effects, paramPrefix: "Effect"); + var messageCommands = Array.Empty(); + + await using var conn = await _connFunc(); + await using var command = (effectCommand == null && messageCommands.Length == 0 + ? failCommand + : StoreCommand.Merge(messageCommands.Append(effectCommand).Append(failCommand))! + ).ToSqlCommand(conn); + var expectedAffectedRows = 1 + (effects?.Count ?? 0); var affectedRows = await command.ExecuteNonQueryAsync(); - return affectedRows > 0; + return affectedRows == expectedAffectedRows; } public async Task SuspendFunction( StoredId storedId, long timestamp, int expectedEpoch, - IReadOnlyList? effects, - IReadOnlyList? messages, + IReadOnlyList? effects, ComplimentaryState complimentaryState) { + var suspendCommand = _sqlGenerator.SuspendFunction(storedId, timestamp, expectedEpoch, paramPrefix: "Suspend"); + var effectCommand = effects == null + ? null + : _sqlGenerator.UpdateEffects(effects, paramPrefix: "Effect"); + + var messageCommands = Array.Empty(); + await using var conn = await _connFunc(); - await using var command = _sqlGenerator - .SuspendFunction( - storedId, - timestamp, - expectedEpoch, - paramPrefix: "" - ).ToSqlCommand(conn); + await using var command = (effectCommand == null && messageCommands.Length == 0 + ? suspendCommand + : StoreCommand.Merge(messageCommands.Append(effectCommand).Append(suspendCommand))! + ).ToSqlCommand(conn); + var expectedAffectedRows = 1 + (effects?.Count ?? 0); var affectedRows = await command.ExecuteNonQueryAsync(); - return affectedRows == 1; + return affectedRows == expectedAffectedRows; } private string? _interruptSql; diff --git a/Stores/SqlServer/Cleipnir.ResilientFunctions.SqlServer/SqlServerMessageStore.cs b/Stores/SqlServer/Cleipnir.ResilientFunctions.SqlServer/SqlServerMessageStore.cs index 61e213ed..17c9be8f 100644 --- a/Stores/SqlServer/Cleipnir.ResilientFunctions.SqlServer/SqlServerMessageStore.cs +++ b/Stores/SqlServer/Cleipnir.ResilientFunctions.SqlServer/SqlServerMessageStore.cs @@ -46,6 +46,15 @@ public async Task TruncateTable() public async Task AppendMessage(StoredId storedId, StoredMessage storedMessage) => await AppendMessage(storedId, storedMessage, depth: 0); + public async Task AppendMessageNoStatusAndInterrupt(StoredId storedId, StoredMessage storedMessage) + { + await using var conn = await CreateConnection(); + await using var command = sqlGenerator + .AppendMessage(storedId, storedMessage, paramPrefix: "") + .ToSqlCommand(conn); + await command.ExecuteNonQueryAsync(); + } + public async Task AppendMessages(IReadOnlyList messages, bool interrupt = true) { if (messages.Count == 0) diff --git a/Stores/StressTests/StressTests/PostponedTest.cs b/Stores/StressTests/StressTests/PostponedTest.cs index 0d81ba84..4aad18e6 100644 --- a/Stores/StressTests/StressTests/PostponedTest.cs +++ b/Stores/StressTests/StressTests/PostponedTest.cs @@ -43,7 +43,6 @@ await store.PostponeFunction( ignoreInterrupted: true, expectedEpoch: 0, effects: null, - messages: null, complimentaryState: new ComplimentaryState(() => storedParameter.ToUtf8Bytes(), LeaseLength: 0) ); }