diff --git a/.github/workflows/nuget-package.yml b/.github/workflows/nuget-package.yml
index c08c88fd..8ecfc2f3 100644
--- a/.github/workflows/nuget-package.yml
+++ b/.github/workflows/nuget-package.yml
@@ -171,6 +171,10 @@ jobs:
src/Temporalio/bin/Release/*.snupkg
src/Temporalio.Extensions.DiagnosticSource/bin/Release/*.nupkg
src/Temporalio.Extensions.DiagnosticSource/bin/Release/*.snupkg
+ src/Temporalio.Extensions.Aws.Lambda/bin/Release/*.nupkg
+ src/Temporalio.Extensions.Aws.Lambda/bin/Release/*.snupkg
+ src/Temporalio.Extensions.Aws.Lambda.OpenTelemetry/bin/Release/*.nupkg
+ src/Temporalio.Extensions.Aws.Lambda.OpenTelemetry/bin/Release/*.snupkg
src/Temporalio.Extensions.Hosting/bin/Release/*.nupkg
src/Temporalio.Extensions.Hosting/bin/Release/*.snupkg
src/Temporalio.Extensions.OpenTelemetry/bin/Release/*.nupkg
diff --git a/Directory.Packages.props b/Directory.Packages.props
index a57f3505..34ba9ffd 100644
--- a/Directory.Packages.props
+++ b/Directory.Packages.props
@@ -3,6 +3,7 @@
true
+
@@ -15,8 +16,11 @@
+
+
+
diff --git a/README.md b/README.md
index 5fa1d8e6..72c2362b 100644
--- a/README.md
+++ b/README.md
@@ -276,6 +276,26 @@ var client = await TemporalClient.ConnectAsync(new("my-namespace.a1b2c.tmprl.clo
});
```
+#### Client Configuration From Environment
+
+Client connection options can be loaded from a `temporal.toml` file and environment variables:
+
+```csharp
+using Temporalio.Client;
+using Temporalio.Common.EnvConfig;
+
+var client = await TemporalClient.ConnectAsync(
+ ClientEnvConfig.LoadClientConnectOptions());
+```
+
+By default, the loader checks `TEMPORAL_CONFIG_FILE`; if unset, it looks for `temporal.toml` in the
+user config directory under `temporalio`. The selected profile is `TEMPORAL_PROFILE`, or `default`
+when unset. Environment variables such as `TEMPORAL_ADDRESS`, `TEMPORAL_NAMESPACE`,
+`TEMPORAL_API_KEY`, TLS certificate/key settings, and `TEMPORAL_GRPC_META_*` override file values.
+
+Use `ClientEnvConfig.ProfileLoadOptions` to choose a profile, provide an explicit config source, or
+disable file or environment loading.
+
#### Client Dependency Injection
To create clients for use with dependency injection, see the
diff --git a/Temporalio.sln b/Temporalio.sln
index be9ec1f3..faaca396 100644
--- a/Temporalio.sln
+++ b/Temporalio.sln
@@ -17,8 +17,12 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Temporalio.Extensions.Hosti
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Temporalio.Extensions.DiagnosticSource", "src\Temporalio.Extensions.DiagnosticSource\Temporalio.Extensions.DiagnosticSource.csproj", "{CC7EA7CD-BBE7-448C-8A4B-F8B2D1E55990}"
EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Temporalio.Extensions.Aws.Lambda", "src\Temporalio.Extensions.Aws.Lambda\Temporalio.Extensions.Aws.Lambda.csproj", "{B7CDF2C9-1D1D-446C-AF90-6C758D7DF19D}"
+EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Temporalio.SimpleBench", "tests\Temporalio.SimpleBench\Temporalio.SimpleBench.csproj", "{2610AFAE-FD3A-4583-8CA5-4869E1347A3C}"
EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Temporalio.Extensions.Aws.Lambda.OpenTelemetry", "src\Temporalio.Extensions.Aws.Lambda.OpenTelemetry\Temporalio.Extensions.Aws.Lambda.OpenTelemetry.csproj", "{9A2C7274-7ED2-4C92-BE92-13887C3309B4}"
+EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
@@ -48,10 +52,18 @@ Global
{CC7EA7CD-BBE7-448C-8A4B-F8B2D1E55990}.Debug|Any CPU.Build.0 = Debug|Any CPU
{CC7EA7CD-BBE7-448C-8A4B-F8B2D1E55990}.Release|Any CPU.ActiveCfg = Release|Any CPU
{CC7EA7CD-BBE7-448C-8A4B-F8B2D1E55990}.Release|Any CPU.Build.0 = Release|Any CPU
+ {B7CDF2C9-1D1D-446C-AF90-6C758D7DF19D}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {B7CDF2C9-1D1D-446C-AF90-6C758D7DF19D}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {B7CDF2C9-1D1D-446C-AF90-6C758D7DF19D}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {B7CDF2C9-1D1D-446C-AF90-6C758D7DF19D}.Release|Any CPU.Build.0 = Release|Any CPU
{2610AFAE-FD3A-4583-8CA5-4869E1347A3C}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{2610AFAE-FD3A-4583-8CA5-4869E1347A3C}.Debug|Any CPU.Build.0 = Debug|Any CPU
{2610AFAE-FD3A-4583-8CA5-4869E1347A3C}.Release|Any CPU.ActiveCfg = Release|Any CPU
{2610AFAE-FD3A-4583-8CA5-4869E1347A3C}.Release|Any CPU.Build.0 = Release|Any CPU
+ {9A2C7274-7ED2-4C92-BE92-13887C3309B4}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {9A2C7274-7ED2-4C92-BE92-13887C3309B4}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {9A2C7274-7ED2-4C92-BE92-13887C3309B4}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {9A2C7274-7ED2-4C92-BE92-13887C3309B4}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(NestedProjects) = preSolution
{7AE1422A-0937-40D7-9A62-431DD0E2F6D5} = {758B61E2-9AB6-46BF-B53C-16BD140BF56B}
@@ -59,6 +71,8 @@ Global
{D4AC2E2B-1C24-491D-9175-874D448D30FE} = {758B61E2-9AB6-46BF-B53C-16BD140BF56B}
{E8D1975A-5AF7-4375-BAD0-3C256DCB7F87} = {758B61E2-9AB6-46BF-B53C-16BD140BF56B}
{CC7EA7CD-BBE7-448C-8A4B-F8B2D1E55990} = {758B61E2-9AB6-46BF-B53C-16BD140BF56B}
+ {B7CDF2C9-1D1D-446C-AF90-6C758D7DF19D} = {758B61E2-9AB6-46BF-B53C-16BD140BF56B}
{2610AFAE-FD3A-4583-8CA5-4869E1347A3C} = {F2683DAA-F157-448E-96C8-DF7BB019886D}
+ {9A2C7274-7ED2-4C92-BE92-13887C3309B4} = {758B61E2-9AB6-46BF-B53C-16BD140BF56B}
EndGlobalSection
EndGlobal
diff --git a/src/Temporalio.ApiDoc/api/index.md b/src/Temporalio.ApiDoc/api/index.md
index 00aa778f..866d67c7 100644
--- a/src/Temporalio.ApiDoc/api/index.md
+++ b/src/Temporalio.ApiDoc/api/index.md
@@ -16,5 +16,7 @@ Commonly used namespaces:
Extensions:
* [Temporalio.Extensions.DiagnosticSource](/api/Temporalio.Extensions.DiagnosticSource.html)
+* [Temporalio.Extensions.Aws.Lambda](/api/Temporalio.Extensions.Aws.Lambda.html)
+* [Temporalio.Extensions.Aws.Lambda.OpenTelemetry](/api/Temporalio.Extensions.Aws.Lambda.OpenTelemetry.html)
* [Temporalio.Extensions.Hosting](/api/Temporalio.Extensions.Hosting.html)
-* [Temporalio.Extensions.OpenTelemetry](/api/Temporalio.Extensions.OpenTelemetry.html)
\ No newline at end of file
+* [Temporalio.Extensions.OpenTelemetry](/api/Temporalio.Extensions.OpenTelemetry.html)
diff --git a/src/Temporalio.ApiDoc/docfx.json b/src/Temporalio.ApiDoc/docfx.json
index 9adb184e..4606b23e 100644
--- a/src/Temporalio.ApiDoc/docfx.json
+++ b/src/Temporalio.ApiDoc/docfx.json
@@ -6,6 +6,8 @@
"files": [
"Temporalio/*.csproj",
"Temporalio.Extensions.DiagnosticSource/*.csproj",
+ "Temporalio.Extensions.Aws.Lambda/*.csproj",
+ "Temporalio.Extensions.Aws.Lambda.OpenTelemetry/*.csproj",
"Temporalio.Extensions.Hosting/*.csproj",
"Temporalio.Extensions.OpenTelemetry/*.csproj"
],
@@ -72,4 +74,4 @@
"keepFileLink": false,
"disableGitFeatures": false
}
-}
\ No newline at end of file
+}
diff --git a/src/Temporalio.Extensions.Aws.Lambda.OpenTelemetry/LambdaWorkerOpenTelemetry.cs b/src/Temporalio.Extensions.Aws.Lambda.OpenTelemetry/LambdaWorkerOpenTelemetry.cs
new file mode 100644
index 00000000..3db4c63c
--- /dev/null
+++ b/src/Temporalio.Extensions.Aws.Lambda.OpenTelemetry/LambdaWorkerOpenTelemetry.cs
@@ -0,0 +1,204 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Threading;
+using System.Threading.Tasks;
+using OpenTelemetry;
+using OpenTelemetry.Exporter;
+using OpenTelemetry.Resources;
+using OpenTelemetry.Trace;
+using Temporalio.Client.Interceptors;
+using Temporalio.Runtime;
+using TemporalOpenTelemetry = Temporalio.Extensions.OpenTelemetry;
+
+namespace Temporalio.Extensions.Aws.Lambda.OpenTelemetry
+{
+ ///
+ /// OpenTelemetry helpers for Temporal workers running inside AWS Lambda.
+ ///
+ public static class LambdaWorkerOpenTelemetry
+ {
+ private const string DefaultCollectorEndpoint = "http://localhost:4317";
+ private const string DefaultServiceName = "temporal-lambda-worker";
+ private const string OTelExporterOtlpEndpointEnvironmentVariable =
+ "OTEL_EXPORTER_OTLP_ENDPOINT";
+
+ private const string OTelServiceNameEnvironmentVariable = "OTEL_SERVICE_NAME";
+ private const string LambdaFunctionNameEnvironmentVariable = "AWS_LAMBDA_FUNCTION_NAME";
+ private const string ServiceNameResourceAttribute = "service.name";
+
+ ///
+ /// Configure OpenTelemetry metrics and tracing with AWS Lambda defaults.
+ ///
+ /// Lambda worker configuration to mutate.
+ /// Optional OpenTelemetry configuration.
+ ///
+ /// This creates an OTLP trace exporter and tracer provider, configures Core SDK metrics
+ /// through a Temporal runtime, adds the Temporal tracing interceptor, and registers a
+ /// per-invocation shutdown hook to force-flush traces before the Lambda invocation ends.
+ ///
+ public static void ApplyDefaults(
+ LambdaWorkerConfig config,
+ LambdaWorkerOpenTelemetryOptions? options = null)
+ {
+ if (config == null)
+ {
+ throw new ArgumentNullException(nameof(config));
+ }
+
+ var resolvedOptions = ResolveOptions(options);
+#pragma warning disable CA2000 // Provider is intentionally retained for Lambda warm invocations.
+ var tracerProvider = CreateTracerProvider(resolvedOptions);
+#pragma warning restore CA2000
+
+ config.ClientOptions.Interceptors = AddTracingInterceptor(
+ config.ClientOptions.Interceptors);
+ config.ClientOptions.Runtime = CreateRuntime(resolvedOptions);
+ config.ShutdownHooks.Add(
+ cancellationToken => ForceFlushAsync(
+ tracerProvider,
+ config.ShutdownDeadlineBuffer,
+ cancellationToken));
+ }
+
+ ///
+ /// Resolve options using process environment variables.
+ ///
+ /// Options to resolve.
+ /// Resolved options.
+ internal static ResolvedLambdaWorkerOpenTelemetryOptions ResolveOptions(
+ LambdaWorkerOpenTelemetryOptions? options = null)
+ {
+ options ??= new LambdaWorkerOpenTelemetryOptions();
+ if (options.MetricsExportInterval <= TimeSpan.Zero)
+ {
+ throw new ArgumentOutOfRangeException(
+ nameof(options),
+ "MetricsExportInterval must be greater than zero");
+ }
+
+ var serviceName = FirstNonEmpty(
+ options.ServiceName,
+ Environment.GetEnvironmentVariable(OTelServiceNameEnvironmentVariable),
+ Environment.GetEnvironmentVariable(LambdaFunctionNameEnvironmentVariable),
+ DefaultServiceName);
+ var collectorEndpoint = FirstNonEmpty(
+ options.CollectorEndpoint,
+ Environment.GetEnvironmentVariable(OTelExporterOtlpEndpointEnvironmentVariable),
+ DefaultCollectorEndpoint);
+
+ return new ResolvedLambdaWorkerOpenTelemetryOptions(
+ new Uri(collectorEndpoint),
+ serviceName,
+ options.MetricsExportInterval);
+ }
+
+ ///
+ /// Force-flush the tracer provider asynchronously.
+ ///
+ /// Tracer provider to flush.
+ /// Maximum time to wait for the flush.
+ /// Cancellation token.
+ /// A task for the flush.
+ internal static async Task ForceFlushAsync(
+ TracerProvider tracerProvider,
+ TimeSpan shutdownDeadlineBuffer,
+ CancellationToken cancellationToken)
+ {
+ if (cancellationToken.IsCancellationRequested)
+ {
+ return;
+ }
+
+ var flushTask = Task.Run(
+ () => tracerProvider.ForceFlush(ToTimeoutMilliseconds(shutdownDeadlineBuffer)));
+ if (flushTask == await Task.WhenAny(
+ flushTask,
+ Task.Delay(Timeout.Infinite, cancellationToken)).ConfigureAwait(false))
+ {
+ await flushTask.ConfigureAwait(false);
+ }
+ else
+ {
+ ObserveTaskException(flushTask);
+ }
+ }
+
+ private static string FirstNonEmpty(params string?[] values) =>
+ values.First(value => !string.IsNullOrEmpty(value))!;
+
+ private static void ObserveTaskException(Task task) =>
+ _ = task.ContinueWith(
+ completedTask => _ = completedTask.Exception,
+ CancellationToken.None,
+ TaskContinuationOptions.OnlyOnFaulted | TaskContinuationOptions.ExecuteSynchronously,
+ TaskScheduler.Default);
+
+ private static TracerProvider CreateTracerProvider(
+ ResolvedLambdaWorkerOpenTelemetryOptions options) =>
+ Sdk.CreateTracerProviderBuilder().
+ AddXRayTraceId().
+ SetResourceBuilder(
+ ResourceBuilder.CreateDefault().AddService(options.ServiceName)).
+ AddSource(
+ TemporalOpenTelemetry.TracingInterceptor.ClientSource.Name,
+ TemporalOpenTelemetry.TracingInterceptor.WorkflowsSource.Name,
+ TemporalOpenTelemetry.TracingInterceptor.ActivitiesSource.Name,
+ TemporalOpenTelemetry.TracingInterceptor.NexusSource.Name).
+ AddOtlpExporter(exporterOptions =>
+ {
+ exporterOptions.Endpoint = options.CollectorEndpoint;
+#pragma warning disable CS0618 // ADOT Lambda parity uses OTLP gRPC on localhost:4317.
+ exporterOptions.Protocol = OtlpExportProtocol.Grpc;
+#pragma warning restore CS0618
+ }).
+ Build();
+
+ private static List AddTracingInterceptor(
+ IReadOnlyCollection? interceptors)
+ {
+ var newInterceptors = interceptors?.ToList() ?? new List();
+ newInterceptors.Add(new TemporalOpenTelemetry.TracingInterceptor());
+ return newInterceptors;
+ }
+
+ private static TemporalRuntime CreateRuntime(
+ ResolvedLambdaWorkerOpenTelemetryOptions options)
+ {
+ var openTelemetryOptions = new Temporalio.Runtime.OpenTelemetryOptions(
+ options.CollectorEndpoint)
+ {
+ MetricsExportInterval = options.MetricsExportInterval,
+ Protocol = OpenTelemetryProtocol.Grpc,
+ };
+ return new TemporalRuntime(new TemporalRuntimeOptions
+ {
+ Telemetry = new TelemetryOptions
+ {
+ Metrics = new MetricsOptions(openTelemetryOptions)
+ {
+ GlobalTags = new[]
+ {
+ new KeyValuePair(
+ ServiceNameResourceAttribute,
+ options.ServiceName),
+ },
+ },
+ },
+ });
+ }
+
+ private static int ToTimeoutMilliseconds(TimeSpan timeout)
+ {
+ if (timeout <= TimeSpan.Zero)
+ {
+ return 0;
+ }
+ if (timeout.TotalMilliseconds >= int.MaxValue)
+ {
+ return int.MaxValue;
+ }
+ return (int)timeout.TotalMilliseconds;
+ }
+ }
+}
diff --git a/src/Temporalio.Extensions.Aws.Lambda.OpenTelemetry/LambdaWorkerOpenTelemetryOptions.cs b/src/Temporalio.Extensions.Aws.Lambda.OpenTelemetry/LambdaWorkerOpenTelemetryOptions.cs
new file mode 100644
index 00000000..b03ad440
--- /dev/null
+++ b/src/Temporalio.Extensions.Aws.Lambda.OpenTelemetry/LambdaWorkerOpenTelemetryOptions.cs
@@ -0,0 +1,27 @@
+using System;
+
+namespace Temporalio.Extensions.Aws.Lambda.OpenTelemetry
+{
+ ///
+ /// Options for .
+ ///
+ public class LambdaWorkerOpenTelemetryOptions
+ {
+ ///
+ /// Gets or sets how often the Core SDK exports metrics to the collector.
+ ///
+ public TimeSpan MetricsExportInterval { get; set; } = TimeSpan.FromSeconds(10);
+
+ ///
+ /// Gets or sets the OpenTelemetry service name. If unset, this falls back to
+ /// OTEL_SERVICE_NAME, then AWS_LAMBDA_FUNCTION_NAME, then "temporal-lambda-worker".
+ ///
+ public string? ServiceName { get; set; }
+
+ ///
+ /// Gets or sets the OTLP collector endpoint. If unset, this falls back to
+ /// OTEL_EXPORTER_OTLP_ENDPOINT, then "http://localhost:4317".
+ ///
+ public string? CollectorEndpoint { get; set; }
+ }
+}
diff --git a/src/Temporalio.Extensions.Aws.Lambda.OpenTelemetry/README.md b/src/Temporalio.Extensions.Aws.Lambda.OpenTelemetry/README.md
new file mode 100644
index 00000000..f9a487ff
--- /dev/null
+++ b/src/Temporalio.Extensions.Aws.Lambda.OpenTelemetry/README.md
@@ -0,0 +1,71 @@
+# AWS Lambda Worker OpenTelemetry Support
+
+This extension adds OpenTelemetry helpers for Temporal workers running inside AWS Lambda.
+
+Add the `Temporalio.Extensions.Aws.Lambda.OpenTelemetry` package from
+[NuGet](https://www.nuget.org/packages/Temporalio.Extensions.Aws.Lambda.OpenTelemetry). For example, using the
+`dotnet` CLI:
+
+ dotnet add package Temporalio.Extensions.Aws.Lambda.OpenTelemetry
+
+## Quick Start
+
+Call `LambdaWorkerOpenTelemetry.ApplyDefaults` from the Lambda worker configure callback:
+
+```csharp
+using Amazon.Lambda.Core;
+using Temporalio.Common;
+using Temporalio.Extensions.Aws.Lambda;
+using Temporalio.Extensions.Aws.Lambda.OpenTelemetry;
+
+private static readonly Func
diff --git a/src/Temporalio/Worker/TemporalWorker.cs b/src/Temporalio/Worker/TemporalWorker.cs
index deea8248..7d924d93 100644
--- a/src/Temporalio/Worker/TemporalWorker.cs
+++ b/src/Temporalio/Worker/TemporalWorker.cs
@@ -57,6 +57,7 @@ public TemporalWorker(IWorkerClient client, TemporalWorkerOptions options)
{
plugin.ConfigureWorker(Options);
}
+ Options.ApplyPostPluginConfiguration();
// Ensure later accesses use the modified version of options.
options = Options;
diff --git a/src/Temporalio/Worker/TemporalWorkerOptions.cs b/src/Temporalio/Worker/TemporalWorkerOptions.cs
index e3953a91..ee335d9b 100644
--- a/src/Temporalio/Worker/TemporalWorkerOptions.cs
+++ b/src/Temporalio/Worker/TemporalWorkerOptions.cs
@@ -376,6 +376,14 @@ public TemporalWorkerOptions()
internal Func WorkflowInstanceFactory { get; set; } =
DefaultWorkflowInstanceFactory;
+ ///
+ /// Gets or sets a function to run after worker plugins configure options.
+ ///
+ ///
+ /// Don't expose this until there's a use case.
+ ///
+ internal Action? PostPluginConfiguration { get; set; }
+
///
/// Add the given delegate with as an activity. This is
/// usually a method reference.
@@ -517,5 +525,10 @@ internal void OnTaskCompleted(WorkflowInstance instance, Exception? failureExcep
handler(instance, new(instance, failureException));
}
}
+
+ ///
+ /// Run post-plugin configuration.
+ ///
+ internal void ApplyPostPluginConfiguration() => PostPluginConfiguration?.Invoke(this);
}
}
diff --git a/tests/Temporalio.SimpleBench/Program.cs b/tests/Temporalio.SimpleBench/Program.cs
index d8492b6d..e16add84 100644
--- a/tests/Temporalio.SimpleBench/Program.cs
+++ b/tests/Temporalio.SimpleBench/Program.cs
@@ -153,4 +153,4 @@ public record Results(
TimeSpan StartDuration,
TimeSpan ResultDuration,
decimal WorkflowsPerSecond);
-}
\ No newline at end of file
+}
diff --git a/tests/Temporalio.Tests/Common/EnvConfig/ClientConfigTests.cs b/tests/Temporalio.Tests/Common/EnvConfig/ClientConfigTests.cs
index baa467f0..45632842 100644
--- a/tests/Temporalio.Tests/Common/EnvConfig/ClientConfigTests.cs
+++ b/tests/Temporalio.Tests/Common/EnvConfig/ClientConfigTests.cs
@@ -6,7 +6,7 @@ namespace Temporalio.Tests.Common.EnvConfig
{
///
/// Environment configuration tests following Python/TypeScript patterns for cross-SDK consistency.
- /// Comprehensive 34-test suite covering all aspects of environment configuration.
+ /// Comprehensive test suite covering all aspects of environment configuration.
///
public class ClientConfigTests : TestBase
{
@@ -196,7 +196,7 @@ public void Test_Profile_Null_Address_Preserves_Null_In_Connection_Options()
Assert.Equal("test-namespace", options.Namespace);
}
- // === ENVIRONMENT VARIABLES TESTS (4 tests) ===
+ // === ENVIRONMENT VARIABLES TESTS (5 tests) ===
[Fact]
public void Test_Load_Profile_Grpc_Meta_Env_Overrides()
{
@@ -287,6 +287,32 @@ public void Test_Load_Profile_Disable_Env()
Assert.Equal("default-address", profile.Address);
}
+ [Fact]
+ public void EmptyOverrideEnvVarsSuppressesSystemEnvironment()
+ {
+ var previousAddress = Environment.GetEnvironmentVariable("TEMPORAL_ADDRESS");
+ var previousNamespace = Environment.GetEnvironmentVariable("TEMPORAL_NAMESPACE");
+ try
+ {
+ Environment.SetEnvironmentVariable("TEMPORAL_ADDRESS", "system-address");
+ Environment.SetEnvironmentVariable("TEMPORAL_NAMESPACE", "system-namespace");
+
+ var profile = ClientEnvConfig.Profile.Load(new ClientEnvConfig.ProfileLoadOptions
+ {
+ DisableFile = true,
+ OverrideEnvVars = new Dictionary(),
+ });
+
+ Assert.Null(profile.Address);
+ Assert.Null(profile.Namespace);
+ }
+ finally
+ {
+ Environment.SetEnvironmentVariable("TEMPORAL_ADDRESS", previousAddress);
+ Environment.SetEnvironmentVariable("TEMPORAL_NAMESPACE", previousNamespace);
+ }
+ }
+
// === CONTROL FLAGS TESTS (3 tests) ===
[Fact]
public void Test_Load_Profile_Disable_File()
@@ -465,7 +491,8 @@ public void Test_Load_Profile_Tls_Options()
Assert.True(profileDisabled.Tls.Disabled);
var optionsDisabled = profileDisabled.ToClientConnectionOptions();
- Assert.Null(optionsDisabled.Tls);
+ Assert.NotNull(optionsDisabled.Tls);
+ Assert.True(optionsDisabled.Tls.Disabled);
// Test TLS with certs
var profileCerts = ClientEnvConfig.Profile.Load(new ClientEnvConfig.ProfileLoadOptions
@@ -682,7 +709,9 @@ public void Test_Tls_Disabled_Tri_State_Behavior()
ConfigSource = DataSource.FromUTF8String(tomlTrue),
});
Assert.True(profileTrue.Tls!.Disabled); // explicitly disabled=true
- Assert.Null(profileTrue.ToClientConnectionOptions().Tls); // TLS disabled even with API key
+ var optionsTrue = profileTrue.ToClientConnectionOptions();
+ Assert.NotNull(optionsTrue.Tls);
+ Assert.True(optionsTrue.Tls.Disabled); // TLS disabled even with API key
}
// === ERROR HANDLING TESTS (4 tests) ===
diff --git a/tests/Temporalio.Tests/Extensions/Aws/Lambda/OpenTelemetry/LambdaWorkerOpenTelemetryTests.cs b/tests/Temporalio.Tests/Extensions/Aws/Lambda/OpenTelemetry/LambdaWorkerOpenTelemetryTests.cs
new file mode 100644
index 00000000..f0e0c3e5
--- /dev/null
+++ b/tests/Temporalio.Tests/Extensions/Aws/Lambda/OpenTelemetry/LambdaWorkerOpenTelemetryTests.cs
@@ -0,0 +1,295 @@
+namespace Temporalio.Tests.Extensions.Aws.Lambda.OpenTelemetry;
+
+using global::OpenTelemetry;
+using global::OpenTelemetry.Trace;
+using Temporalio.Client;
+using Temporalio.Client.Interceptors;
+using Temporalio.Extensions.Aws.Lambda;
+using Temporalio.Extensions.Aws.Lambda.OpenTelemetry;
+using Xunit;
+using TemporalOpenTelemetry = Temporalio.Extensions.OpenTelemetry;
+
+public class LambdaWorkerOpenTelemetryTests
+{
+ private const string OTelExporterOtlpEndpointEnvironmentVariable =
+ "OTEL_EXPORTER_OTLP_ENDPOINT";
+
+ private const string OTelServiceNameEnvironmentVariable = "OTEL_SERVICE_NAME";
+ private const string LambdaFunctionNameEnvironmentVariable = "AWS_LAMBDA_FUNCTION_NAME";
+
+ [Fact]
+ public void ApplyDefaults_NullConfigThrows()
+ {
+ Assert.Throws(() =>
+ LambdaWorkerOpenTelemetry.ApplyDefaults(null!));
+ }
+
+ [Fact]
+ public void ResolveOptions_ExplicitOptionsWin()
+ {
+ using var env = new EnvironmentScope(
+ KeyValuePair.Create(
+ OTelExporterOtlpEndpointEnvironmentVariable,
+ "http://env:4317"),
+ KeyValuePair.Create(
+ OTelServiceNameEnvironmentVariable,
+ "env-service"),
+ KeyValuePair.Create(
+ LambdaFunctionNameEnvironmentVariable,
+ "lambda-service"));
+ var resolved = LambdaWorkerOpenTelemetry.ResolveOptions(new LambdaWorkerOpenTelemetryOptions
+ {
+ CollectorEndpoint = "http://explicit:4317",
+ ServiceName = "explicit-service",
+ MetricsExportInterval = TimeSpan.FromSeconds(3),
+ });
+
+ Assert.Equal(new Uri("http://explicit:4317"), resolved.CollectorEndpoint);
+ Assert.Equal("explicit-service", resolved.ServiceName);
+ Assert.Equal(TimeSpan.FromSeconds(3), resolved.MetricsExportInterval);
+ }
+
+ [Fact]
+ public void ResolveOptions_EnvironmentWinsOverFallbacks()
+ {
+ using var env = new EnvironmentScope(
+ KeyValuePair.Create(
+ OTelExporterOtlpEndpointEnvironmentVariable,
+ "http://env:4317"),
+ KeyValuePair.Create(
+ OTelServiceNameEnvironmentVariable,
+ "env-service"),
+ KeyValuePair.Create(
+ LambdaFunctionNameEnvironmentVariable,
+ "lambda-service"));
+ var resolved = LambdaWorkerOpenTelemetry.ResolveOptions();
+
+ Assert.Equal(new Uri("http://env:4317"), resolved.CollectorEndpoint);
+ Assert.Equal("env-service", resolved.ServiceName);
+ Assert.Equal(TimeSpan.FromSeconds(10), resolved.MetricsExportInterval);
+ }
+
+ [Fact]
+ public void ResolveOptions_LambdaFunctionNameWinsOverDefaultServiceName()
+ {
+ using var env = new EnvironmentScope(
+ KeyValuePair.Create(
+ OTelExporterOtlpEndpointEnvironmentVariable,
+ null),
+ KeyValuePair.Create(
+ OTelServiceNameEnvironmentVariable,
+ null),
+ KeyValuePair.Create(
+ LambdaFunctionNameEnvironmentVariable,
+ "lambda-service"));
+ var resolved = LambdaWorkerOpenTelemetry.ResolveOptions();
+
+ Assert.Equal(new Uri("http://localhost:4317"), resolved.CollectorEndpoint);
+ Assert.Equal("lambda-service", resolved.ServiceName);
+ }
+
+ [Fact]
+ public void ResolveOptions_UsesFallbacks()
+ {
+ using var env = new EnvironmentScope(
+ KeyValuePair.Create(
+ OTelExporterOtlpEndpointEnvironmentVariable,
+ null),
+ KeyValuePair.Create(
+ OTelServiceNameEnvironmentVariable,
+ null),
+ KeyValuePair.Create(
+ LambdaFunctionNameEnvironmentVariable,
+ null));
+ var resolved = LambdaWorkerOpenTelemetry.ResolveOptions();
+
+ Assert.Equal(new Uri("http://localhost:4317"), resolved.CollectorEndpoint);
+ Assert.Equal("temporal-lambda-worker", resolved.ServiceName);
+ Assert.Equal(TimeSpan.FromSeconds(10), resolved.MetricsExportInterval);
+ }
+
+ [Fact]
+ public void ResolveOptions_InvalidMetricsExportIntervalThrows()
+ {
+ Assert.Throws(() =>
+ LambdaWorkerOpenTelemetry.ResolveOptions(
+ new LambdaWorkerOpenTelemetryOptions
+ {
+ MetricsExportInterval = TimeSpan.Zero,
+ }));
+ }
+
+ [Fact]
+ public void ApplyDefaults_PreservesInterceptorsAndAddsTracing()
+ {
+ var existingInterceptor = new NoopClientInterceptor();
+ var config = new LambdaWorkerConfig
+ {
+ ClientOptions = new TemporalClientConnectOptions
+ {
+ Interceptors = new IClientInterceptor[] { existingInterceptor },
+ },
+ };
+
+ LambdaWorkerOpenTelemetry.ApplyDefaults(config);
+
+ var interceptors = Assert.IsAssignableFrom>(
+ config.ClientOptions.Interceptors);
+ Assert.Equal(2, interceptors.Count);
+ Assert.Same(existingInterceptor, interceptors.First());
+ Assert.IsType(interceptors.Last());
+ }
+
+ [Fact]
+ public async Task ApplyDefaults_ConfiguresRuntimeAndShutdownHook()
+ {
+ var config = new LambdaWorkerConfig
+ {
+ ShutdownDeadlineBuffer = TimeSpan.FromMilliseconds(1),
+ };
+ config.ShutdownHooks.Add(_ => Task.CompletedTask);
+
+ LambdaWorkerOpenTelemetry.ApplyDefaults(
+ config,
+ new LambdaWorkerOpenTelemetryOptions
+ {
+ CollectorEndpoint = "http://localhost:4317",
+ ServiceName = "test-service",
+ MetricsExportInterval = TimeSpan.FromSeconds(1),
+ });
+
+ Assert.NotNull(config.ClientOptions.Runtime);
+ Assert.Equal(2, config.ShutdownHooks.Count);
+ await config.ShutdownHooks[1](CancellationToken.None);
+ }
+
+ [Fact]
+ public async Task ForceFlushAsync_RunsForceFlushOffCallerThread()
+ {
+ using var flushStarted = new ManualResetEventSlim();
+ using var releaseFlush = new ManualResetEventSlim();
+#pragma warning disable CA2000 // Tracer provider owns the processor/exporter.
+ using var provider = Sdk.CreateTracerProviderBuilder().
+ AddProcessor(new SimpleActivityExportProcessor(
+ new BlockingForceFlushExporter(flushStarted, releaseFlush))).
+ Build();
+#pragma warning restore CA2000
+
+#pragma warning disable CA2025 // The task is completed before the provider exits scope.
+ var flushTask = LambdaWorkerOpenTelemetry.ForceFlushAsync(
+ provider,
+ TimeSpan.FromSeconds(10),
+ CancellationToken.None);
+#pragma warning restore CA2025
+
+ try
+ {
+ Assert.True(flushStarted.Wait(TimeSpan.FromSeconds(5)));
+ Assert.False(flushTask.IsCompleted);
+ }
+ finally
+ {
+ releaseFlush.Set();
+ await flushTask.WaitAsync(TimeSpan.FromSeconds(5));
+ }
+ }
+
+ [Fact]
+ public async Task ForceFlushAsync_ReturnsWhenCancellationRequested()
+ {
+ using var flushStarted = new ManualResetEventSlim();
+ using var releaseFlush = new ManualResetEventSlim();
+ using var flushCompleted = new ManualResetEventSlim();
+#pragma warning disable CA2000 // Tracer provider owns the processor/exporter.
+ using var provider = Sdk.CreateTracerProviderBuilder().
+ AddProcessor(new SimpleActivityExportProcessor(
+ new BlockingForceFlushExporter(flushStarted, releaseFlush, flushCompleted))).
+ Build();
+#pragma warning restore CA2000
+ using var cts = new CancellationTokenSource();
+
+#pragma warning disable CA2025 // The provider exits scope after the blocking flush is released.
+ var flushTask = LambdaWorkerOpenTelemetry.ForceFlushAsync(
+ provider,
+ TimeSpan.FromSeconds(10),
+ cts.Token);
+#pragma warning restore CA2025
+
+ try
+ {
+ Assert.True(flushStarted.Wait(TimeSpan.FromSeconds(5)));
+ await cts.CancelAsync();
+ await flushTask.WaitAsync(TimeSpan.FromSeconds(5));
+ Assert.False(flushCompleted.IsSet);
+ }
+ finally
+ {
+ releaseFlush.Set();
+ Assert.True(flushCompleted.Wait(TimeSpan.FromSeconds(5)));
+ }
+ }
+
+ private sealed class BlockingForceFlushExporter :
+ BaseExporter
+ {
+ private readonly ManualResetEventSlim flushStarted;
+ private readonly ManualResetEventSlim releaseFlush;
+ private readonly ManualResetEventSlim? flushCompleted;
+
+ public BlockingForceFlushExporter(
+ ManualResetEventSlim flushStarted,
+ ManualResetEventSlim releaseFlush,
+ ManualResetEventSlim? flushCompleted = null)
+ {
+ this.flushStarted = flushStarted;
+ this.releaseFlush = releaseFlush;
+ this.flushCompleted = flushCompleted;
+ }
+
+ public override ExportResult Export(in Batch batch) =>
+ ExportResult.Success;
+
+ protected override bool OnForceFlush(int timeoutMilliseconds)
+ {
+ flushStarted.Set();
+ try
+ {
+ return releaseFlush.Wait(timeoutMilliseconds);
+ }
+ finally
+ {
+ flushCompleted?.Set();
+ }
+ }
+ }
+
+ private sealed class NoopClientInterceptor : IClientInterceptor
+ {
+ public ClientOutboundInterceptor InterceptClient(
+ ClientOutboundInterceptor nextInterceptor) => nextInterceptor;
+ }
+
+ private sealed class EnvironmentScope : IDisposable
+ {
+ private readonly IReadOnlyDictionary previousValues;
+
+ public EnvironmentScope(params KeyValuePair[] values)
+ {
+ previousValues = values.ToDictionary(
+ pair => pair.Key,
+ pair => Environment.GetEnvironmentVariable(pair.Key));
+ foreach (var pair in values)
+ {
+ Environment.SetEnvironmentVariable(pair.Key, pair.Value);
+ }
+ }
+
+ public void Dispose()
+ {
+ foreach (var pair in previousValues)
+ {
+ Environment.SetEnvironmentVariable(pair.Key, pair.Value);
+ }
+ }
+ }
+}
diff --git a/tests/Temporalio.Tests/Extensions/Aws/Lambda/TemporalLambdaWorkerNonParallelDefinition.cs b/tests/Temporalio.Tests/Extensions/Aws/Lambda/TemporalLambdaWorkerNonParallelDefinition.cs
new file mode 100644
index 00000000..4265f4ed
--- /dev/null
+++ b/tests/Temporalio.Tests/Extensions/Aws/Lambda/TemporalLambdaWorkerNonParallelDefinition.cs
@@ -0,0 +1,8 @@
+namespace Temporalio.Tests.Extensions.Aws.Lambda;
+
+using Xunit;
+
+[CollectionDefinition("TemporalLambdaWorkerNonParallel", DisableParallelization = true)]
+public sealed class TemporalLambdaWorkerNonParallelDefinition
+{
+}
diff --git a/tests/Temporalio.Tests/Extensions/Aws/Lambda/TemporalLambdaWorkerTests.cs b/tests/Temporalio.Tests/Extensions/Aws/Lambda/TemporalLambdaWorkerTests.cs
new file mode 100644
index 00000000..3d03852b
--- /dev/null
+++ b/tests/Temporalio.Tests/Extensions/Aws/Lambda/TemporalLambdaWorkerTests.cs
@@ -0,0 +1,1095 @@
+namespace Temporalio.Tests.Extensions.Aws.Lambda;
+
+using Amazon.Lambda.Core;
+using Temporalio.Activities;
+using Temporalio.Client;
+using Temporalio.Common;
+using Temporalio.Common.EnvConfig;
+using Temporalio.Extensions.Aws.Lambda;
+using Temporalio.Worker;
+using Temporalio.Worker.Tuning;
+using Temporalio.Workflows;
+using Xunit;
+
+[Collection("TemporalLambdaWorkerNonParallel")]
+public class TemporalLambdaWorkerTests
+{
+ private static readonly WorkerDeploymentVersion Version = new("deployment", "build");
+
+ [Fact]
+ public async Task CreateHandler_DefaultsAreAppliedAndUserOverridesWin()
+ {
+ var configureCalls = 0;
+ TemporalClientConnectOptions? capturedClientOptions = null;
+ TemporalWorkerOptions? capturedWorkerOptions = null;
+ var handler = TemporalLambdaWorker.CreateHandler(
+ Version,
+ config =>
+ {
+ configureCalls++;
+ Assert.Equal(2, config.WorkerOptions.MaxConcurrentActivities);
+ Assert.Equal(10, config.WorkerOptions.MaxConcurrentWorkflowTasks);
+ Assert.Equal(2, config.WorkerOptions.MaxConcurrentLocalActivities);
+ Assert.Equal(5, config.WorkerOptions.MaxConcurrentNexusTasks);
+ Assert.Equal(TimeSpan.FromSeconds(5), config.WorkerOptions.GracefulShutdownTimeout);
+ Assert.Equal(30, config.WorkerOptions.MaxCachedWorkflows);
+ Assert.Equal(2, config.WorkerOptions.MaxConcurrentWorkflowTaskPolls);
+ Assert.Equal(1, config.WorkerOptions.MaxConcurrentActivityTaskPolls);
+ Assert.Equal(1, config.WorkerOptions.MaxConcurrentNexusTaskPolls);
+ Assert.Null(config.WorkerOptions.WorkflowTaskPollerBehavior);
+ Assert.Null(config.WorkerOptions.ActivityTaskPollerBehavior);
+ Assert.Null(config.WorkerOptions.NexusTaskPollerBehavior);
+ Assert.True(config.WorkerOptions.DisableEagerActivityExecution);
+ Assert.NotNull(config.WorkerOptions.DeploymentOptions);
+ Assert.Equal(Version, config.WorkerOptions.DeploymentOptions.Version);
+ Assert.True(config.WorkerOptions.DeploymentOptions.UseWorkerVersioning);
+ Assert.Equal(
+ VersioningBehavior.AutoUpgrade,
+ config.WorkerOptions.DeploymentOptions.DefaultVersioningBehavior);
+ Assert.Equal("env-task-queue", config.WorkerOptions.TaskQueue);
+ Assert.Equal("loaded-address", config.ClientOptions.TargetHost);
+ Assert.Equal("loaded-namespace", config.ClientOptions.Namespace);
+
+ config.ClientOptions.TargetHost = "localhost:7233";
+ config.WorkerOptions.TaskQueue = "configured-task-queue";
+ config.WorkerOptions.MaxConcurrentActivities = 8;
+ config.WorkerOptions.MaxConcurrentActivityTaskPolls = 4;
+ config.WorkerOptions.MaxCachedWorkflows = 12;
+ config.WorkerOptions.DisableEagerActivityExecution = false;
+ config.WorkerOptions.DeploymentOptions = new WorkerDeploymentOptions(
+ new WorkerDeploymentVersion("ignored", "ignored"),
+ useWorkerVersioning: false)
+ {
+ DefaultVersioningBehavior = VersioningBehavior.Pinned,
+ };
+ config.WorkerOptions.Activities.Add(DummyActivity());
+ },
+ new TemporalLambdaWorkerHandlerOptions
+ {
+ LoadClientConnectOptions = _ => new TemporalClientConnectOptions
+ {
+ TargetHost = "loaded-address",
+ Namespace = "loaded-namespace",
+ },
+ GetEnvironmentVariable = name =>
+ name == "TEMPORAL_TASK_QUEUE" ? "env-task-queue" : null,
+ ConnectClientAsync = options =>
+ {
+ capturedClientOptions = options;
+ return Task.FromResult(new object());
+ },
+ CreateWorker = (_, options) =>
+ {
+ capturedWorkerOptions = options;
+ return new FakeLambdaWorker(_ => Task.CompletedTask);
+ },
+ });
+
+ Assert.Equal(1, configureCalls);
+ await handler(null, new FakeLambdaContext());
+
+ Assert.NotNull(capturedClientOptions);
+ Assert.NotNull(capturedWorkerOptions);
+ Assert.Equal("localhost:7233", capturedClientOptions.TargetHost);
+ Assert.Equal("loaded-namespace", capturedClientOptions.Namespace);
+ Assert.Equal("configured-task-queue", capturedWorkerOptions.TaskQueue);
+ Assert.Equal(8, capturedWorkerOptions.MaxConcurrentActivities);
+ Assert.Equal(10, capturedWorkerOptions.MaxConcurrentWorkflowTasks);
+ Assert.Equal(2, capturedWorkerOptions.MaxConcurrentLocalActivities);
+ Assert.Equal(5, capturedWorkerOptions.MaxConcurrentNexusTasks);
+ Assert.Equal(2, capturedWorkerOptions.MaxConcurrentWorkflowTaskPolls);
+ Assert.Equal(4, capturedWorkerOptions.MaxConcurrentActivityTaskPolls);
+ Assert.Equal(1, capturedWorkerOptions.MaxConcurrentNexusTaskPolls);
+ Assert.Null(capturedWorkerOptions.WorkflowTaskPollerBehavior);
+ Assert.Null(capturedWorkerOptions.ActivityTaskPollerBehavior);
+ Assert.Null(capturedWorkerOptions.NexusTaskPollerBehavior);
+ Assert.Equal(12, capturedWorkerOptions.MaxCachedWorkflows);
+ Assert.False(capturedWorkerOptions.DisableEagerActivityExecution);
+ Assert.NotNull(capturedWorkerOptions.DeploymentOptions);
+ Assert.Equal(Version, capturedWorkerOptions.DeploymentOptions.Version);
+ Assert.True(capturedWorkerOptions.DeploymentOptions.UseWorkerVersioning);
+ Assert.Equal(
+ VersioningBehavior.Pinned,
+ capturedWorkerOptions.DeploymentOptions.DefaultVersioningBehavior);
+#pragma warning disable CS0618 // Verifying the Lambda helper clears legacy versioning options.
+ Assert.Null(capturedWorkerOptions.BuildId);
+ Assert.False(capturedWorkerOptions.UseWorkerVersioning);
+#pragma warning restore CS0618
+ }
+
+ [Fact]
+ public async Task CreateHandler_DefaultsVersioningBehaviorToAutoUpgrade()
+ {
+ TemporalWorkerOptions? capturedWorkerOptions = null;
+ var handler = TemporalLambdaWorker.CreateHandler(
+ Version,
+ config =>
+ {
+ config.ClientOptions.TargetHost = "localhost:7233";
+ config.WorkerOptions.TaskQueue = "task-queue";
+ config.WorkerOptions.AddWorkflow();
+ },
+ new TemporalLambdaWorkerHandlerOptions
+ {
+ ConnectClientAsync = _ => Task.FromResult(new object()),
+ CreateWorker = (_, options) =>
+ {
+ capturedWorkerOptions = options;
+ return new FakeLambdaWorker(_ => Task.CompletedTask);
+ },
+ });
+
+ await handler(null, new FakeLambdaContext());
+
+ Assert.NotNull(capturedWorkerOptions);
+ Assert.NotNull(capturedWorkerOptions.DeploymentOptions);
+ Assert.Equal(Version, capturedWorkerOptions.DeploymentOptions.Version);
+ Assert.True(capturedWorkerOptions.DeploymentOptions.UseWorkerVersioning);
+ Assert.Equal(
+ VersioningBehavior.AutoUpgrade,
+ capturedWorkerOptions.DeploymentOptions.DefaultVersioningBehavior);
+ }
+
+ [Fact]
+ public async Task CreateHandler_LoadsDefaultClientOptionsWhenNotOverridden()
+ {
+ var loadCalls = 0;
+ TemporalClientConnectOptions? capturedClientOptions = null;
+ var handler = TemporalLambdaWorker.CreateHandler(
+ Version,
+ config =>
+ {
+ config.WorkerOptions.TaskQueue = "task-queue";
+ },
+ new TemporalLambdaWorkerHandlerOptions
+ {
+ LoadClientConnectOptions = _ =>
+ {
+ loadCalls++;
+ return new TemporalClientConnectOptions
+ {
+ TargetHost = "loaded-address",
+ Namespace = "loaded-namespace",
+ };
+ },
+ ConnectClientAsync = options =>
+ {
+ capturedClientOptions = options;
+ return Task.FromResult(new object());
+ },
+ CreateWorker = (_, _) => new FakeLambdaWorker(_ => Task.CompletedTask),
+ });
+
+ await handler(null, new FakeLambdaContext());
+
+ Assert.Equal(1, loadCalls);
+ Assert.NotNull(capturedClientOptions);
+ Assert.Equal("loaded-address", capturedClientOptions.TargetHost);
+ Assert.Equal("loaded-namespace", capturedClientOptions.Namespace);
+ }
+
+ [Fact]
+ public async Task CreateHandler_ExplicitClientOptionsBypassDefaultConfigLoad()
+ {
+ TemporalClientConnectOptions? capturedClientOptions = null;
+ var handler = TemporalLambdaWorker.CreateHandler(
+ Version,
+ config =>
+ {
+ config.ClientOptions = new TemporalClientConnectOptions
+ {
+ TargetHost = "explicit-address",
+ Namespace = "explicit-namespace",
+ };
+ config.WorkerOptions.TaskQueue = "task-queue";
+ },
+ new TemporalLambdaWorkerHandlerOptions
+ {
+ LoadClientConnectOptions = _ =>
+ throw new InvalidOperationException("Config should not be loaded"),
+ ConnectClientAsync = options =>
+ {
+ capturedClientOptions = options;
+ return Task.FromResult(new object());
+ },
+ CreateWorker = (_, _) => new FakeLambdaWorker(_ => Task.CompletedTask),
+ });
+
+ await handler(null, new FakeLambdaContext());
+
+ Assert.NotNull(capturedClientOptions);
+ Assert.Equal("explicit-address", capturedClientOptions.TargetHost);
+ Assert.Equal("explicit-namespace", capturedClientOptions.Namespace);
+ }
+
+ [Fact]
+ public async Task CreateHandler_ClearsConcurrencyDefaultsWhenTunerSet()
+ {
+ var tuner = WorkerTuner.CreateFixedSize(
+ workflowTaskSlots: 1,
+ activityTaskSlots: 2,
+ localActivitySlots: 3,
+ nexusTaskSlots: 4);
+ TemporalWorkerOptions? capturedWorkerOptions = null;
+ var handler = TemporalLambdaWorker.CreateHandler(
+ Version,
+ config =>
+ {
+ config.ClientOptions.TargetHost = "localhost:7233";
+ config.WorkerOptions.TaskQueue = "task-queue";
+ config.WorkerOptions.Tuner = tuner;
+ },
+ new TemporalLambdaWorkerHandlerOptions
+ {
+ ConnectClientAsync = _ => Task.FromResult(new object()),
+ CreateWorker = (_, options) =>
+ {
+ capturedWorkerOptions = options;
+ return new FakeLambdaWorker(_ => Task.CompletedTask);
+ },
+ });
+
+ await handler(null, new FakeLambdaContext());
+
+ Assert.NotNull(capturedWorkerOptions);
+ Assert.Same(tuner, capturedWorkerOptions.Tuner);
+ Assert.Null(capturedWorkerOptions.MaxConcurrentActivities);
+ Assert.Null(capturedWorkerOptions.MaxConcurrentWorkflowTasks);
+ Assert.Null(capturedWorkerOptions.MaxConcurrentLocalActivities);
+ Assert.Null(capturedWorkerOptions.MaxConcurrentNexusTasks);
+ }
+
+ [Fact]
+ public async Task CreateHandler_ClearsConcurrencyDefaultsWhenPluginSetsTuner()
+ {
+ var tuner = WorkerTuner.CreateFixedSize(
+ workflowTaskSlots: 1,
+ activityTaskSlots: 2,
+ localActivitySlots: 3,
+ nexusTaskSlots: 4);
+ TemporalWorkerOptions? capturedWorkerOptions = null;
+ var handler = TemporalLambdaWorker.CreateHandler(
+ Version,
+ config =>
+ {
+ config.ClientOptions.TargetHost = "localhost:7233";
+ config.WorkerOptions.TaskQueue = "task-queue";
+ config.WorkerOptions.Plugins = new[] { new TunerPlugin(tuner) };
+ },
+ new TemporalLambdaWorkerHandlerOptions
+ {
+ ConnectClientAsync = _ => Task.FromResult(new object()),
+ CreateWorker = (_, options) =>
+ {
+ foreach (var plugin in options.Plugins ?? Array.Empty())
+ {
+ plugin.ConfigureWorker(options);
+ }
+ options.ApplyPostPluginConfiguration();
+ capturedWorkerOptions = options;
+ return new FakeLambdaWorker(_ => Task.CompletedTask);
+ },
+ });
+
+ await handler(null, new FakeLambdaContext());
+
+ Assert.NotNull(capturedWorkerOptions);
+ Assert.Same(tuner, capturedWorkerOptions.Tuner);
+ Assert.Null(capturedWorkerOptions.MaxConcurrentActivities);
+ Assert.Null(capturedWorkerOptions.MaxConcurrentWorkflowTasks);
+ Assert.Null(capturedWorkerOptions.MaxConcurrentLocalActivities);
+ Assert.Null(capturedWorkerOptions.MaxConcurrentNexusTasks);
+ }
+
+ [Fact]
+ public async Task CreateHandler_ReappliesDeploymentVersionAfterPlugins()
+ {
+ TemporalWorkerOptions? capturedWorkerOptions = null;
+ var handler = TemporalLambdaWorker.CreateHandler(
+ Version,
+ config =>
+ {
+ config.ClientOptions.TargetHost = "localhost:7233";
+ config.WorkerOptions.TaskQueue = "task-queue";
+ config.WorkerOptions.Plugins = new[] { new VersioningPlugin() };
+ },
+ new TemporalLambdaWorkerHandlerOptions
+ {
+ ConnectClientAsync = _ => Task.FromResult(new object()),
+ CreateWorker = (_, options) =>
+ {
+ foreach (var plugin in options.Plugins ?? Array.Empty())
+ {
+ plugin.ConfigureWorker(options);
+ }
+ options.ApplyPostPluginConfiguration();
+ capturedWorkerOptions = options;
+ return new FakeLambdaWorker(_ => Task.CompletedTask);
+ },
+ });
+
+ await handler(null, new FakeLambdaContext());
+
+ Assert.NotNull(capturedWorkerOptions);
+ Assert.NotNull(capturedWorkerOptions.DeploymentOptions);
+ Assert.Equal(Version, capturedWorkerOptions.DeploymentOptions.Version);
+ Assert.True(capturedWorkerOptions.DeploymentOptions.UseWorkerVersioning);
+ Assert.Equal(
+ VersioningBehavior.AutoUpgrade,
+ capturedWorkerOptions.DeploymentOptions.DefaultVersioningBehavior);
+#pragma warning disable CS0618 // Verifying the Lambda helper clears legacy versioning options.
+ Assert.Null(capturedWorkerOptions.BuildId);
+ Assert.False(capturedWorkerOptions.UseWorkerVersioning);
+#pragma warning restore CS0618
+ }
+
+ [Fact]
+ public void CreateHandler_MissingDeploymentNameOrBuildIdThrows()
+ {
+ Assert.Throws(() =>
+ TemporalLambdaWorker.CreateHandler(
+ new WorkerDeploymentVersion(string.Empty, "build"),
+ _ => { }));
+ Assert.Throws(() =>
+ TemporalLambdaWorker.CreateHandler(
+ new WorkerDeploymentVersion("deployment", string.Empty),
+ _ => { }));
+ }
+
+ [Fact]
+ public async Task CreateHandler_TaskQueueCanComeFromEnvironment()
+ {
+ Assert.Throws(() =>
+ TemporalLambdaWorker.CreateHandler(
+ Version,
+ _ => { },
+ new TemporalLambdaWorkerHandlerOptions
+ {
+ GetEnvironmentVariable = _ => null,
+ }));
+
+ TemporalWorkerOptions? capturedWorkerOptions = null;
+ var handler = TemporalLambdaWorker.CreateHandler(
+ Version,
+ config => config.ClientOptions.TargetHost = "localhost:7233",
+ new TemporalLambdaWorkerHandlerOptions
+ {
+ GetEnvironmentVariable = name =>
+ name == "TEMPORAL_TASK_QUEUE" ? "env-task-queue" : null,
+ ConnectClientAsync = _ => Task.FromResult(new object()),
+ CreateWorker = (_, options) =>
+ {
+ capturedWorkerOptions = options;
+ return new FakeLambdaWorker(_ => Task.CompletedTask);
+ },
+ });
+
+ await handler(null, new FakeLambdaContext());
+ Assert.NotNull(capturedWorkerOptions);
+ Assert.Equal("env-task-queue", capturedWorkerOptions.TaskQueue);
+ }
+
+ [Fact]
+ public void LoadClientConnectOptions_ExplicitConfigSourceWins()
+ {
+ var tempDir = CreateTempDirectory();
+ try
+ {
+ var envConfigPath = Path.Combine(tempDir, "env.toml");
+ File.WriteAllText(envConfigPath, ConfigToml("env-address", "env-namespace"));
+
+ var options = TemporalLambdaWorker.LoadClientConnectOptions(
+ new ClientEnvConfig.ProfileLoadOptions
+ {
+ ConfigSource = DataSource.FromUTF8String(
+ ConfigToml("explicit-address", "explicit-namespace")),
+ OverrideEnvVars = new Dictionary
+ {
+ ["TEMPORAL_CONFIG_FILE"] = envConfigPath,
+ },
+ });
+
+ Assert.Equal("explicit-address", options.TargetHost);
+ Assert.Equal("explicit-namespace", options.Namespace);
+ }
+ finally
+ {
+ Directory.Delete(tempDir, recursive: true);
+ }
+ }
+
+ [Fact]
+ public void LoadClientConnectOptions_TemporalConfigFileWinsOverLambdaTaskRoot()
+ {
+ var tempDir = CreateTempDirectory();
+ try
+ {
+ var envConfigPath = Path.Combine(tempDir, "env.toml");
+ File.WriteAllText(envConfigPath, ConfigToml("env-address", "env-namespace"));
+ var lambdaRoot = Path.Combine(tempDir, "lambda-root");
+ Directory.CreateDirectory(lambdaRoot);
+ File.WriteAllText(
+ Path.Combine(lambdaRoot, "temporal.toml"),
+ ConfigToml("lambda-address", "lambda-namespace"));
+
+ var options = TemporalLambdaWorker.LoadClientConnectOptions(
+ new ClientEnvConfig.ProfileLoadOptions
+ {
+ OverrideEnvVars = new Dictionary
+ {
+ ["TEMPORAL_CONFIG_FILE"] = envConfigPath,
+ ["LAMBDA_TASK_ROOT"] = lambdaRoot,
+ },
+ });
+
+ Assert.Equal("env-address", options.TargetHost);
+ Assert.Equal("env-namespace", options.Namespace);
+ }
+ finally
+ {
+ Directory.Delete(tempDir, recursive: true);
+ }
+ }
+
+ [Fact]
+ public void LoadClientConnectOptions_UsesLambdaTaskRootTemporalToml()
+ {
+ var tempDir = CreateTempDirectory();
+ try
+ {
+ File.WriteAllText(
+ Path.Combine(tempDir, "temporal.toml"),
+ ConfigToml("lambda-address", "lambda-namespace"));
+
+ var options = TemporalLambdaWorker.LoadClientConnectOptions(
+ new ClientEnvConfig.ProfileLoadOptions
+ {
+ OverrideEnvVars = new Dictionary
+ {
+ ["LAMBDA_TASK_ROOT"] = tempDir,
+ },
+ });
+
+ Assert.Equal("lambda-address", options.TargetHost);
+ Assert.Equal("lambda-namespace", options.Namespace);
+ }
+ finally
+ {
+ Directory.Delete(tempDir, recursive: true);
+ }
+ }
+
+ [Fact]
+ public void LoadClientConnectOptions_FallsBackToCurrentDirectoryTemporalToml()
+ {
+ var previousDirectory = Directory.GetCurrentDirectory();
+ var tempDir = CreateTempDirectory();
+ try
+ {
+ File.WriteAllText(
+ Path.Combine(tempDir, "temporal.toml"),
+ ConfigToml("cwd-address", "cwd-namespace"));
+ Directory.SetCurrentDirectory(tempDir);
+
+ var options = TemporalLambdaWorker.LoadClientConnectOptions(
+ new ClientEnvConfig.ProfileLoadOptions
+ {
+ OverrideEnvVars = new Dictionary(),
+ });
+
+ Assert.Equal("cwd-address", options.TargetHost);
+ Assert.Equal("cwd-namespace", options.Namespace);
+ }
+ finally
+ {
+ Directory.SetCurrentDirectory(previousDirectory);
+ Directory.Delete(tempDir, recursive: true);
+ }
+ }
+
+ [Fact]
+ public void LoadClientConnectOptions_MissingLambdaConfigAllowsEnvOnly()
+ {
+ var previousDirectory = Directory.GetCurrentDirectory();
+ var tempDir = CreateTempDirectory();
+ try
+ {
+ Directory.SetCurrentDirectory(tempDir);
+
+ var options = TemporalLambdaWorker.LoadClientConnectOptions(
+ new ClientEnvConfig.ProfileLoadOptions
+ {
+ OverrideEnvVars = new Dictionary
+ {
+ ["TEMPORAL_ADDRESS"] = "env-only-address",
+ ["TEMPORAL_NAMESPACE"] = "env-only-namespace",
+ },
+ });
+
+ Assert.Equal("env-only-address", options.TargetHost);
+ Assert.Equal("env-only-namespace", options.Namespace);
+ }
+ finally
+ {
+ Directory.SetCurrentDirectory(previousDirectory);
+ Directory.Delete(tempDir, recursive: true);
+ }
+ }
+
+ [Fact]
+ public async Task Invoke_SetsLambdaIdentityUnlessUserConfiguredIdentity()
+ {
+ TemporalClientConnectOptions? capturedClientOptions = null;
+ var context = new FakeLambdaContext
+ {
+ AwsRequestId = "request-id",
+ InvokedFunctionArn = "function-arn",
+ };
+ var handler = CreateCapturingHandler(
+ config =>
+ {
+ config.ClientOptions.TargetHost = "localhost:7233";
+ config.WorkerOptions.TaskQueue = "task-queue";
+ },
+ options => capturedClientOptions = options);
+
+ await handler(null, context);
+
+ Assert.NotNull(capturedClientOptions);
+ Assert.Equal("request-id@function-arn", capturedClientOptions.Identity);
+
+ handler = CreateCapturingHandler(
+ config =>
+ {
+ config.ClientOptions.TargetHost = "localhost:7233";
+ config.ClientOptions.Identity = "user-identity";
+ config.WorkerOptions.TaskQueue = "task-queue";
+ },
+ options => capturedClientOptions = options);
+
+ await handler(null, context);
+
+ Assert.NotNull(capturedClientOptions);
+ Assert.Equal("user-identity", capturedClientOptions.Identity);
+ }
+
+ [Fact]
+ public async Task Invoke_DeadlineCancellationIsNormalAndRunsShutdownHooks()
+ {
+ var hookRan = false;
+ CancellationToken workerToken = default;
+ var handler = TemporalLambdaWorker.CreateHandler(
+ Version,
+ config =>
+ {
+ config.ClientOptions.TargetHost = "localhost:7233";
+ config.WorkerOptions.TaskQueue = "task-queue";
+ config.ShutdownDeadlineBuffer = TimeSpan.FromMilliseconds(10);
+ config.ShutdownHooks.Add(_ =>
+ {
+ hookRan = true;
+ return Task.CompletedTask;
+ });
+ },
+ new TemporalLambdaWorkerHandlerOptions
+ {
+ ConnectClientAsync = _ => Task.FromResult(new object()),
+ CreateWorker = (_, _) => new FakeLambdaWorker(async token =>
+ {
+ workerToken = token;
+ await Task.Delay(Timeout.InfiniteTimeSpan, token);
+ }),
+ });
+
+ await handler(null, new FakeLambdaContext { RemainingTime = TimeSpan.FromMilliseconds(40) });
+
+ Assert.True(workerToken.IsCancellationRequested);
+ Assert.True(hookRan);
+ }
+
+ [Fact]
+ public async Task Invoke_RecomputesWorkerBudgetAfterSetupAndBeforeWorkerRun()
+ {
+ var context = new FakeLambdaContext(
+ TimeSpan.FromMilliseconds(200),
+ TimeSpan.FromMilliseconds(40),
+ TimeSpan.FromSeconds(1));
+ var handler = TemporalLambdaWorker.CreateHandler(
+ Version,
+ config =>
+ {
+ config.ClientOptions.TargetHost = "localhost:7233";
+ config.WorkerOptions.TaskQueue = "task-queue";
+ config.ShutdownDeadlineBuffer = TimeSpan.FromMilliseconds(10);
+ },
+ new TemporalLambdaWorkerHandlerOptions
+ {
+ ConnectClientAsync = _ =>
+ {
+ Assert.Equal(1, context.RemainingTimeReadCount);
+ return Task.FromResult(new object());
+ },
+ CreateWorker = (_, _) =>
+ {
+ Assert.Equal(1, context.RemainingTimeReadCount);
+ return new FakeLambdaWorker(async token =>
+ {
+ Assert.Equal(2, context.RemainingTimeReadCount);
+ await Task.Delay(Timeout.InfiniteTimeSpan, token);
+ });
+ },
+ });
+
+ await handler(null, context);
+
+ Assert.Equal(3, context.RemainingTimeReadCount);
+ }
+
+ [Fact]
+ public async Task Invoke_TightDeadlinesThrowOrWarn()
+ {
+ var connectCalls = 0;
+ var throwingHandler = TemporalLambdaWorker.CreateHandler(
+ Version,
+ config =>
+ {
+ config.ClientOptions.TargetHost = "localhost:7233";
+ config.WorkerOptions.TaskQueue = "task-queue";
+ config.ShutdownDeadlineBuffer = TimeSpan.FromMilliseconds(100);
+ },
+ new TemporalLambdaWorkerHandlerOptions
+ {
+ ConnectClientAsync = _ =>
+ {
+ connectCalls++;
+ return Task.FromResult(new object());
+ },
+ });
+
+ await Assert.ThrowsAsync(() =>
+ throwingHandler(
+ null,
+ new FakeLambdaContext { RemainingTime = TimeSpan.FromMilliseconds(50) }));
+ Assert.Equal(0, connectCalls);
+
+ var warningContext = new FakeLambdaContext { RemainingTime = TimeSpan.FromMilliseconds(40) };
+ var warningHandler = TemporalLambdaWorker.CreateHandler(
+ Version,
+ config =>
+ {
+ config.ClientOptions.TargetHost = "localhost:7233";
+ config.WorkerOptions.TaskQueue = "task-queue";
+ config.ShutdownDeadlineBuffer = TimeSpan.FromMilliseconds(10);
+ },
+ new TemporalLambdaWorkerHandlerOptions
+ {
+ ConnectClientAsync = _ => Task.FromResult(new object()),
+ CreateWorker = (_, _) => new FakeLambdaWorker(
+ token => Task.Delay(Timeout.InfiniteTimeSpan, token)),
+ });
+
+ await warningHandler(null, warningContext);
+
+ Assert.Contains(
+ warningContext.CaptureLogger.Lines,
+ line => line.Contains("WARNING: Temporal Lambda worker budget", StringComparison.Ordinal));
+ }
+
+ [Fact]
+ public async Task Invoke_ShutdownHooksRunInOrderPerInvocationAndContinueAfterFailures()
+ {
+ var hookCalls = new List();
+ var connectCalls = 0;
+ var workerCreations = 0;
+ var context = new FakeLambdaContext();
+ var handler = TemporalLambdaWorker.CreateHandler(
+ Version,
+ config =>
+ {
+ config.ClientOptions.TargetHost = "localhost:7233";
+ config.WorkerOptions.TaskQueue = "task-queue";
+ config.ShutdownHooks.Add(_ =>
+ {
+ hookCalls.Add("first");
+ return Task.CompletedTask;
+ });
+ config.ShutdownHooks.Add(_ =>
+ {
+ hookCalls.Add("second");
+ throw new InvalidOperationException("hook failed");
+ });
+ config.ShutdownHooks.Add(_ =>
+ {
+ hookCalls.Add("third");
+ return Task.CompletedTask;
+ });
+ },
+ new TemporalLambdaWorkerHandlerOptions
+ {
+ ConnectClientAsync = _ =>
+ {
+ connectCalls++;
+ return Task.FromResult(new object());
+ },
+ CreateWorker = (_, _) =>
+ {
+ workerCreations++;
+ return new FakeLambdaWorker(_ => Task.CompletedTask);
+ },
+ });
+
+ await handler(null, context);
+ await handler(null, context);
+
+ Assert.Equal(
+ new[] { "first", "second", "third", "first", "second", "third" },
+ hookCalls);
+ Assert.Equal(
+ 2,
+ context.CaptureLogger.Lines.Count(
+ line => line.Contains("shutdown hook failed", StringComparison.Ordinal)));
+ Assert.Equal(2, connectCalls);
+ Assert.Equal(2, workerCreations);
+ }
+
+ [Fact]
+ public async Task CreateHandler_AsyncConfigureRunsPerInvocationWithFreshConfig()
+ {
+ var configureCalls = 0;
+ var capturedConfigs = new List();
+ var capturedTargets = new List();
+ var capturedTaskQueues = new List();
+ var hookCalls = new List();
+ var handler = TemporalLambdaWorker.CreateHandler(
+ Version,
+ async config =>
+ {
+ await Task.Yield();
+ var call = ++configureCalls;
+ capturedConfigs.Add(config);
+ Assert.Equal("env-task-queue", config.WorkerOptions.TaskQueue);
+
+ config.ClientOptions.TargetHost = $"target-{call}";
+ config.WorkerOptions.TaskQueue = $"task-queue-{call}";
+ config.ShutdownHooks.Add(_ =>
+ {
+ hookCalls.Add($"hook-{call}");
+ return Task.CompletedTask;
+ });
+ },
+ new TemporalLambdaWorkerHandlerOptions
+ {
+ GetEnvironmentVariable = name =>
+ name == "TEMPORAL_TASK_QUEUE" ? "env-task-queue" : null,
+ ConnectClientAsync = options =>
+ {
+ capturedTargets.Add(options.TargetHost);
+ return Task.FromResult(new object());
+ },
+ CreateWorker = (_, options) =>
+ {
+ capturedTaskQueues.Add(options.TaskQueue);
+ return new FakeLambdaWorker(_ => Task.CompletedTask);
+ },
+ });
+
+ await handler(null, new FakeLambdaContext());
+ await handler(null, new FakeLambdaContext());
+
+ Assert.Equal(2, configureCalls);
+ Assert.Equal(2, capturedConfigs.Count);
+ Assert.NotSame(capturedConfigs[0], capturedConfigs[1]);
+ Assert.Equal(new[] { "target-1", "target-2" }, capturedTargets);
+ Assert.Equal(new[] { "task-queue-1", "task-queue-2" }, capturedTaskQueues);
+ Assert.Equal(new[] { "hook-1", "hook-2" }, hookCalls);
+ }
+
+ [Fact]
+ public async Task CreateHandler_AsyncConfigureErrorsSurfaceOnInvocation()
+ {
+ var configureCalls = 0;
+ var handler = TemporalLambdaWorker.CreateHandler(
+ Version,
+ async config =>
+ {
+ _ = config;
+ await Task.Yield();
+ configureCalls++;
+ throw new InvalidOperationException("bad config");
+ },
+ new TemporalLambdaWorkerHandlerOptions());
+
+ var error = await Assert.ThrowsAsync(() =>
+ handler(null, new FakeLambdaContext()));
+ Assert.Equal("bad config", error.Message);
+ Assert.Equal(1, configureCalls);
+ }
+
+ [Fact]
+ public async Task CreateHandler_AsyncConfigureValidatesTaskQueueOnInvocation()
+ {
+ var handler = TemporalLambdaWorker.CreateHandler(
+ Version,
+ _ => Task.CompletedTask,
+ new TemporalLambdaWorkerHandlerOptions
+ {
+ GetEnvironmentVariable = _ => null,
+ });
+
+ await Assert.ThrowsAsync(() =>
+ handler(null, new FakeLambdaContext()));
+ }
+
+ [Fact]
+ public async Task CreateHandler_AsyncConfigureShutdownHooksRunAfterFailures()
+ {
+ var hookCalls = new List();
+ var connectFailureHandler = TemporalLambdaWorker.CreateHandler(
+ Version,
+ config =>
+ {
+ config.WorkerOptions.TaskQueue = "task-queue";
+ config.ShutdownHooks.Add(_ =>
+ {
+ hookCalls.Add("connect");
+ return Task.CompletedTask;
+ });
+ return Task.CompletedTask;
+ },
+ new TemporalLambdaWorkerHandlerOptions
+ {
+ ConnectClientAsync = _ =>
+ throw new InvalidOperationException("connect failed"),
+ });
+
+ await Assert.ThrowsAsync(() =>
+ connectFailureHandler(null, new FakeLambdaContext()));
+
+ var workerFailureHandler = TemporalLambdaWorker.CreateHandler(
+ Version,
+ config =>
+ {
+ config.WorkerOptions.TaskQueue = "task-queue";
+ config.ShutdownHooks.Add(_ =>
+ {
+ hookCalls.Add("worker");
+ return Task.CompletedTask;
+ });
+ return Task.CompletedTask;
+ },
+ new TemporalLambdaWorkerHandlerOptions
+ {
+ ConnectClientAsync = _ => Task.FromResult(new object()),
+ CreateWorker = (_, _) => new FakeLambdaWorker(
+ _ => throw new InvalidOperationException("worker failed")),
+ });
+
+ await Assert.ThrowsAsync(() =>
+ workerFailureHandler(null, new FakeLambdaContext()));
+
+ Assert.Equal(new[] { "connect", "worker" }, hookCalls);
+ }
+
+ private static Func CreateCapturingHandler(
+ Action configure,
+ Action captureClientOptions) =>
+ TemporalLambdaWorker.CreateHandler(
+ Version,
+ configure,
+ new TemporalLambdaWorkerHandlerOptions
+ {
+ ConnectClientAsync = options =>
+ {
+ captureClientOptions(options);
+ return Task.FromResult(new object());
+ },
+ CreateWorker = (_, _) => new FakeLambdaWorker(_ => Task.CompletedTask),
+ });
+
+ private static string ConfigToml(string address, string nameSpace) => $@"
+[profile.default]
+address = ""{address}""
+namespace = ""{nameSpace}""
+";
+
+ private static string CreateTempDirectory()
+ {
+ var tempDir = Path.Combine(
+ Path.GetTempPath(),
+ $"TemporalLambdaWorkerTests-{Guid.NewGuid():N}");
+ Directory.CreateDirectory(tempDir);
+ return tempDir;
+ }
+
+ private static ActivityDefinition DummyActivity() =>
+ ActivityDefinition.Create(
+ "dummy",
+ typeof(Task),
+ Array.Empty(),
+ 0,
+ _ => Task.CompletedTask);
+
+ [Workflow]
+ public sealed class WorkflowWithoutVersioningBehavior
+ {
+ [WorkflowRun]
+ public Task RunAsync() => Task.CompletedTask;
+ }
+
+ private sealed class FakeLambdaWorker : ILambdaWorker
+ {
+ private readonly Func executeAsync;
+
+ public FakeLambdaWorker(Func executeAsync) =>
+ this.executeAsync = executeAsync;
+
+ public Task ExecuteAsync(CancellationToken stoppingToken) =>
+ executeAsync(stoppingToken);
+
+ public void Dispose()
+ {
+ }
+ }
+
+ private sealed class TunerPlugin : ITemporalWorkerPlugin
+ {
+ private readonly WorkerTuner tuner;
+
+ public TunerPlugin(WorkerTuner tuner) => this.tuner = tuner;
+
+ public string Name => "TunerPlugin";
+
+ public void ConfigureWorker(TemporalWorkerOptions options) => options.Tuner = tuner;
+
+ public Task RunWorkerAsync(
+ TemporalWorker worker,
+ Func> continuation,
+ CancellationToken stoppingToken) =>
+ throw new NotImplementedException();
+
+ public void ConfigureReplayer(WorkflowReplayerOptions options) =>
+ throw new NotImplementedException();
+
+ public Task> ReplayWorkflowsAsync(
+ WorkflowReplayer replayer,
+ Func>> continuation,
+ CancellationToken cancellationToken) =>
+ throw new NotImplementedException();
+
+ public IAsyncEnumerable ReplayWorkflowsAsync(
+ WorkflowReplayer replayer,
+ Func> continuation,
+ CancellationToken cancellationToken) =>
+ throw new NotImplementedException();
+ }
+
+ private sealed class VersioningPlugin : ITemporalWorkerPlugin
+ {
+ public string Name => "VersioningPlugin";
+
+ public void ConfigureWorker(TemporalWorkerOptions options)
+ {
+ options.DeploymentOptions = new WorkerDeploymentOptions(
+ new WorkerDeploymentVersion("plugin-deployment", "plugin-build"),
+ useWorkerVersioning: false)
+ {
+ DefaultVersioningBehavior = VersioningBehavior.AutoUpgrade,
+ };
+#pragma warning disable CS0618 // Verifying the Lambda helper clears legacy versioning options.
+ options.BuildId = "legacy-build";
+ options.UseWorkerVersioning = true;
+#pragma warning restore CS0618
+ }
+
+ public Task RunWorkerAsync(
+ TemporalWorker worker,
+ Func> continuation,
+ CancellationToken stoppingToken) =>
+ throw new NotImplementedException();
+
+ public void ConfigureReplayer(WorkflowReplayerOptions options) =>
+ throw new NotImplementedException();
+
+ public Task> ReplayWorkflowsAsync(
+ WorkflowReplayer replayer,
+ Func>> continuation,
+ CancellationToken cancellationToken) =>
+ throw new NotImplementedException();
+
+ public IAsyncEnumerable ReplayWorkflowsAsync(
+ WorkflowReplayer replayer,
+ Func> continuation,
+ CancellationToken cancellationToken) =>
+ throw new NotImplementedException();
+ }
+
+ private sealed class FakeLambdaContext : ILambdaContext
+ {
+ private readonly Queue remainingTimes = new();
+ private TimeSpan remainingTime = TimeSpan.FromMinutes(1);
+
+ public FakeLambdaContext()
+ {
+ }
+
+ public FakeLambdaContext(params TimeSpan[] remainingTimes)
+ {
+ foreach (var remaining in remainingTimes)
+ {
+ this.remainingTimes.Enqueue(remaining);
+ }
+ }
+
+ public CaptureLambdaLogger CaptureLogger { get; } = new();
+
+ public string AwsRequestId { get; set; } = "request-id";
+
+ public IClientContext ClientContext { get; } = null!;
+
+ public string FunctionName { get; } = "function-name";
+
+ public string FunctionVersion { get; } = "1";
+
+ public ICognitoIdentity Identity { get; } = null!;
+
+ public string InvokedFunctionArn { get; set; } = "function-arn";
+
+ public ILambdaLogger Logger => CaptureLogger;
+
+ public string LogGroupName { get; } = "log-group";
+
+ public string LogStreamName { get; } = "log-stream";
+
+ public int MemoryLimitInMB { get; } = 128;
+
+ public int RemainingTimeReadCount { get; private set; }
+
+ public TimeSpan RemainingTime
+ {
+ get
+ {
+ RemainingTimeReadCount++;
+ if (remainingTimes.Count > 0)
+ {
+ remainingTime = remainingTimes.Dequeue();
+ }
+ return remainingTime;
+ }
+
+ set
+ {
+ remainingTimes.Clear();
+ remainingTime = value;
+ }
+ }
+ }
+
+ private sealed class CaptureLambdaLogger : ILambdaLogger
+ {
+ public List Lines { get; } = new();
+
+ public void Log(string message) => Lines.Add(message);
+
+ public void LogLine(string message) => Lines.Add(message);
+ }
+}
diff --git a/tests/Temporalio.Tests/Temporalio.Tests.csproj b/tests/Temporalio.Tests/Temporalio.Tests.csproj
index f424273b..0f01826f 100644
--- a/tests/Temporalio.Tests/Temporalio.Tests.csproj
+++ b/tests/Temporalio.Tests/Temporalio.Tests.csproj
@@ -25,6 +25,8 @@
+
+
diff --git a/tests/Temporalio.Tests/packages.lock.json b/tests/Temporalio.Tests/packages.lock.json
index 0aee28c3..ff3ff8af 100644
--- a/tests/Temporalio.Tests/packages.lock.json
+++ b/tests/Temporalio.Tests/packages.lock.json
@@ -378,16 +378,6 @@
"resolved": "5.11.0",
"contentHash": "eaiXkUjC4NPcquGWzAGMXjuxvLwc6XGKMptSyOGQeT0X70BUZObuybJFZLA0OfTdueLd3US23NBPTBb6iF3V1Q=="
},
- "OpenTelemetry": {
- "type": "Transitive",
- "resolved": "1.15.3",
- "contentHash": "N0i6WjPoHPbZyms1ugbDIFAJFuGlpeExJMU/+XSL0lQRUkg/D0utFkDoLXf8Z1km5B+xVZ2GyMXXiX8qdeNmPg==",
- "dependencies": {
- "Microsoft.Extensions.Diagnostics.Abstractions": "10.0.0",
- "Microsoft.Extensions.Logging.Configuration": "10.0.0",
- "OpenTelemetry.Api.ProviderBuilderExtensions": "1.15.3"
- }
- },
"OpenTelemetry.Api.ProviderBuilderExtensions": {
"type": "Transitive",
"resolved": "1.15.3",
@@ -476,6 +466,24 @@
"NexusRpc": "[0.3.0, )"
}
},
+ "temporalio.extensions.aws.lambda": {
+ "type": "Project",
+ "dependencies": {
+ "Amazon.Lambda.Core": "[3.1.0, )",
+ "Temporalio": "[1.14.1, )"
+ }
+ },
+ "temporalio.extensions.aws.lambda.opentelemetry": {
+ "type": "Project",
+ "dependencies": {
+ "OpenTelemetry": "[1.15.3, )",
+ "OpenTelemetry.Exporter.OpenTelemetryProtocol": "[1.15.3, )",
+ "OpenTelemetry.Extensions.AWS": "[1.15.1, )",
+ "Temporalio": "[1.14.1, )",
+ "Temporalio.Extensions.Aws.Lambda": "[1.14.1, )",
+ "Temporalio.Extensions.OpenTelemetry": "[1.14.1, )"
+ }
+ },
"temporalio.extensions.diagnosticsource": {
"type": "Project",
"dependencies": {
@@ -496,6 +504,12 @@
"Temporalio": "[1.15.0, )"
}
},
+ "Amazon.Lambda.Core": {
+ "type": "CentralTransitive",
+ "requested": "[3.1.0, )",
+ "resolved": "3.1.0",
+ "contentHash": "uZZ2k5lMoB9OzPTmKkkEKpyFcnLxcb7FxtxrA3+HBg/sooTzu402iCcSk5r+N62Qokhwr4Q9cbaVJSM6Dln3aA=="
+ },
"Google.Protobuf": {
"type": "CentralTransitive",
"requested": "[3.26.1, )",
@@ -528,11 +542,40 @@
"resolved": "0.3.0",
"contentHash": "Kr+NMSZ5428AvxpzShdJcQxc9w6HT8SM6FXQMekC4K9wGpmC1m/L2pQJydpvVTwRBu3qAIYKPI37KWexF4Gtcg=="
},
+ "OpenTelemetry": {
+ "type": "CentralTransitive",
+ "requested": "[1.15.3, )",
+ "resolved": "1.15.3",
+ "contentHash": "N0i6WjPoHPbZyms1ugbDIFAJFuGlpeExJMU/+XSL0lQRUkg/D0utFkDoLXf8Z1km5B+xVZ2GyMXXiX8qdeNmPg==",
+ "dependencies": {
+ "Microsoft.Extensions.Diagnostics.Abstractions": "10.0.0",
+ "Microsoft.Extensions.Logging.Configuration": "10.0.0",
+ "OpenTelemetry.Api.ProviderBuilderExtensions": "1.15.3"
+ }
+ },
"OpenTelemetry.Api": {
"type": "CentralTransitive",
"requested": "[1.15.3, )",
"resolved": "1.15.3",
"contentHash": "fX+fkCysfPut+qCcT3bKqyX4QN9Saf4CgX8HLOHywEVD+Xr7sULtfuypITpoDysjx8R59dn/3mWhgimMH8cm/g=="
+ },
+ "OpenTelemetry.Exporter.OpenTelemetryProtocol": {
+ "type": "CentralTransitive",
+ "requested": "[1.15.3, )",
+ "resolved": "1.15.3",
+ "contentHash": "FEXJepcseTGbATiCkUfP7ipoFEYYfl/0UmmUwi0KxCPg9PaUA8ab2P1LGopK+/HExasJ1ZutFhZrN6WvUIR23g==",
+ "dependencies": {
+ "OpenTelemetry": "1.15.3"
+ }
+ },
+ "OpenTelemetry.Extensions.AWS": {
+ "type": "CentralTransitive",
+ "requested": "[1.15.1, )",
+ "resolved": "1.15.1",
+ "contentHash": "T+Vrhlv79PyG+fK6XnEtdJW9VtYp5WxSsVajplnkbuY0Q3gTyFNiLPP8tyu1qmEL19bKc9i6Wgp4JqhvupqirA==",
+ "dependencies": {
+ "OpenTelemetry": "[1.15.3, 2.0.0)"
+ }
}
}
}