-
Notifications
You must be signed in to change notification settings - Fork 502
Add lambda.DurableFunction blueprint (vs2026) #2445
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: durabletesting2
Are you sure you want to change the base?
Changes from 1 commit
be96fc0
50107b5
3583655
136e31c
6a203ee
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,13 @@ | ||
| { | ||
| "display-name": "Durable Function", | ||
| "system-name": "DurableFunction", | ||
| "description": "A durable execution workflow that checkpoints every step, so it can be suspended during waits and resumed after a crash without re-running completed work.", | ||
| "sort-order": 130, | ||
| "hidden-tags": [ | ||
| "C#", | ||
| "ServerlessProject" | ||
| ], | ||
| "tags": [ | ||
| "Durable" | ||
| ] | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,39 @@ | ||
| { | ||
|
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. intentionally only added to vs2026. didnt feel like it was worth adding to 2024 folder since dotnet8 will be end of life soon |
||
| "author": "AWS", | ||
| "classifications": [ | ||
| "AWS", | ||
| "Lambda", | ||
| "Serverless" | ||
| ], | ||
| "name": "Lambda Durable Function", | ||
| "identity": "AWS.Lambda.Function.Durable.CSharp", | ||
| "groupIdentity": "AWS.Lambda.Function.Durable", | ||
| "shortName": "lambda.DurableFunction", | ||
| "tags": { | ||
| "language": "C#", | ||
| "type": "project" | ||
| }, | ||
| "sourceName": "BlueprintBaseName.1", | ||
| "preferNameDirectory": true, | ||
| "symbols": { | ||
| "profile": { | ||
| "type": "parameter", | ||
| "description": "The AWS credentials profile set in aws-lambda-tools-defaults.json and used as the default profile when interacting with AWS.", | ||
| "datatype": "string", | ||
| "replaces": "DefaultProfile", | ||
| "defaultValue": "" | ||
| }, | ||
| "region": { | ||
| "type": "parameter", | ||
| "description": "The AWS region set in aws-lambda-tools-defaults.json and used as the default region when interacting with AWS.", | ||
| "datatype": "string", | ||
| "replaces": "DefaultRegion", | ||
| "defaultValue": "" | ||
| } | ||
| }, | ||
| "primaryOutputs": [ | ||
| { | ||
| "path": "./BlueprintBaseName.1.csproj" | ||
| } | ||
| ] | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,26 @@ | ||
| <Project Sdk="Microsoft.NET.Sdk"> | ||
| <PropertyGroup> | ||
| <!-- | ||
| Class-library programming model: there is NO OutputType=Exe and NO hand-written Main. | ||
| The Amazon.Lambda.Annotations source generator emits the durable handler wrapper, and the | ||
| managed dotnet10 runtime hosts its own bootstrap and invokes that wrapper via an | ||
| Assembly::Type::Method handler string. Durable execution requires the managed dotnet10 runtime. | ||
| --> | ||
| <OutputType>Library</OutputType> | ||
| <TargetFramework>net10.0</TargetFramework> | ||
| <ImplicitUsings>enable</ImplicitUsings> | ||
| <Nullable>enable</Nullable> | ||
| <GenerateRuntimeConfigurationFiles>true</GenerateRuntimeConfigurationFiles> | ||
| <AWSProjectType>Lambda</AWSProjectType> | ||
| <!-- This property makes the build directory similar to a publish directory and helps the AWS .NET Lambda Mock Test Tool find project dependencies. --> | ||
| <CopyLocalLockFileAssemblies>true</CopyLocalLockFileAssemblies> | ||
| <!-- Generate ready to run images during publishing to improvement cold starts. --> | ||
| <PublishReadyToRun>true</PublishReadyToRun> | ||
| </PropertyGroup> | ||
| <ItemGroup> | ||
| <PackageReference Include="Amazon.Lambda.Core" Version="3.1.1" /> | ||
| <PackageReference Include="Amazon.Lambda.Serialization.SystemTextJson" Version="3.0.0" /> | ||
| <PackageReference Include="Amazon.Lambda.Annotations" Version="2.0.1" /> | ||
| <PackageReference Include="Amazon.Lambda.DurableExecution" Version="0.1.1-preview" /> | ||
|
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. will update to ga version once released |
||
| </ItemGroup> | ||
| </Project> | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,116 @@ | ||
| using Amazon.Lambda.Annotations; | ||
| using Amazon.Lambda.Core; | ||
| using Amazon.Lambda.DurableExecution; | ||
| using Microsoft.Extensions.Logging; | ||
|
|
||
| // Durable execution uses the Lambda Annotations programming model in the CLASS-LIBRARY variant: | ||
| // there is no hand-written Main. The Amazon.Lambda.Annotations source generator turns the | ||
| // [LambdaFunction] + [DurableExecution] method below into a handler wrapper that delegates to | ||
| // Amazon.Lambda.DurableExecution.DurableFunction.WrapAsync. The managed dotnet10 runtime hosts its | ||
| // own bootstrap, resolves the serializer from the assembly attribute here, and invokes the | ||
| // generated wrapper directly. | ||
| [assembly: LambdaSerializer(typeof(Amazon.Lambda.Serialization.SystemTextJson.DefaultLambdaJsonSerializer))] | ||
|
|
||
| namespace BlueprintBaseName._1; | ||
|
|
||
| /// <summary> | ||
| /// A durable order-processing workflow. A single invocation reads like one straight-line method, | ||
| /// but durable execution checkpoints every operation, so the function can be suspended (during the | ||
| /// settlement wait) and re-invoked without re-running completed work. If the process crashes | ||
| /// mid-flight it resumes from the last checkpoint — no lost orders, no double charges. | ||
| /// </summary> | ||
| public class Function | ||
| { | ||
| /// <summary> | ||
| /// The durable workflow entry point. The method signature is | ||
| /// <c>(TInput, IDurableContext) -> Task<TOutput></c>; the source generator wires it to the | ||
| /// durable runtime and emits the matching CloudFormation resource (with DurableConfig and the | ||
| /// durable IAM policy) in serverless.template. | ||
| /// </summary> | ||
| [LambdaFunction] | ||
| [DurableExecution] | ||
| public async Task<OrderResult> ProcessOrder(OrderRequest order, IDurableContext context) | ||
| { | ||
| // The durable logger is replay-aware: this line is emitted once, not once per replay. | ||
| context.Logger.LogInformation("Processing order {OrderId}", order.OrderId); | ||
|
|
||
| // 1) VALIDATE — a plain step. The result is checkpointed; on replay the cached value is | ||
| // returned instead of re-running the body. | ||
| var itemCount = await context.StepAsync( | ||
| async (_, _) => | ||
| { | ||
| await Task.CompletedTask; | ||
| if (order.Items is null || order.Items.Length == 0) | ||
| throw new InvalidOperationException("Order has no items."); | ||
| return order.Items.Length; | ||
| }, | ||
| name: "validate_order"); | ||
|
|
||
| // 2) CHARGE PAYMENT — a step with a retry policy. Payment gateways are flaky, so the SDK | ||
| // transparently retries with exponential backoff and checkpoints only the successful | ||
| // attempt. AtMostOncePerRetry avoids double-charging if Lambda is re-invoked mid-attempt. | ||
| var transactionId = await context.StepAsync( | ||
| async (_, _) => | ||
| { | ||
| await Task.CompletedTask; | ||
| return $"txn-{order.OrderId}"; | ||
| }, | ||
| name: "charge_payment", | ||
| config: new StepConfig | ||
| { | ||
| RetryStrategy = RetryStrategy.Exponential( | ||
| maxAttempts: 5, | ||
| initialDelay: TimeSpan.FromSeconds(2), | ||
| maxDelay: TimeSpan.FromSeconds(30), | ||
| backoffRate: 2.0), | ||
| Semantics = StepSemantics.AtMostOncePerRetry, | ||
| }); | ||
|
|
||
| // 3) SETTLEMENT WAIT — suspend the workflow for a fixed delay. While suspended there is no | ||
| // compute charge; the runtime re-invokes the function when the timer fires. | ||
| await context.WaitAsync(TimeSpan.FromSeconds(5), name: "settlement_delay"); | ||
|
|
||
| // 4) SHIP — group related steps into a single child context. The packing and labeling steps | ||
| // are checkpointed together as one logical operation. | ||
| var trackingId = await context.RunInChildContextAsync( | ||
| async (childContext, _) => | ||
| { | ||
| await childContext.StepAsync( | ||
| async (_, _) => { await Task.CompletedTask; return "packed"; }, | ||
| name: "pack"); | ||
|
|
||
| return await childContext.StepAsync( | ||
| async (_, _) => { await Task.CompletedTask; return $"trk-{order.OrderId}"; }, | ||
| name: "label"); | ||
| }, | ||
| name: "ship_order"); | ||
|
|
||
| context.Logger.LogInformation("Order {OrderId} shipped: {TrackingId}", order.OrderId, trackingId); | ||
|
|
||
| return new OrderResult | ||
| { | ||
| OrderId = order.OrderId, | ||
| Status = "shipped", | ||
| ItemCount = itemCount, | ||
| TransactionId = transactionId, | ||
| TrackingId = trackingId, | ||
| }; | ||
| } | ||
| } | ||
|
|
||
| /// <summary>Input payload for the workflow.</summary> | ||
| public class OrderRequest | ||
| { | ||
| public string? OrderId { get; set; } | ||
| public string[]? Items { get; set; } | ||
| } | ||
|
|
||
| /// <summary>Output payload returned when the workflow completes.</summary> | ||
| public class OrderResult | ||
| { | ||
| public string? OrderId { get; set; } | ||
| public string? Status { get; set; } | ||
| public int ItemCount { get; set; } | ||
| public string? TransactionId { get; set; } | ||
| public string? TrackingId { get; set; } | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,71 @@ | ||
| # Durable Lambda Function | ||
|
|
||
| This project contains a Lambda **durable execution** workflow built with the | ||
| [Lambda Annotations](https://github.com/aws/aws-lambda-dotnet/tree/master/Libraries/src/Amazon.Lambda.Annotations) | ||
| programming model. | ||
|
|
||
| Durable execution lets you write a multi-step workflow as a single straight-line method. The | ||
| runtime checkpoints every operation, so the function can be **suspended** during waits and | ||
| **resumed after a crash** without re-running completed work. | ||
|
|
||
| ## How it works | ||
|
|
||
| `Function.ProcessOrder` is the workflow entry point. It is marked with two attributes: | ||
|
|
||
| * `[LambdaFunction]` — registers the method with the Annotations source generator. | ||
| * `[DurableExecution]` — tells the generator to wrap the method with the durable runtime and to add | ||
| the durable configuration and IAM policy to `serverless.template`. | ||
|
|
||
| The workflow uses the core durable primitives on `IDurableContext`: | ||
|
|
||
| | Primitive | Used for | | ||
| |-----------|----------| | ||
| | `StepAsync` | A checkpointed unit of work. On replay the cached result is returned instead of re-running the body. | | ||
| | `StepAsync` + `StepConfig.RetryStrategy` | Retry a flaky step with exponential backoff; only the successful attempt is checkpointed. | | ||
| | `StepSemantics.AtMostOncePerRetry` | Avoid re-running a side-effecting step (e.g. charging a card) if Lambda is re-invoked mid-attempt. | | ||
| | `WaitAsync` | Suspend the workflow for a delay. There is no compute charge while suspended. | | ||
| | `RunInChildContextAsync` | Group related steps into a single logical operation. | | ||
|
|
||
| The class-library model is used (no `Main`): the managed `dotnet10` runtime hosts the bootstrap and | ||
| invokes the generated handler wrapper directly. | ||
|
|
||
| > **Note:** Durable execution requires the managed **`dotnet10`** runtime. | ||
|
|
||
| ## Requirements | ||
|
|
||
| * [.NET 10 SDK](https://dotnet.microsoft.com/download) | ||
| * [Amazon.Lambda.Tools](https://github.com/aws/aws-extensions-for-dotnet-cli#aws-lambda-amazonlambdatools) | ||
|
|
||
| ```bash | ||
| dotnet tool install -g Amazon.Lambda.Tools | ||
| ``` | ||
|
|
||
| ## Deploy | ||
|
|
||
| The `[DurableExecution]` attribute drives the source generator, which keeps the function resource in | ||
| `serverless.template` in sync (runtime, handler, `DurableConfig`, and the | ||
| `AWSLambdaBasicDurableExecutionRolePolicy` managed policy). Deploy the serverless application with: | ||
|
|
||
| ```bash | ||
| dotnet lambda deploy-serverless | ||
| ``` | ||
|
|
||
| ## Invoke | ||
|
|
||
| After deploying, invoke the function with a sample order payload: | ||
|
|
||
| ```bash | ||
| dotnet lambda invoke-function BlueprintBaseName.1 --payload '{"OrderId":"order-123","Items":["sku-1","sku-2"]}' | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Can you add the |
||
| ``` | ||
|
|
||
| The workflow validates the order, charges payment, waits out a short settlement period, ships the | ||
| order in a child context, and returns the result. | ||
|
|
||
| ## Test | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Don't have this section since the test project will only exist if the create the project via Visual Studio. You could have a more general section about how to test and and pointing out the |
||
|
|
||
| The included test project drives the workflow locally with the | ||
| `Amazon.Lambda.DurableExecution.Testing` runner — no AWS resources required: | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I would be careful calling it a |
||
|
|
||
| ```bash | ||
| dotnet test | ||
| ``` | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,14 @@ | ||
| { | ||
| "Information": [ | ||
| "This file provides default values for the deployment wizard inside Visual Studio and the AWS Lambda commands added to the .NET Core CLI.", | ||
| "To learn more about the Lambda commands with the .NET Core CLI execute the following command at the command line in the project root directory.", | ||
| "dotnet lambda help", | ||
| "All the command line options for the Lambda command can be specified in this file." | ||
| ], | ||
| "profile": "DefaultProfile", | ||
| "region": "DefaultRegion", | ||
| "configuration": "Release", | ||
| "s3-prefix": "BlueprintBaseName.1/", | ||
| "template": "serverless.template", | ||
| "template-parameters": "" | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,28 @@ | ||
| { | ||
| "AWSTemplateFormatVersion": "2010-09-09", | ||
| "Transform": "AWS::Serverless-2016-10-31", | ||
| "Description": "An AWS Serverless Application with a durable execution function. This template is partially managed by Amazon.Lambda.Annotations.", | ||
| "Resources": { | ||
| "BlueprintBaseName1FunctionProcessOrderGenerated": { | ||
| "Type": "AWS::Serverless::Function", | ||
| "Metadata": { | ||
| "Tool": "Amazon.Lambda.Annotations", | ||
| "SyncedDurableConfig": true, | ||
| "SyncedDurablePolicy": true | ||
| }, | ||
| "Properties": { | ||
| "Runtime": "dotnet10", | ||
| "CodeUri": ".", | ||
| "MemorySize": 512, | ||
| "Timeout": 30, | ||
| "Policies": [ | ||
| "AWSLambdaBasicExecutionRole", | ||
| "arn:aws:iam::aws:policy/service-role/AWSLambdaBasicDurableExecutionRolePolicy" | ||
| ], | ||
| "PackageType": "Zip", | ||
| "Handler": "BlueprintBaseName.1::BlueprintBaseName._1.Function_ProcessOrder_Generated::ProcessOrder", | ||
| "DurableConfig": {} | ||
| } | ||
| } | ||
| } | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,19 @@ | ||
| <Project Sdk="Microsoft.NET.Sdk"> | ||
| <PropertyGroup> | ||
| <TargetFramework>net10.0</TargetFramework> | ||
| <ImplicitUsings>enable</ImplicitUsings> | ||
| <Nullable>enable</Nullable> | ||
| <IsTestProject>true</IsTestProject> | ||
| </PropertyGroup> | ||
| <ItemGroup> | ||
| <PackageReference Include="Amazon.Lambda.Core" Version="3.1.1" /> | ||
| <PackageReference Include="Amazon.Lambda.TestUtilities" Version="4.1.0" /> | ||
| <PackageReference Include="Amazon.Lambda.DurableExecution.Testing" Version="0.0.1-preview" /> | ||
|
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. since this pr isnt merged yet, i tested with local nuget feed. |
||
| <PackageReference Include="Microsoft.NET.Test.Sdk" Version="18.0.1" /> | ||
| <PackageReference Include="xunit.v3" Version="3.2.2" /> | ||
| <PackageReference Include="xunit.runner.visualstudio" Version="3.1.5" /> | ||
| </ItemGroup> | ||
| <ItemGroup> | ||
| <ProjectReference Include="..\..\src\BlueprintBaseName.1\BlueprintBaseName.1.csproj" /> | ||
| </ItemGroup> | ||
| </Project> | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,56 @@ | ||
| using Amazon.Lambda.DurableExecution.Testing; | ||
| using Xunit; | ||
|
|
||
| namespace BlueprintBaseName._1.Tests; | ||
|
|
||
| public class FunctionTest | ||
| { | ||
| [Fact] | ||
| public async Task ProcessOrder_ShipsOrder() | ||
| { | ||
| var function = new Function(); | ||
|
|
||
| // The local runner drives the workflow to completion in-process using the real durable | ||
| // runtime with an in-memory backend. SkipTime collapses the settlement WaitAsync delay so | ||
| // the test does not actually block for 5 seconds. | ||
| await using var runner = new DurableTestRunner<OrderRequest, OrderResult>( | ||
| handler: function.ProcessOrder, | ||
| options: new TestRunnerOptions { SkipTime = true }); | ||
|
|
||
| var input = new OrderRequest | ||
| { | ||
| OrderId = "order-123", | ||
| Items = new[] { "sku-1", "sku-2" }, | ||
| }; | ||
|
|
||
| var result = await runner.RunAsync(input, cancellationToken: TestContext.Current.CancellationToken); | ||
|
|
||
| result.EnsureSucceeded(); | ||
| Assert.NotNull(result.Result); | ||
| Assert.Equal("order-123", result.Result!.OrderId); | ||
| Assert.Equal("shipped", result.Result.Status); | ||
| Assert.Equal(2, result.Result.ItemCount); | ||
| Assert.Equal("txn-order-123", result.Result.TransactionId); | ||
| Assert.Equal("trk-order-123", result.Result.TrackingId); | ||
|
|
||
| // Each named operation is checkpointed and inspectable. | ||
| Assert.Equal(OperationStatus.Succeeded, result.GetStep("validate_order").Status); | ||
| Assert.Equal(OperationStatus.Succeeded, result.GetStep("charge_payment").Status); | ||
| } | ||
|
|
||
| [Fact] | ||
| public async Task ProcessOrder_EmptyOrder_Fails() | ||
| { | ||
| var function = new Function(); | ||
|
|
||
| await using var runner = new DurableTestRunner<OrderRequest, OrderResult>( | ||
| handler: function.ProcessOrder, | ||
| options: new TestRunnerOptions { SkipTime = true }); | ||
|
|
||
| var input = new OrderRequest { OrderId = "order-456", Items = System.Array.Empty<string>() }; | ||
|
|
||
| var result = await runner.RunAsync(input, cancellationToken: TestContext.Current.CancellationToken); | ||
|
|
||
| Assert.True(result.IsFailed); | ||
| } | ||
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This controls the order the blueprint shows up in the VS blueprint dialog
130 would basically put this at the bottom where users would have to scroll. Looking at the other sort orders if you set this to
124it would be next to the PowerTools template and before all of the "Simple" templates.