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
5 changes: 4 additions & 1 deletion .autover/autover.json
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,10 @@
},
{
"Name": "Amazon.Lambda.DurableExecution",
"Path": "Libraries/src/Amazon.Lambda.DurableExecution/Amazon.Lambda.DurableExecution.csproj",
"Paths": [
"Libraries/src/Amazon.Lambda.DurableExecution/Amazon.Lambda.DurableExecution.csproj",
"Libraries/src/Amazon.Lambda.DurableExecution.Analyzers/Amazon.Lambda.DurableExecution.Analyzers.csproj"
],
"PrereleaseLabel": "preview"
},
{
Expand Down
11 changes: 11 additions & 0 deletions .autover/changes/durable-execution-analyzers.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
{
"Projects": [
{
"Name": "Amazon.Lambda.DurableExecution",
"Type": "Minor",
"ChangelogMessages": [
"Add Roslyn analyzers (DE001-DE004) that catch common durable-execution authoring mistakes at build time, bundled in the package so they activate automatically for consumers. DE001 (Warning) flags non-deterministic APIs (DateTime.Now, Guid.NewGuid(), Random, Stopwatch, Environment.TickCount, crypto RNG) used in workflow code outside a step. DE002 (Warning) flags a durable operation invoked inside a step body via the captured outer IDurableContext. DE003 (Warning) flags mutation of a captured outer-scope variable inside a durable-operation delegate. DE004 (Info) suggests ParallelAsync/MapAsync over Task.WhenAll/Task.WhenAny for durable tasks. DE001 and DE004 include code fixes. Preview."
]
}
]
}
30 changes: 30 additions & 0 deletions Libraries/Libraries.sln
Original file line number Diff line number Diff line change
Expand Up @@ -165,6 +165,10 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Amazon.Lambda.DurableExecut
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "AnnotationsClassLibraryFunction", "test\Amazon.Lambda.DurableExecution.IntegrationTests\TestFunctions\AnnotationsClassLibraryFunction\AnnotationsClassLibraryFunction.csproj", "{D55E2D57-8374-4573-999B-6E64E109C25F}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Amazon.Lambda.DurableExecution.Analyzers", "src\Amazon.Lambda.DurableExecution.Analyzers\Amazon.Lambda.DurableExecution.Analyzers.csproj", "{6702F1BE-3A11-4DFB-93C3-50065789D814}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Amazon.Lambda.DurableExecution.Analyzers.Tests", "test\Amazon.Lambda.DurableExecution.Analyzers.Tests\Amazon.Lambda.DurableExecution.Analyzers.Tests.csproj", "{592F5A22-B03A-4E0B-9DCD-093FBF29ACFC}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
Expand Down Expand Up @@ -1039,6 +1043,30 @@ Global
{D55E2D57-8374-4573-999B-6E64E109C25F}.Release|x64.Build.0 = Release|Any CPU
{D55E2D57-8374-4573-999B-6E64E109C25F}.Release|x86.ActiveCfg = Release|Any CPU
{D55E2D57-8374-4573-999B-6E64E109C25F}.Release|x86.Build.0 = Release|Any CPU
{6702F1BE-3A11-4DFB-93C3-50065789D814}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{6702F1BE-3A11-4DFB-93C3-50065789D814}.Debug|Any CPU.Build.0 = Debug|Any CPU
{6702F1BE-3A11-4DFB-93C3-50065789D814}.Debug|x64.ActiveCfg = Debug|Any CPU
{6702F1BE-3A11-4DFB-93C3-50065789D814}.Debug|x64.Build.0 = Debug|Any CPU
{6702F1BE-3A11-4DFB-93C3-50065789D814}.Debug|x86.ActiveCfg = Debug|Any CPU
{6702F1BE-3A11-4DFB-93C3-50065789D814}.Debug|x86.Build.0 = Debug|Any CPU
{6702F1BE-3A11-4DFB-93C3-50065789D814}.Release|Any CPU.ActiveCfg = Release|Any CPU
{6702F1BE-3A11-4DFB-93C3-50065789D814}.Release|Any CPU.Build.0 = Release|Any CPU
{6702F1BE-3A11-4DFB-93C3-50065789D814}.Release|x64.ActiveCfg = Release|Any CPU
{6702F1BE-3A11-4DFB-93C3-50065789D814}.Release|x64.Build.0 = Release|Any CPU
{6702F1BE-3A11-4DFB-93C3-50065789D814}.Release|x86.ActiveCfg = Release|Any CPU
{6702F1BE-3A11-4DFB-93C3-50065789D814}.Release|x86.Build.0 = Release|Any CPU
{592F5A22-B03A-4E0B-9DCD-093FBF29ACFC}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{592F5A22-B03A-4E0B-9DCD-093FBF29ACFC}.Debug|Any CPU.Build.0 = Debug|Any CPU
{592F5A22-B03A-4E0B-9DCD-093FBF29ACFC}.Debug|x64.ActiveCfg = Debug|Any CPU
{592F5A22-B03A-4E0B-9DCD-093FBF29ACFC}.Debug|x64.Build.0 = Debug|Any CPU
{592F5A22-B03A-4E0B-9DCD-093FBF29ACFC}.Debug|x86.ActiveCfg = Debug|Any CPU
{592F5A22-B03A-4E0B-9DCD-093FBF29ACFC}.Debug|x86.Build.0 = Debug|Any CPU
{592F5A22-B03A-4E0B-9DCD-093FBF29ACFC}.Release|Any CPU.ActiveCfg = Release|Any CPU
{592F5A22-B03A-4E0B-9DCD-093FBF29ACFC}.Release|Any CPU.Build.0 = Release|Any CPU
{592F5A22-B03A-4E0B-9DCD-093FBF29ACFC}.Release|x64.ActiveCfg = Release|Any CPU
{592F5A22-B03A-4E0B-9DCD-093FBF29ACFC}.Release|x64.Build.0 = Release|Any CPU
{592F5A22-B03A-4E0B-9DCD-093FBF29ACFC}.Release|x86.ActiveCfg = Release|Any CPU
{592F5A22-B03A-4E0B-9DCD-093FBF29ACFC}.Release|x86.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
Expand Down Expand Up @@ -1120,6 +1148,8 @@ Global
{CA132CAB-FF4F-4312-B3A3-66DE9D360F27} = {1DE4EE60-45BA-4EF7-BE00-B9EB861E4C69}
{16B1B1CC-3AFC-4DC7-8DB6-D14AE12924A2} = {1DE4EE60-45BA-4EF7-BE00-B9EB861E4C69}
{D55E2D57-8374-4573-999B-6E64E109C25F} = {1DE4EE60-45BA-4EF7-BE00-B9EB861E4C69}
{6702F1BE-3A11-4DFB-93C3-50065789D814} = {AAB54E74-20B1-42ED-BC3D-CE9F7BC7FD12}
{592F5A22-B03A-4E0B-9DCD-093FBF29ACFC} = {1DE4EE60-45BA-4EF7-BE00-B9EB861E4C69}
EndGlobalSection
GlobalSection(ExtensibilityGlobals) = postSolution
SolutionGuid = {503678A4-B8D1-4486-8915-405A3E9CF0EB}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
<Project Sdk="Microsoft.NET.Sdk">

<PropertyGroup>
<!-- netstandard2.0 is required for a Roslyn analyzer/code-fix assembly. -->
<TargetFramework>netstandard2.0</TargetFramework>
<LangVersion>latest</LangVersion>
<Nullable>enable</Nullable>
<ImplicitUsings>disable</ImplicitUsings>

<Description>Roslyn analyzers and code fixes for Amazon.Lambda.DurableExecution — catches durable-execution determinism and authoring mistakes (DE001-DE004) at build time.</Description>
<AssemblyTitle>Amazon.Lambda.DurableExecution.Analyzers</AssemblyTitle>
<AssemblyName>Amazon.Lambda.DurableExecution.Analyzers</AssemblyName>
<PackageTags>AWS;Amazon;Lambda;Durable;Workflow;Analyzer;Roslyn</PackageTags>

<!-- This assembly is bundled INSIDE the Amazon.Lambda.DurableExecution package
(analyzers/dotnet/cs), so it is never packed on its own. -->
<IsPackable>false</IsPackable>
<IncludeBuildOutput>false</IncludeBuildOutput>
<EnforceExtendedAnalyzerRules>true</EnforceExtendedAnalyzerRules>

<!-- Strong-named so it sits alongside the rest of the repo's signed assemblies. -->
<AssemblyOriginatorKeyFile>..\..\..\buildtools\public.snk</AssemblyOriginatorKeyFile>
<SignAssembly>true</SignAssembly>

<TreatWarningsAsErrors>true</TreatWarningsAsErrors>
<GenerateDocumentationFile>true</GenerateDocumentationFile>

<!-- The repo precedent (Amazon.Lambda.Annotations.SourceGenerator) does not localize
diagnostic strings; match it. Suppress the localization-nag analyzers. -->
<NoWarn>$(NoWarn);RS1032;RS1033</NoWarn>
</PropertyGroup>

<!-- Pinned to 4.0.1 to match Amazon.Lambda.Annotations.SourceGenerator and the
Microsoft.CodeAnalysis.*.Testing 1.1.2 harness used by the test project. Keeps the analyzer's
minimum Roslyn floor low (broad VS / SDK compatibility) and avoids version-conflict load errors. -->
<ItemGroup>
<PackageReference Include="Microsoft.CodeAnalysis.CSharp" Version="4.0.1" PrivateAssets="all" />
<PackageReference Include="Microsoft.CodeAnalysis.CSharp.Workspaces" Version="4.0.1" PrivateAssets="all" />
</ItemGroup>

<!-- Release-tracking files required by the Microsoft.CodeAnalysis release-tracking
analyzer (RS2007/RS2008) so every shipped diagnostic id is accounted for. -->
<ItemGroup>
<AdditionalFiles Include="AnalyzerReleases.Shipped.md" />
<AdditionalFiles Include="AnalyzerReleases.Unshipped.md" />
</ItemGroup>

</Project>
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
; Shipped analyzer releases
; https://github.com/dotnet/roslyn-analyzers/blob/main/src/Microsoft.CodeAnalysis.Analyzers/ReleaseTrackingAnalyzers.Help.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
; Unshipped analyzer release
; https://github.com/dotnet/roslyn-analyzers/blob/main/src/Microsoft.CodeAnalysis.Analyzers/ReleaseTrackingAnalyzers.Help.md

### New Rules

Rule ID | Category | Severity | Notes
--------|----------|----------|----------------------------------------------------------------------
DE001 | AWSLambdaDurableExecution | Warning | Non-deterministic call outside a step. https://github.com/aws/aws-lambda-dotnet/blob/master/Libraries/src/Amazon.Lambda.DurableExecution/docs/analyzers.md#de001
DE002 | AWSLambdaDurableExecution | Warning | Nested durable operation inside a step body. https://github.com/aws/aws-lambda-dotnet/blob/master/Libraries/src/Amazon.Lambda.DurableExecution/docs/analyzers.md#de002
DE003 | AWSLambdaDurableExecution | Warning | Mutable variable captured and modified inside a durable operation. https://github.com/aws/aws-lambda-dotnet/blob/master/Libraries/src/Amazon.Lambda.DurableExecution/docs/analyzers.md#de003
DE004 | AWSLambdaDurableExecution | Info | Task.WhenAll/WhenAny over durable tasks; prefer ParallelAsync/MapAsync. https://github.com/aws/aws-lambda-dotnet/blob/master/Libraries/src/Amazon.Lambda.DurableExecution/docs/analyzers.md#de004
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
// SPDX-License-Identifier: Apache-2.0

using Microsoft.CodeAnalysis;

namespace Amazon.Lambda.DurableExecution.Analyzers
{
/// <summary>
/// Diagnostic descriptors for the durable-execution analyzers. These mirror the
/// JavaScript SDK's ESLint plugin rules (no-non-deterministic-outside-step,
/// no-nested-durable-operations, no-closure-in-durable-operations) and add a
/// .NET-specific rule (DE004) for the <c>Task.WhenAll</c>/<c>WhenAny</c> pattern.
/// </summary>
public static class DurableDiagnostics
{
internal const string Category = "AWSLambdaDurableExecution";

private const string HelpRoot =
"https://github.com/aws/aws-lambda-dotnet/blob/master/Libraries/src/Amazon.Lambda.DurableExecution/docs/analyzers.md";

/// <summary>
/// DE001 — a non-deterministic API (DateTime.Now, Guid.NewGuid, Random, …) is used in
/// workflow code outside a step. On replay the workflow re-runs from the top, so the value
/// differs between the original run and replays, corrupting checkpoint-derived state.
/// </summary>
public static readonly DiagnosticDescriptor NonDeterministicCallOutsideStep = new DiagnosticDescriptor(
id: "DE001",
title: "Non-deterministic call outside a step",
messageFormat: "Non-deterministic operation '{0}' is used in workflow code outside a step. Move it inside a step (e.g. context.StepAsync(...)) so its result is checkpointed and replays consistently",
category: Category,
defaultSeverity: DiagnosticSeverity.Warning,
isEnabledByDefault: true,
description: "Durable workflow code re-executes from the top on every invocation. Values from non-deterministic APIs differ between the original execution and replays unless they are captured inside a step.",
helpLinkUri: HelpRoot + "#de001");

/// <summary>
/// DE002 — a durable operation is invoked inside a step body by capturing the outer
/// <c>IDurableContext</c>. Step bodies must contain only plain, deterministic code;
/// nesting durable operations requires RunInChildContextAsync.
/// </summary>
public static readonly DiagnosticDescriptor NestedDurableOperationInsideStep = new DiagnosticDescriptor(
id: "DE002",
title: "Nested durable operation inside a step body",
messageFormat: "Durable operation '{0}' is called on the outer durable context '{1}' inside a {2} body. Step bodies must contain only plain, deterministic code; nest durable operations with context.RunInChildContextAsync(...) instead",
category: Category,
defaultSeverity: DiagnosticSeverity.Warning,
isEnabledByDefault: true,
description: "A step body is replayed verbatim on every retry. Calling another durable operation from inside it produces unpredictable behavior; use RunInChildContextAsync to group durable operations.",
helpLinkUri: HelpRoot + "#de002");

/// <summary>
/// DE003 — a variable captured from an outer scope is mutated inside a durable-operation
/// delegate. On replay the operation returns its cached result without re-executing the body,
/// so the write never happens and the captured variable holds stale state.
/// </summary>
public static readonly DiagnosticDescriptor MutableCaptureInDurableOperation = new DiagnosticDescriptor(
id: "DE003",
title: "Mutable variable captured and modified inside a durable operation",
messageFormat: "Variable '{0}' is captured from an outer scope and modified inside a durable operation. On replay the operation returns its cached result without re-executing the body, so this write is lost and '{0}' becomes stale. Return the value from the operation and assign it (e.g. {0} = await context.StepAsync(...)) instead",
category: Category,
defaultSeverity: DiagnosticSeverity.Warning,
isEnabledByDefault: true,
description: "Reading a captured variable inside a durable operation is safe; mutating it is not, because the body is skipped on replay.",
helpLinkUri: HelpRoot + "#de003");

/// <summary>
/// DE004 — <c>Task.WhenAll</c>/<c>Task.WhenAny</c> is called with tasks produced by durable
/// operations. This is not incorrect (operation IDs are allocated deterministically), but it
/// bypasses completion policies, concurrency limits, branch naming, and IBatchResult output.
/// Advisory (Info) only.
/// </summary>
public static readonly DiagnosticDescriptor DurableTaskInTaskCombinator = new DiagnosticDescriptor(
id: "DE004",
title: "Prefer ParallelAsync/MapAsync over Task.WhenAll/WhenAny for durable tasks",
messageFormat: "'{0}' over durable tasks bypasses completion policies, concurrency limits, branch naming, and IBatchResult output. Use context.ParallelAsync (or MapAsync) so concurrent durable operations get framework coordination and complete execution traces",
category: Category,
defaultSeverity: DiagnosticSeverity.Info,
isEnabledByDefault: true,
description: "Task.WhenAll/WhenAny work correctly with durable tasks, but ParallelAsync/MapAsync are preferred for completion policies, concurrency control, and observability.",
helpLinkUri: HelpRoot + "#de004");
}
}
Loading