Skip to content

Commit 7571b4f

Browse files
committed
Improved effect id serialization and simplified sub-effect implementation
1 parent 6e340da commit 7571b4f

File tree

31 files changed

+463
-95
lines changed

31 files changed

+463
-95
lines changed
Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
using Cleipnir.ResilientFunctions.Domain;
2+
using Microsoft.VisualStudio.TestTools.UnitTesting;
3+
using Shouldly;
4+
5+
namespace Cleipnir.ResilientFunctions.Tests.InMemoryTests;
6+
7+
[TestClass]
8+
public class EffectContextTests
9+
{
10+
[TestMethod]
11+
public void NewlyCreatedContextHasZeroCounter()
12+
{
13+
var context = EffectContext.Empty;
14+
context.Parent.ShouldBeNull();
15+
context.NextImplicitId().ShouldBe("0");
16+
}
17+
18+
[TestMethod]
19+
public void CounterCanBeIncrementedWithoutCreatingContext()
20+
{
21+
var context = EffectContext.Empty;
22+
context.NextImplicitId();
23+
context.Parent.ShouldBeNull();
24+
context.NextImplicitId().ShouldBe("1");
25+
}
26+
}

Core/Cleipnir.ResilientFunctions.Tests/InMemoryTests/EffectIdTests.cs

Lines changed: 50 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -11,25 +11,69 @@ public class EffectIdTests
1111
[TestMethod]
1212
public void EffectIdWithStateCanBeDeserialized()
1313
{
14-
var effectId = new EffectId("SomeValue", IsState: true);
15-
var deserializedId = EffectId.Deserialize(effectId.Serialize());
14+
var effectId = new EffectId("SomeValue", EffectType.State, Context: "");
15+
var serializedId = effectId.Serialize();
16+
var deserializedId = EffectId.Deserialize(serializedId);
1617

17-
deserializedId.ShouldBe(effectId); ;
18+
deserializedId.ShouldBe(effectId);
19+
}
20+
21+
[TestMethod]
22+
public void EffectIdWithContextCanBeDeserialized()
23+
{
24+
var parentEffect = new EffectId("SomeParentId", EffectType.Effect, Context: "ESomeParentContext");
25+
var effectId = new EffectId("SomeValue", EffectType.State, Context: parentEffect.Serialize());
26+
var serializedId = effectId.Serialize();
27+
var deserializedId = EffectId.Deserialize(serializedId);
28+
29+
deserializedId.ShouldBe(effectId);
30+
}
31+
32+
[TestMethod]
33+
public void EffectIdWithContextAndEscapedCharactersCanBeDeserialized()
34+
{
35+
var parentEffect = new EffectId("SomeParentId", EffectType.Effect, Context: "");
36+
var effectId = new EffectId("Some.Value\\WithBackSlash", EffectType.State, Context: parentEffect.Serialize());
37+
var serializedId = effectId.Serialize();
38+
var deserializedId = EffectId.Deserialize(serializedId);
39+
40+
deserializedId.ShouldBe(effectId);
41+
}
42+
43+
[TestMethod]
44+
public void EffectIdWithBackslashIsSerializedCorrectly()
45+
{
46+
var effectId = new EffectId("\\", EffectType.State, Context: "");
47+
var serializedId = effectId.Serialize();
48+
serializedId.ShouldBe("S\\\\");
49+
var deserializedId = EffectId.Deserialize(serializedId);
50+
deserializedId.ShouldBe(effectId);
51+
}
52+
53+
[TestMethod]
54+
public void EffectIdWithDotIsSerializedCorrectly()
55+
{
56+
var effectId = new EffectId(".", EffectType.State, Context: "");
57+
var serializedId = effectId.Serialize();
58+
serializedId.ShouldBe("S\\.");
59+
var deserializedId = EffectId.Deserialize(serializedId);
60+
deserializedId.ShouldBe(effectId);
1861
}
1962

2063
[TestMethod]
2164
public void EffectIdWithoutStateCanBeDeserialized()
2265
{
23-
var effectId = new EffectId("SomeValue", IsState: false);
24-
var deserializedId = EffectId.Deserialize(effectId.Serialize());
66+
var effectId = new EffectId("SomeValue", EffectType.Effect, Context: "");
67+
var serializedId = effectId.Serialize();
68+
var deserializedId = EffectId.Deserialize(serializedId);
2569

2670
deserializedId.ShouldBe(effectId);
2771
}
2872

2973
[TestMethod]
3074
public void StoredEffectIdIsBasedOnSerializedEffectIdValue()
3175
{
32-
var effectId = new EffectId("SomeValue", IsState: false);
76+
var effectId = new EffectId("SomeId", EffectType.Effect, Context: new EffectId("SomeParentId", EffectType.Effect, Context: "ESomeParentContext").Serialize());
3377
var serializedEffectId = effectId.Serialize();
3478

3579
var storedEffectId = effectId.ToStoredEffectId();

Core/Cleipnir.ResilientFunctions.Tests/InMemoryTests/RFunctionTests/EffectTests.cs

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,4 @@
11
using System.Threading.Tasks;
2-
using Cleipnir.ResilientFunctions.Helpers;
3-
using Cleipnir.ResilientFunctions.Storage;
42
using Microsoft.VisualStudio.TestTools.UnitTesting;
53

64
namespace Cleipnir.ResilientFunctions.Tests.InMemoryTests.RFunctionTests;
@@ -47,4 +45,12 @@ public override Task EffectsCrudTest()
4745
[TestMethod]
4846
public override Task ExistingEffectsFuncIsOnlyInvokedAfterGettingValue()
4947
=> ExistingEffectsFuncIsOnlyInvokedAfterGettingValue(FunctionStoreFactory.Create());
48+
49+
[TestMethod]
50+
public override Task SubEffectHasImplicitContext()
51+
=> SubEffectHasImplicitContext(FunctionStoreFactory.Create());
52+
53+
[TestMethod]
54+
public override Task SubEffectHasExplicitContext()
55+
=> SubEffectHasExplicitContext(FunctionStoreFactory.Create());
5056
}

Core/Cleipnir.ResilientFunctions.Tests/TestTemplates/EffectStoreTests.cs

Lines changed: 12 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -17,14 +17,14 @@ protected async Task SunshineScenarioTest(Task<IEffectsStore> storeTask)
1717
var functionId = TestStoredId.Create();
1818
var storedEffect1 = new StoredEffect(
1919
"EffectId1".ToEffectId(),
20-
"EffectId1".ToStoredEffectId(isState: false),
20+
"EffectId1".ToStoredEffectId(EffectType.Effect),
2121
WorkStatus.Started,
2222
Result: null,
2323
StoredException: null
2424
);
2525
var storedEffect2 = new StoredEffect(
2626
"EffectId2".ToEffectId(),
27-
"EffectId2".ToStoredEffectId(isState: false),
27+
"EffectId2".ToStoredEffectId(EffectType.Effect),
2828
WorkStatus.Completed,
2929
Result: null,
3030
StoredException: null
@@ -65,7 +65,7 @@ protected async Task SingleEffectWithResultLifeCycle(Task<IEffectsStore> storeTa
6565
var functionId = TestStoredId.Create();
6666
var effect = new StoredEffect(
6767
"EffectId1".ToEffectId(),
68-
"EffectId1".ToStoredEffectId(isState: false),
68+
"EffectId1".ToStoredEffectId(EffectType.Effect),
6969
WorkStatus.Started,
7070
Result: null,
7171
StoredException: null
@@ -102,7 +102,7 @@ protected async Task SingleFailingEffectLifeCycle(Task<IEffectsStore> storeTask)
102102
);
103103
var storedEffect = new StoredEffect(
104104
"EffectId1".ToEffectId(),
105-
"EffectId1".ToStoredEffectId(isState: false),
105+
"EffectId1".ToStoredEffectId(EffectType.Effect),
106106
WorkStatus.Started,
107107
Result: null,
108108
StoredException: null
@@ -129,14 +129,14 @@ protected async Task EffectCanBeDeleted(Task<IEffectsStore> storeTask)
129129
var functionId = TestStoredId.Create();
130130
var storedEffect1 = new StoredEffect(
131131
"EffectId1".ToEffectId(),
132-
"EffectId1".ToStoredEffectId(isState: false),
132+
"EffectId1".ToStoredEffectId(EffectType.Effect),
133133
WorkStatus.Started,
134134
Result: null,
135135
StoredException: null
136136
);
137137
var storedEffect2 = new StoredEffect(
138138
"EffectId2".ToEffectId(),
139-
"EffectId2".ToStoredEffectId(isState: false),
139+
"EffectId2".ToStoredEffectId(EffectType.Effect),
140140
WorkStatus.Completed,
141141
Result: null,
142142
StoredException: null
@@ -175,14 +175,14 @@ protected async Task DeleteFunctionIdDeletesAllRelatedEffects(Task<IEffectsStore
175175

176176
var storedEffect1 = new StoredEffect(
177177
"EffectId1".ToEffectId(),
178-
"EffectId1".ToStoredEffectId(isState: false),
178+
"EffectId1".ToStoredEffectId(EffectType.Effect),
179179
WorkStatus.Started,
180180
Result: null,
181181
StoredException: null
182182
);
183183
var storedEffect2 = new StoredEffect(
184184
"EffectId2".ToEffectId(),
185-
"EffectId2".ToStoredEffectId(isState: false),
185+
"EffectId2".ToStoredEffectId(EffectType.Effect),
186186
WorkStatus.Completed,
187187
Result: null,
188188
StoredException: null
@@ -219,14 +219,14 @@ protected async Task TruncateDeletesAllEffects(Task<IEffectsStore> storeTask)
219219

220220
var storedEffect1 = new StoredEffect(
221221
"EffectId1".ToEffectId(),
222-
"EffectId1".ToStoredEffectId(isState: false),
222+
"EffectId1".ToStoredEffectId(EffectType.Effect),
223223
WorkStatus.Started,
224224
Result: null,
225225
StoredException: null
226226
);
227227
var storedEffect2 = new StoredEffect(
228228
"EffectId2".ToEffectId(),
229-
"EffectId2".ToStoredEffectId(isState: false),
229+
"EffectId2".ToStoredEffectId(EffectType.Effect),
230230
WorkStatus.Completed,
231231
Result: null,
232232
StoredException: null
@@ -256,14 +256,14 @@ protected async Task BulkInsertTest(Task<IEffectsStore> storeTask)
256256
var storedId = TestStoredId.Create();
257257
var storedEffect1 = new StoredEffect(
258258
"EffectId1".ToEffectId(),
259-
"EffectId1".ToStoredEffectId(isState: false),
259+
"EffectId1".ToStoredEffectId(EffectType.Effect),
260260
WorkStatus.Started,
261261
Result: "some result 1".ToUtf8Bytes(),
262262
StoredException: null
263263
);
264264
var storedEffect2 = new StoredEffect(
265265
"EffectId2".ToEffectId(),
266-
"EffectId2".ToStoredEffectId(isState: false),
266+
"EffectId2".ToStoredEffectId(EffectType.Effect),
267267
WorkStatus.Completed,
268268
Result: "some result 2".ToUtf8Bytes(),
269269
StoredException: null

Core/Cleipnir.ResilientFunctions.Tests/TestTemplates/RFunctionTests/ControlPanelTests.cs

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1284,7 +1284,7 @@ await store.EffectsStore.SetEffectResult(
12841284
rAction.MapToStoredId(functionId),
12851285
new StoredEffect(
12861286
"SomeId".ToEffectId(),
1287-
"SomeId".ToStoredEffectId(isState: false),
1287+
"SomeId".ToStoredEffectId(EffectType.Effect),
12881288
WorkStatus.Completed,
12891289
Result: "SomeResult".ToJson().ToUtf8Bytes(),
12901290
StoredException: null
@@ -1320,8 +1320,8 @@ protected async Task EffectsAreCachedAfterInitialFetch(Task<IFunctionStore> stor
13201320
await store.EffectsStore.SetEffectResult(
13211321
rAction.MapToStoredId(functionId),
13221322
new StoredEffect(
1323-
new EffectId("SomeId", IsState: false),
1324-
"SomeId".ToStoredEffectId(isState: false),
1323+
"SomeId".ToEffectId(),
1324+
"SomeId".ToStoredEffectId(EffectType.Effect),
13251325
WorkStatus.Completed,
13261326
Result: "SomeResult".ToJson().ToUtf8Bytes(),
13271327
StoredException: null
@@ -1360,7 +1360,7 @@ protected async Task EffectsAreUpdatedAfterRefresh(Task<IFunctionStore> storeTas
13601360

13611361
await secondControlPanel.Refresh();
13621362
await secondControlPanel.Effects.GetValue<string>("Id").ShouldBeAsync("SomeResult");
1363-
await secondControlPanel.Effects.GetStatus("Id".ToEffectId(isState: false)).ShouldBeAsync(WorkStatus.Completed);
1363+
await secondControlPanel.Effects.GetStatus("Id".ToEffectId(EffectType.Effect)).ShouldBeAsync(WorkStatus.Completed);
13641364

13651365
unhandledExceptionCatcher.ShouldNotHaveExceptions();
13661366
}

Core/Cleipnir.ResilientFunctions.Tests/TestTemplates/RFunctionTests/CrashedTests.cs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -128,7 +128,7 @@ await store
128128
storedFunction.ShouldNotBeNull();
129129
storedFunction.Status.ShouldBe(Status.Succeeded);
130130
var effects = await store.EffectsStore.GetEffectResults(registration.MapToStoredId(functionId));
131-
var stateResult = effects.Single(e => e.EffectId == "State".ToEffectId(isState: true)).Result!;
131+
var stateResult = effects.Single(e => e.EffectId == "State".ToEffectId(EffectType.State)).Result!;
132132
stateResult.ShouldNotBeNull();
133133
stateResult.ToStringFromUtf8Bytes().DeserializeFromJsonTo<State>().Value.ShouldBe(1);
134134
await rFunc(flowInstance.Value, param).ShouldBeAsync("TEST");
@@ -252,7 +252,7 @@ await store
252252
storedFunction.ShouldNotBeNull();
253253
storedFunction.Status.ShouldBe(Status.Succeeded);
254254
var effects = await store.EffectsStore.GetEffectResults(registration.MapToStoredId(functionId));
255-
var state = effects.Single(e => e.EffectId == "State".ToEffectId(isState: true)).Result;
255+
var state = effects.Single(e => e.EffectId == "State".ToEffectId(EffectType.State)).Result;
256256
state!.ToStringFromUtf8Bytes().DeserializeFromJsonTo<State>().Value.ShouldBe(1);
257257
await rAction(flowInstance.Value, param);
258258
}

Core/Cleipnir.ResilientFunctions.Tests/TestTemplates/RFunctionTests/EffectTests.cs

Lines changed: 86 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -388,4 +388,90 @@ public async Task ExistingEffectsFuncIsOnlyInvokedAfterGettingValue(Task<IFuncti
388388
await effect.TryGet<int>("Id1");
389389
syncedCounter.Current.ShouldBe(1);
390390
}
391+
392+
public abstract Task SubEffectHasImplicitContext();
393+
public async Task SubEffectHasImplicitContext(Task<IFunctionStore> storeTask)
394+
{
395+
var store = await storeTask;
396+
using var functionsRegistry = new FunctionsRegistry(store);
397+
var flowId = TestFlowId.Create();
398+
var (flowType, flowInstance) = flowId;
399+
var rAction = functionsRegistry.RegisterParamless(
400+
flowType,
401+
async Task (workflow) =>
402+
{
403+
var effect = workflow.Effect;
404+
await effect.Capture(async () =>
405+
{
406+
var e1 = effect.Capture(async () =>
407+
{
408+
await Task.Delay(10);
409+
await effect.Upsert("SubEffectValue1", "some value");
410+
});
411+
await e1;
412+
var e2 = effect.Capture(async () =>
413+
{
414+
await Task.Delay(1);
415+
await effect.Upsert("SubEffectValue2", "some other value");
416+
});
417+
418+
await Task.WhenAll(e1, e2);
419+
});
420+
}
421+
);
422+
423+
await rAction.Invoke(flowInstance.ToString());
424+
425+
var storedId = rAction.MapToStoredId(flowId);
426+
var effectResults = await store.EffectsStore.GetEffectResults(storedId);
427+
428+
var subEffectValue1Id = effectResults.Single(se => se.EffectId.Id == "SubEffectValue1").EffectId;
429+
subEffectValue1Id.Context.ShouldBe("E0.E0");
430+
431+
var subEffectValue2Id = effectResults.Single(se => se.EffectId.Id == "SubEffectValue2").EffectId;
432+
subEffectValue2Id.Context.ShouldBe("E0.E1");
433+
}
434+
435+
public abstract Task SubEffectHasExplicitContext();
436+
public async Task SubEffectHasExplicitContext(Task<IFunctionStore> storeTask)
437+
{
438+
var store = await storeTask;
439+
using var functionsRegistry = new FunctionsRegistry(store);
440+
var flowId = TestFlowId.Create();
441+
var (flowType, flowInstance) = flowId;
442+
var rAction = functionsRegistry.RegisterParamless(
443+
flowType,
444+
async Task (workflow) =>
445+
{
446+
var effect = workflow.Effect;
447+
await effect.Capture("GrandParent", async () =>
448+
{
449+
var e1 = effect.Capture("Mother", async () =>
450+
{
451+
await Task.Delay(10);
452+
await effect.Upsert("SubEffectValue1", "some value");
453+
});
454+
await e1;
455+
var e2 = effect.Capture("Father",async () =>
456+
{
457+
await Task.Delay(1);
458+
await effect.Upsert("SubEffectValue2", "some other value");
459+
});
460+
461+
await Task.WhenAll(e1, e2);
462+
});
463+
}
464+
);
465+
466+
await rAction.Invoke(flowInstance.ToString());
467+
468+
var storedId = rAction.MapToStoredId(flowId);
469+
var effectResults = await store.EffectsStore.GetEffectResults(storedId);
470+
471+
var subEffectValue1Id = effectResults.Single(se => se.EffectId.Id == "SubEffectValue1").EffectId;
472+
subEffectValue1Id.Context.ShouldBe("EGrandParent.EMother");
473+
474+
var subEffectValue2Id = effectResults.Single(se => se.EffectId.Id == "SubEffectValue2").EffectId;
475+
subEffectValue2Id.Context.ShouldBe("EGrandParent.EFather");
476+
}
391477
}

Core/Cleipnir.ResilientFunctions.Tests/TestTemplates/RFunctionTests/PostponedTests.cs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -137,7 +137,7 @@ await BusyWait.Until(
137137
storedFunction.ShouldNotBeNull();
138138

139139
var states = await store.EffectsStore.GetEffectResults(rFunc.MapToStoredId(functionId));
140-
var state = states.Single(e => e.EffectId == "State".ToEffectId(isState: true));
140+
var state = states.Single(e => e.EffectId == "State".ToEffectId(EffectType.State));
141141
state.Result!.ToStringFromUtf8Bytes().DeserializeFromJsonTo<State>().Value.ShouldBe(1);
142142

143143
await rFunc.Invoke(param, param).ShouldBeAsync("TEST");
@@ -244,7 +244,7 @@ await Should.ThrowAsync<InvocationPostponedException>(() =>
244244
storedFunction.ShouldNotBeNull();
245245

246246
var states = await store.EffectsStore.GetEffectResults(rFunc.MapToStoredId(functionId));
247-
var state = states.Single(e => e.EffectId == "State".ToEffectId(isState: true));
247+
var state = states.Single(e => e.EffectId == "State".ToEffectId(EffectType.State));
248248
state.Result!.ToStringFromUtf8Bytes().DeserializeFromJsonTo<State>().Value.ShouldBe(1);
249249

250250
await rFunc.Invoke(flowInstance.Value, param);

0 commit comments

Comments
 (0)