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

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
24 changes: 12 additions & 12 deletions Core/Cleipnir.ResilientFunctions.Tests/TestTemplates/StoreTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,7 @@ await store.SucceedFunction(
result: resultJson.ToUtf8Bytes(),
expectedReplica: ReplicaId.Empty,
timestamp: DateTime.UtcNow.Ticks,
effects: null,
effects: [],
messages: null,
storageSession: null
).ShouldBeTrueAsync();
Expand Down Expand Up @@ -460,7 +460,7 @@ await store.FailFunction(
storedException,
timestamp: DateTime.UtcNow.Ticks,
expectedReplica: ReplicaId.Empty,
effects: null,
effects: [],
messages: null,
storageSession: null
);
Expand Down Expand Up @@ -728,7 +728,7 @@ await store.SucceedFunction(
result: null,
DateTime.UtcNow.Ticks,
expectedReplica: ReplicaId.Empty,
effects: null,
effects: [],
messages: null,
storageSession: null
);
Expand Down Expand Up @@ -788,7 +788,7 @@ await store.FailFunction(
new StoredException("ExceptionMessage", ExceptionStackTrace: null, typeof(Exception).SimpleQualifiedName()),
timestamp: DateTime.UtcNow.Ticks,
expectedReplica: ReplicaId.Empty,
effects: null,
effects: [],
messages: null,
storageSession: null
);
Expand Down Expand Up @@ -877,7 +877,7 @@ await store.SucceedFunction(
result: null,
timestamp: timestamp,
expectedReplica: ReplicaId.Empty,
effects: null,
effects: [],
messages: null,
storageSession: null
).ShouldBeTrueAsync();
Expand Down Expand Up @@ -1127,7 +1127,7 @@ await store.SucceedFunction(
result: null,
timestamp: timestamp,
expectedReplica: ReplicaId.Empty,
effects: null,
effects: [],
messages: null,
storageSession: null
).ShouldBeTrueAsync();
Expand Down Expand Up @@ -1565,7 +1565,7 @@ await store.FailFunction(
storedException: new StoredException("SomeMessage", ExceptionStackTrace: null, "SomeExceptionType"),
timestamp: DateTime.UtcNow.Ticks,
expectedReplica: ReplicaId.Empty,
effects: null,
effects: [],
messages: null,
storageSession: null
).ShouldBeTrueAsync();
Expand Down Expand Up @@ -1627,7 +1627,7 @@ await store.SucceedFunction(
result: null,
timestamp: DateTime.UtcNow.Ticks,
expectedReplica: ReplicaId.Empty,
effects: null,
effects: [],
messages: null,
storageSession: null
).ShouldBeTrueAsync();
Expand Down Expand Up @@ -1664,7 +1664,7 @@ await store.SucceedFunction(
result: result1,
timestamp: DateTime.UtcNow.Ticks,
expectedReplica: ReplicaId.Empty,
effects: null,
effects: [],
messages: null,
storageSession: null
).ShouldBeTrueAsync();
Expand All @@ -1685,7 +1685,7 @@ await store.SucceedFunction(
result: result2,
timestamp: DateTime.UtcNow.Ticks,
expectedReplica: ReplicaId.Empty,
effects: null,
effects: [],
messages: null,
storageSession: null
).ShouldBeTrueAsync();
Expand Down Expand Up @@ -1748,7 +1748,7 @@ await store.SucceedFunction(
result: result,
timestamp: DateTime.UtcNow.Ticks,
expectedReplica: ReplicaId.Empty,
effects: null,
effects: [],
messages: null,
storageSession: null
).ShouldBeTrueAsync();
Expand Down Expand Up @@ -1812,7 +1812,7 @@ await store.SucceedFunction(
result: null,
timestamp: DateTime.UtcNow.Ticks,
expectedReplica: initialOwner,
effects: null,
effects: [],
messages: null,
storageSession: null
).ShouldBeTrueAsync();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -110,7 +110,7 @@ public Task<bool> SucceedFunction(
byte[]? result,
long timestamp,
ReplicaId expectedReplica,
IReadOnlyList<StoredEffect>? effects,
IReadOnlyList<StoredEffect> effects,
IReadOnlyList<StoredMessage>? messages,
IStorageSession? storageSession
) => _crashed
Expand Down Expand Up @@ -141,7 +141,7 @@ public Task<bool> FailFunction(
StoredException storedException,
long timestamp,
ReplicaId expectedReplica,
IReadOnlyList<StoredEffect>? effects,
IReadOnlyList<StoredEffect> effects,
IReadOnlyList<StoredMessage>? messages,
IStorageSession? storageSession
) => _crashed
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -102,7 +102,7 @@ public async Task<PersistedInStoreResult> PersistFunctionInStore(
public async Task<TReturn> WaitForFunctionResult(FlowId flowId, StoredId storedId, bool allowPostponedAndSuspended, TimeSpan? maxWait)
=> await _resultBusyWaiter.WaitForFunctionResult(flowId, storedId, allowPostponedAndSuspended, maxWait);

public async Task PersistFailure(StoredId storedId, FatalWorkflowException exception, TParam param)
public async Task PersistFailure(StoredId storedId, FatalWorkflowException exception, TParam param, IReadOnlyList<StoredEffect> effects)
{
var storedException = exception.ToStoredException();

Expand All @@ -111,15 +111,15 @@ public async Task PersistFailure(StoredId storedId, FatalWorkflowException excep
storedException,
timestamp: UtcNow().Ticks,
_replicaId,
effects: null,
effects,
messages: null,
storageSession: null
);
if (!success)
throw UnexpectedStateException.ConcurrentModification(storedId);
}

public async Task<PersistResultOutcome> PersistResult(StoredId storedId, Result<TReturn> result, TParam param, IStorageSession? storageSession)
public async Task<PersistResultOutcome> PersistResult(StoredId storedId, Result<TReturn> result, TParam param, IReadOnlyList<StoredEffect> effects, IStorageSession? storageSession)
{
switch (result.Outcome)
{
Expand All @@ -129,7 +129,7 @@ public async Task<PersistResultOutcome> PersistResult(StoredId storedId, Result<
result: SerializeResult(result.SucceedWithValue),
timestamp: UtcNow().Ticks,
_replicaId,
effects: null,
effects,
messages: null,
storageSession
) ? PersistResultOutcome.Success : PersistResultOutcome.Failed;
Expand All @@ -149,7 +149,7 @@ public async Task<PersistResultOutcome> PersistResult(StoredId storedId, Result<
storedException: result.Fail!.ToStoredException(),
timestamp: UtcNow().Ticks,
_replicaId,
effects: null,
effects,
messages: null,
storageSession
) ? PersistResultOutcome.Success : PersistResultOutcome.Failed;
Expand Down Expand Up @@ -273,7 +273,7 @@ await _functionStore.FailFunction(
storedException: FatalWorkflowException.CreateNonGeneric(flowId, e).ToStoredException(),
timestamp: UtcNow().Ticks,
_replicaId,
effects: null,
effects: [],
messages: null,
storageSession: null
);
Expand Down
20 changes: 10 additions & 10 deletions Core/Cleipnir.ResilientFunctions/CoreRuntime/Invocation/Invoker.cs
Original file line number Diff line number Diff line change
Expand Up @@ -75,14 +75,14 @@ public async Task<InnerScheduled<TReturn>> ScheduleInvoke(FlowInstance flowInsta
}
catch (FatalWorkflowException exception)
{
await PersistFailure(storedId, flowId, exception, param, parentWorkflow?.StoredId);
await PersistFailure(storedId, flowId, exception, param, parentWorkflow?.StoredId, workflow.Effect.GetStoredEffectsSnapshot());
tcs.TrySetCanceled();
return;
}
catch (Exception exception)
{
var fwe = FatalWorkflowException.CreateNonGeneric(flowId, exception);
await PersistFailure(storedId, flowId, fwe, param, parentWorkflow?.StoredId);
await PersistFailure(storedId, flowId, fwe, param, parentWorkflow?.StoredId, workflow.Effect.GetStoredEffectsSnapshot());
tcs.TrySetCanceled();
return;
}
Expand Down Expand Up @@ -173,14 +173,14 @@ public async Task<InnerScheduled<TReturn>> ScheduleRestart(StoredId storedId)
}
catch (FatalWorkflowException exception)
{
await PersistFailure(storedId, flowId, exception, param, parent);
await PersistFailure(storedId, flowId, exception, param, parent, workflow.Effect.GetStoredEffectsSnapshot());
tcs.TrySetCanceled();
return;
}
catch (Exception exception)
{
var fwe = FatalWorkflowException.CreateNonGeneric(flowId, exception);
await PersistFailure(storedId, flowId, fwe, param, parent);
await PersistFailure(storedId, flowId, fwe, param, parent, workflow.Effect.GetStoredEffectsSnapshot());
tcs.TrySetCanceled();
return;
}
Expand Down Expand Up @@ -226,8 +226,8 @@ internal async Task ScheduleRestart(StoredId storedId, RestartedFunction rf, Act
// *** USER FUNCTION INVOCATION ***
result = await inner(param, workflow);
}
catch (FatalWorkflowException exception) { await PersistFailure(storedId, flowId, exception, param, parent); tcs.TrySetCanceled(); throw; }
catch (Exception exception) { var fwe = FatalWorkflowException.CreateNonGeneric(flowId, exception); await PersistFailure(storedId, flowId, fwe, param, parent); tcs.TrySetCanceled(); throw fwe; }
catch (FatalWorkflowException exception) { await PersistFailure(storedId, flowId, exception, param, parent, workflow.Effect.GetStoredEffectsSnapshot()); tcs.TrySetCanceled(); throw; }
catch (Exception exception) { var fwe = FatalWorkflowException.CreateNonGeneric(flowId, exception); await PersistFailure(storedId, flowId, fwe, param, parent, workflow.Effect.GetStoredEffectsSnapshot()); tcs.TrySetCanceled(); throw fwe; }
finally { disposables.Dispose(); }

await PersistResultAndEnsureSuccess(storedId, flowId, result, param, parent, workflow, storageSession, allowPostponedOrSuspended: true);
Expand Down Expand Up @@ -391,18 +391,18 @@ private record PreparedReInvocation(
IStorageSession? StorageSession
);

private async Task PersistFailure(StoredId storedId, FlowId flowId, FatalWorkflowException exception, TParam param, StoredId? parent)
private async Task PersistFailure(StoredId storedId, FlowId flowId, FatalWorkflowException exception, TParam param, StoredId? parent, IReadOnlyList<StoredEffect> effects)
{
await _invocationHelper.PublishCompletionMessageToParent(parent, childId: flowId, result: Fail.WithException(exception));
await _invocationHelper.PersistFailure(storedId, exception, param);
await _invocationHelper.PersistFailure(storedId, exception, param, effects);
}

private async Task PersistResultAndEnsureSuccess(StoredId storedId, FlowId flowId, Result<TReturn> result, TParam param, StoredId? parent, Workflow workflow, IStorageSession? storageSession, bool allowPostponedOrSuspended = false)
{
await workflow.Effect.Flush();
await _invocationHelper.PublishCompletionMessageToParent(parent, childId: flowId, result);
var outcome = await _invocationHelper.PersistResult(storedId, result, param, storageSession);

var outcome = await _invocationHelper.PersistResult(storedId, result, param, workflow.Effect.GetStoredEffectsSnapshot(), storageSession);
switch (outcome)
{
case PersistResultOutcome.Failed:
Expand Down
2 changes: 2 additions & 0 deletions Core/Cleipnir.ResilientFunctions/Domain/Effect.cs
Original file line number Diff line number Diff line change
Expand Up @@ -103,6 +103,8 @@ internal bool TryGet<T>(string alias, out T? value)

// Raw StoredEffect access - for reserved entries with serializer-independent encodings (pending messages).
internal StoredEffect? GetStoredEffect(EffectId effectId) => effectResults.GetOrValueDefault(effectId);
// The current live effect snapshot (deletes/nulls excluded) - persisted atomically with the terminal status on succeed/fail.
internal IReadOnlyList<StoredEffect> GetStoredEffectsSnapshot() => effectResults.GetStoredEffectsSnapshot();
internal void FlushlessSet(StoredEffect storedEffect) => effectResults.FlushlessSet(storedEffect);
internal T Get<T>(string alias) => Get<T>(
effectResults.GetEffectId(alias) ?? throw new InvalidOperationException($"Unknown alias: '{alias}'")
Expand Down
11 changes: 11 additions & 0 deletions Core/Cleipnir.ResilientFunctions/Domain/EffectResults.cs
Original file line number Diff line number Diff line change
Expand Up @@ -104,6 +104,17 @@ public bool Contains(EffectId effectId)
return _effectResults.GetValueOrDefault(effectId)?.StoredEffect;
}

// The current snapshot of live stored effects - pending deletes and null entries are excluded. Handed to
// SucceedFunction/FailFunction so the terminal status and the effect state are persisted together atomically.
public IReadOnlyList<StoredEffect> GetStoredEffectsSnapshot()
{
lock (_sync)
return _effectResults.Values
.Where(c => c.Operation != CrudOperation.Delete && c.StoredEffect != null)
.Select(c => c.StoredEffect!)
.ToList();
}

public async Task Set(StoredEffect storedEffect, bool flush)
{
await FlushOrAddToPending(
Expand Down
4 changes: 2 additions & 2 deletions Core/Cleipnir.ResilientFunctions/Storage/IFunctionStore.cs
Original file line number Diff line number Diff line change
Expand Up @@ -59,7 +59,7 @@ Task<bool> SucceedFunction(
byte[]? result,
long timestamp,
ReplicaId expectedReplica,
IReadOnlyList<StoredEffect>? effects,
IReadOnlyList<StoredEffect> effects,
IReadOnlyList<StoredMessage>? messages,
IStorageSession? storageSession
);
Expand All @@ -79,7 +79,7 @@ Task<bool> FailFunction(
StoredException storedException,
long timestamp,
ReplicaId expectedReplica,
IReadOnlyList<StoredEffect>? effects,
IReadOnlyList<StoredEffect> effects,
IReadOnlyList<StoredMessage>? messages,
IStorageSession? storageSession
);
Expand Down
23 changes: 21 additions & 2 deletions Core/Cleipnir.ResilientFunctions/Storage/InMemoryFunctionStore.cs
Original file line number Diff line number Diff line change
Expand Up @@ -261,7 +261,7 @@ public Task<bool> SucceedFunction(
byte[]? result,
long timestamp,
ReplicaId? expectedReplica,
IReadOnlyList<StoredEffect>? effects,
IReadOnlyList<StoredEffect> effects,
IReadOnlyList<StoredMessage>? messages,
IStorageSession? storageSession)
{
Expand All @@ -272,6 +272,8 @@ public Task<bool> SucceedFunction(
var state = _states[storedId];
if (state.Owner != expectedReplica) return false.ToTask();

PersistEffects(storedId, effects, storageSession);

state.Status = Status.Succeeded;
state.Result = result;
state.Timestamp = timestamp;
Expand Down Expand Up @@ -311,7 +313,7 @@ public Task<bool> FailFunction(
StoredException storedException,
long timestamp,
ReplicaId? expectedReplica,
IReadOnlyList<StoredEffect>? effects,
IReadOnlyList<StoredEffect> effects,
IReadOnlyList<StoredMessage>? messages,
IStorageSession? storageSession)
{
Expand All @@ -322,6 +324,8 @@ public Task<bool> FailFunction(
var state = _states[storedId];
if (state.Owner != expectedReplica) return false.ToTask();

PersistEffects(storedId, effects, storageSession);

state.Status = Status.Failed;
state.Exception = storedException;
state.Timestamp = timestamp;
Expand All @@ -331,6 +335,21 @@ public Task<bool> FailFunction(
}
}

// Persists the terminal effect snapshot alongside the status change, mirroring the SQL stores writing the
// effects column in the same UPDATE. Empty snapshots are skipped so a failure path that never built effect
// state (e.g. a deserialization failure) does not erase previously flushed effects.
private void PersistEffects(StoredId storedId, IReadOnlyList<StoredEffect> effects, IStorageSession? storageSession)
{
if (effects.Count == 0)
return;

_effectsStore.SetEffectResults(
storedId,
effects.Select(e => new StoredEffectChange(storedId, e.EffectId, CrudOperation.Insert, e)).ToList(),
storageSession
);
}

public Task<bool> SuspendFunction(
StoredId storedId,
long timestamp,
Expand Down
4 changes: 2 additions & 2 deletions Samples/Sample.ConsoleApp/Utils/CrashableFunctionStore.cs
Original file line number Diff line number Diff line change
Expand Up @@ -97,7 +97,7 @@ public Task<bool> SucceedFunction(
byte[]? result,
long timestamp,
ReplicaId expectedReplica,
IReadOnlyList<StoredEffect>? effects,
IReadOnlyList<StoredEffect> effects,
IReadOnlyList<StoredMessage>? messages,
IStorageSession? storageSession
) => _crashed
Expand All @@ -121,7 +121,7 @@ public Task<bool> FailFunction(
StoredException storedException,
long timestamp,
ReplicaId expectedReplica,
IReadOnlyList<StoredEffect>? effects,
IReadOnlyList<StoredEffect> effects,
IReadOnlyList<StoredMessage>? messages,
IStorageSession? storageSession
) => _crashed
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,7 @@ public override Task ReInvokeRActionSucceedsAfterSuccessfullySavingParamAndState
=> ReInvokeRActionSucceedsAfterSuccessfullySavingParamAndState(FunctionStoreFactory.Create());

[TestMethod]
[Ignore("Known-flaky - to be resolved or removed by upcoming functionality changes")]
public override Task ControlPanelsExistingMessagesContainsPreviouslyAddedMessages()
=> ControlPanelsExistingMessagesContainsPreviouslyAddedMessages(FunctionStoreFactory.Create());

Expand Down
Loading