Skip to content
Merged
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
11 changes: 11 additions & 0 deletions .autover/changes/durable-childcontext-virtual.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
{
"Projects": [
{
"Name": "Amazon.Lambda.DurableExecution",
"Type": "Minor",
"ChangelogMessages": [
"Add virtual-context support to standalone RunInChildContextAsync via ChildContextConfig.NestingType. Setting NestingType.Flat runs the child in a virtual context that emits no CONTEXT checkpoint of its own (mirroring MapConfig/ParallelConfig), enabling manual fan-out with Task.WhenAll while reducing checkpoint volume."
]
}
]
}
Original file line number Diff line number Diff line change
Expand Up @@ -32,4 +32,26 @@ public sealed class ChildContextConfig
/// <see cref="ChildContextException"/> is mapped before being thrown).
/// </remarks>
public Func<Exception, Exception>? ErrorMapping { get; set; }

/// <summary>
/// How the child context is represented in the checkpoint graph. Defaults to
/// <see cref="NestingType.Nested"/>.
/// </summary>
/// <remarks>
/// <para>
/// Under <see cref="NestingType.Flat"/> the child runs in a virtual context
/// that emits no <c>CONTEXT</c> checkpoint of its own — reducing checkpoint
/// volume at the cost of less granular execution traces. Operations inside
/// the child (steps, waits, callbacks) still checkpoint, re-parented to this
/// context's parent.
/// </para>
/// <para>
/// This mirrors the <c>NestingType.Flat</c> option on
/// <see cref="MapConfig"/> and <see cref="ParallelConfig"/>, letting a
/// standalone <see cref="IDurableContext.RunInChildContextAsync{T}(System.Func{IDurableContext, System.Threading.CancellationToken, System.Threading.Tasks.Task{T}}, string?, ChildContextConfig?, System.Threading.CancellationToken)"/>
/// call opt into virtual contexts — for example when manually fanning out
/// work and awaiting the returned tasks with <c>Task.WhenAll</c>.
/// </para>
/// </remarks>
public NestingType NestingType { get; set; } = NestingType.Nested;
}
Original file line number Diff line number Diff line change
Expand Up @@ -167,7 +167,8 @@ private Task<T> RunChildContext<T>(

var op = new ChildContextOperation<T>(
operationId, name, _idGenerator.ParentId, func, config, serializer, MakeChildFactory(),
_state, _terminationManager, _workflowCancellation, _durableExecutionArn, _batcher);
_state, _terminationManager, _workflowCancellation, _durableExecutionArn, _batcher,
isVirtual: config?.NestingType == NestingType.Flat);
return op.ExecuteAsync(cancellationToken);
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -610,4 +610,107 @@ await context.RunInChildContextAsync<string>(
}
}

[Fact]
public async Task RunInChildContextAsync_FlatNesting_EmitsNoContextCheckpointAndReparentsInnerOps()
{
var (context, recorder, tm, _) = CreateContext();

var executed = false;
var result = await context.RunInChildContextAsync(
async (childCtx, _) =>
{
executed = true;
return await childCtx.StepAsync(async (_, _) => { await Task.CompletedTask; return "inner"; }, name: "inner_step");
},
name: "phase",
config: new ChildContextConfig { NestingType = NestingType.Flat });

Assert.True(executed);
Assert.Equal("inner", result);
Assert.False(tm.IsTerminated);

await recorder.Batcher.DrainAsync();

// A virtual (Flat) child emits no CONTEXT checkpoint of its own — only
// the inner step's operations are recorded.
var actions = recorder.Flushed.Select(o => $"{o.Type}:{o.Action}").ToArray();
Assert.Equal(new[]
{
"STEP:START",
"STEP:SUCCEED"
}, actions);
Assert.DoesNotContain(recorder.Flushed, o => o.Type == "CONTEXT");

// Inner-op IDs still derive from the child's own operation ID (so sibling
// branches never collide), but they re-parent to the nearest non-virtual
// ancestor — here the root, whose ParentId is null — since the virtual
// branch has no CONTEXT checkpoint for them to reference.
var parentOpId = IdAt(1);
var innerStepId = ChildIdAt(parentOpId, 1);
var stepStart = recorder.Flushed.Single(o => o.Type == "STEP" && o.Action == "START");
Assert.Equal(innerStepId, stepStart.Id);
Assert.Null(stepStart.ParentId);
}

[Fact]
public async Task RunInChildContextAsync_FlatNesting_ReplayReExecutesBodyFromInnerCheckpoint()
{
// No CONTEXT checkpoint exists for a virtual child, so on replay the body
// re-executes; the inner step replays from its own SUCCEEDED checkpoint
// without re-running the step delegate.
var parentOpId = IdAt(1);
var innerStepId = ChildIdAt(parentOpId, 1);
var (context, recorder, _, _) = CreateContext(new InitialExecutionState
{
Operations = new List<Operation>
{
new()
{
Id = innerStepId,
Type = OperationTypes.Step,
Status = OperationStatuses.Succeeded,
Name = "inner_step",
StepDetails = new StepDetails { Result = "\"cached\"" }
}
}
});

var stepRan = false;
var result = await context.RunInChildContextAsync(
async (childCtx, _) =>
await childCtx.StepAsync(async (_, _) =>
{
stepRan = true;
await Task.CompletedTask;
return "fresh";
}, name: "inner_step"),
name: "phase",
config: new ChildContextConfig { NestingType = NestingType.Flat });

Assert.False(stepRan);
Assert.Equal("cached", result);

await recorder.Batcher.DrainAsync();
Assert.DoesNotContain(recorder.Flushed, o => o.Type == "CONTEXT");
}

[Fact]
public async Task RunInChildContextAsync_FlatNesting_FuncThrows_EmitsNoFailCheckpointButPropagates()
{
var (context, recorder, _, _) = CreateContext();

var ex = await Assert.ThrowsAsync<ChildContextException>(() =>
context.RunInChildContextAsync<string>(
(_, _) => throw new InvalidOperationException("boom"),
name: "phase",
config: new ChildContextConfig { NestingType = NestingType.Flat }));

Assert.Equal("boom", ex.Message);

await recorder.Batcher.DrainAsync();
// Virtual child suppresses the CONTEXT FAIL checkpoint; the exception
// still propagates to the caller.
Assert.DoesNotContain(recorder.Flushed, o => o.Type == "CONTEXT");
}

}
Loading