Skip to content

Commit 6d5a12c

Browse files
authored
Merge pull request #834 from hjgraca/feature/coldstart-provisioned-concurrency
chore: Cold start with provisioned concurrency support
2 parents b029cc5 + 6eba427 commit 6d5a12c

File tree

11 files changed

+216
-32
lines changed

11 files changed

+216
-32
lines changed

libraries/src/AWS.Lambda.Powertools.Common/Core/Constants.cs

+8
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,14 @@ namespace AWS.Lambda.Powertools.Common;
2020
/// </summary>
2121
internal static class Constants
2222
{
23+
/// <summary>
24+
/// Constant for AWS_LAMBDA_INITIALIZATION_TYPE environment variable
25+
/// This is used to determine if the Lambda function is running in provisioned concurrency mode
26+
/// or not. If the value is "provisioned-concurrency", it indicates that the function is running in provisioned
27+
/// concurrency mode. Otherwise, it is running in standard mode.
28+
/// </summary>
29+
internal const string AWSInitializationTypeEnv = "AWS_LAMBDA_INITIALIZATION_TYPE";
30+
2331
/// <summary>
2432
/// Constant for POWERTOOLS_SERVICE_NAME environment variable
2533
/// </summary>

libraries/src/AWS.Lambda.Powertools.Common/Core/IPowertoolsConfigurations.cs

+11
Original file line numberDiff line numberDiff line change
@@ -167,4 +167,15 @@ public interface IPowertoolsConfigurations
167167
/// Gets a value indicating whether Metrics are disabled.
168168
/// </summary>
169169
bool MetricsDisabled { get; }
170+
171+
/// <summary>
172+
/// Indicates if the current execution is a cold start.
173+
/// </summary>
174+
bool IsColdStart { get; }
175+
176+
/// <summary>
177+
/// AWS Lambda initialization type.
178+
/// This is set to "on-demand" for on-demand Lambda functions and "provisioned-concurrency" for provisioned concurrency.
179+
/// </summary>
180+
string AwsInitializationType { get; }
170181
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,66 @@
1+
using System;
2+
using System.Threading;
3+
4+
namespace AWS.Lambda.Powertools.Common.Core;
5+
6+
/// <summary>
7+
/// Tracks Lambda lifecycle state including cold starts
8+
/// </summary>
9+
internal static class LambdaLifecycleTracker
10+
{
11+
// Static flag that's true only for the first Lambda container initialization
12+
private static bool _isFirstContainer = true;
13+
14+
// Store the cold start state for the current invocation
15+
private static readonly AsyncLocal<bool?> CurrentInvocationColdStart = new AsyncLocal<bool?>();
16+
17+
private static string _lambdaInitType;
18+
private static string LambdaInitType => _lambdaInitType ?? Environment.GetEnvironmentVariable(Constants.AWSInitializationTypeEnv);
19+
20+
/// <summary>
21+
/// Returns true if the current Lambda invocation is a cold start
22+
/// </summary>
23+
public static bool IsColdStart
24+
{
25+
get
26+
{
27+
if(LambdaInitType == "provisioned-concurrency")
28+
{
29+
// If the Lambda is provisioned concurrency, it is not a cold start
30+
return false;
31+
}
32+
33+
// Initialize the cold start state for this invocation if not already set
34+
if (!CurrentInvocationColdStart.Value.HasValue)
35+
{
36+
// Capture the container's cold start state for this entire invocation
37+
CurrentInvocationColdStart.Value = _isFirstContainer;
38+
39+
// After detecting the first invocation, mark future ones as warm
40+
if (_isFirstContainer)
41+
{
42+
_isFirstContainer = false;
43+
}
44+
}
45+
46+
// Return the cold start state for this invocation (cannot change during the invocation)
47+
return CurrentInvocationColdStart.Value ?? false;
48+
}
49+
}
50+
51+
52+
53+
/// <summary>
54+
/// Resets the cold start state for testing
55+
/// </summary>
56+
/// <param name="resetContainer">Whether to reset the container state (defaults to true)</param>
57+
internal static void Reset(bool resetContainer = true)
58+
{
59+
if (resetContainer)
60+
{
61+
_isFirstContainer = true;
62+
}
63+
CurrentInvocationColdStart.Value = null;
64+
_lambdaInitType = null;
65+
}
66+
}

libraries/src/AWS.Lambda.Powertools.Common/Core/PowertoolsConfigurations.cs

+8
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@
1414
*/
1515

1616
using System.Globalization;
17+
using AWS.Lambda.Powertools.Common.Core;
1718

1819
namespace AWS.Lambda.Powertools.Common;
1920

@@ -222,4 +223,11 @@ public void SetExecutionEnvironment<T>(T type)
222223

223224
/// <inheritdoc />
224225
public bool MetricsDisabled => GetEnvironmentVariableOrDefault(Constants.PowertoolsMetricsDisabledEnv, false);
226+
227+
/// <inheritdoc />
228+
public bool IsColdStart => LambdaLifecycleTracker.IsColdStart;
229+
230+
/// <inheritdoc />
231+
public string AwsInitializationType =>
232+
GetEnvironmentVariable(Constants.AWSInitializationTypeEnv);
225233
}

libraries/src/AWS.Lambda.Powertools.Common/InternalsVisibleTo.cs

+1
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@
1717

1818
[assembly: InternalsVisibleTo("AWS.Lambda.Powertools.Logging")]
1919
[assembly: InternalsVisibleTo("AWS.Lambda.Powertools.Metrics")]
20+
[assembly: InternalsVisibleTo("AWS.Lambda.Powertools.Tracing")]
2021
[assembly: InternalsVisibleTo("AWS.Lambda.Powertools.Idempotency")]
2122
[assembly: InternalsVisibleTo("AWS.Lambda.Powertools.Common.Tests")]
2223
[assembly: InternalsVisibleTo("AWS.Lambda.Powertools.Tracing.Tests")]

libraries/src/AWS.Lambda.Powertools.Logging/Internal/LoggingAspect.cs

+2-7
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@
2121
using System.Text.Json;
2222
using AspectInjector.Broker;
2323
using AWS.Lambda.Powertools.Common;
24+
using AWS.Lambda.Powertools.Common.Core;
2425
using AWS.Lambda.Powertools.Logging.Serializers;
2526
using Microsoft.Extensions.Logging;
2627

@@ -34,11 +35,6 @@ namespace AWS.Lambda.Powertools.Logging.Internal;
3435
[Aspect(Scope.Global, Factory = typeof(LoggingAspectFactory))]
3536
public class LoggingAspect
3637
{
37-
/// <summary>
38-
/// The is cold start
39-
/// </summary>
40-
private bool _isColdStart = true;
41-
4238
/// <summary>
4339
/// The initialize context
4440
/// </summary>
@@ -143,9 +139,8 @@ public void OnEntry(
143139
if (!_initializeContext)
144140
return;
145141

146-
Logger.AppendKey(LoggingConstants.KeyColdStart, _isColdStart);
142+
Logger.AppendKey(LoggingConstants.KeyColdStart, LambdaLifecycleTracker.IsColdStart);
147143

148-
_isColdStart = false;
149144
_initializeContext = false;
150145
_isContextInitialized = true;
151146

libraries/src/AWS.Lambda.Powertools.Metrics/Internal/MetricsAspect.cs

+3-13
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@
2020
using Amazon.Lambda.Core;
2121
using AspectInjector.Broker;
2222
using AWS.Lambda.Powertools.Common;
23+
using AWS.Lambda.Powertools.Common.Core;
2324

2425
namespace AWS.Lambda.Powertools.Metrics;
2526

@@ -30,22 +31,12 @@ namespace AWS.Lambda.Powertools.Metrics;
3031
[Aspect(Scope.Global)]
3132
public class MetricsAspect
3233
{
33-
/// <summary>
34-
/// The is cold start
35-
/// </summary>
36-
private static bool _isColdStart;
37-
3834
/// <summary>
3935
/// Gets the metrics instance.
4036
/// </summary>
4137
/// <value>The metrics instance.</value>
4238
private static IMetrics _metricsInstance;
4339

44-
static MetricsAspect()
45-
{
46-
_isColdStart = true;
47-
}
48-
4940
/// <summary>
5041
/// Runs before the execution of the method marked with the Metrics Attribute
5142
/// </summary>
@@ -89,10 +80,9 @@ public void Before(
8980
Triggers = triggers
9081
};
9182

92-
if (_isColdStart)
83+
if (LambdaLifecycleTracker.IsColdStart)
9384
{
9485
_metricsInstance.CaptureColdStartMetric(GetContext(eventArgs));
95-
_isColdStart = false;
9686
}
9787
}
9888

@@ -112,7 +102,7 @@ public void Exit()
112102
internal static void ResetForTest()
113103
{
114104
_metricsInstance = null;
115-
_isColdStart = true;
105+
LambdaLifecycleTracker.Reset();
116106
Metrics.ResetForTest();
117107
}
118108

libraries/src/AWS.Lambda.Powertools.Tracing/Internal/TracingAspect.cs

+3-9
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@
2020
using System.Threading.Tasks;
2121
using AspectInjector.Broker;
2222
using AWS.Lambda.Powertools.Common;
23+
using AWS.Lambda.Powertools.Common.Core;
2324
using AWS.Lambda.Powertools.Common.Utils;
2425

2526
namespace AWS.Lambda.Powertools.Tracing.Internal;
@@ -41,11 +42,6 @@ public class TracingAspect
4142
/// </summary>
4243
private readonly IXRayRecorder _xRayRecorder;
4344

44-
/// <summary>
45-
/// If true, then is cold start
46-
/// </summary>
47-
private static bool _isColdStart = true;
48-
4945
/// <summary>
5046
/// If true, capture annotations
5147
/// </summary>
@@ -148,16 +144,14 @@ private void BeginSegment(string segmentName, string @namespace)
148144

149145
if (_captureAnnotations)
150146
{
151-
_xRayRecorder.AddAnnotation("ColdStart", _isColdStart);
147+
_xRayRecorder.AddAnnotation("ColdStart", LambdaLifecycleTracker.IsColdStart);
152148

153149
_captureAnnotations = false;
154150
_isAnnotationsCaptured = true;
155151

156152
if (_powertoolsConfigurations.IsServiceDefined)
157153
_xRayRecorder.AddAnnotation("Service", _powertoolsConfigurations.Service);
158154
}
159-
160-
_isColdStart = false;
161155
}
162156

163157
private void HandleResponse(string name, object result, TracingCaptureMode captureMode, string @namespace)
@@ -253,7 +247,7 @@ private bool CaptureError(TracingCaptureMode captureMode)
253247

254248
internal static void ResetForTest()
255249
{
256-
_isColdStart = true;
250+
LambdaLifecycleTracker.Reset();
257251
_captureAnnotations = true;
258252
}
259253
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,105 @@
1+
using System;
2+
using AWS.Lambda.Powertools.Common.Core;
3+
using Xunit;
4+
5+
namespace AWS.Lambda.Powertools.Common.Tests;
6+
7+
public class LambdaLifecycleTrackerTests : IDisposable
8+
{
9+
public LambdaLifecycleTrackerTests()
10+
{
11+
// Reset before each test to ensure clean state
12+
LambdaLifecycleTracker.Reset();
13+
Environment.SetEnvironmentVariable(Constants.AWSInitializationTypeEnv, null);
14+
}
15+
16+
public void Dispose()
17+
{
18+
// Reset after each test
19+
LambdaLifecycleTracker.Reset();
20+
Environment.SetEnvironmentVariable(Constants.AWSInitializationTypeEnv, null);
21+
}
22+
23+
[Fact]
24+
public void IsColdStart_FirstInvocation_ReturnsTrue()
25+
{
26+
// Act
27+
var result = LambdaLifecycleTracker.IsColdStart;
28+
29+
// Assert
30+
Assert.True(result);
31+
}
32+
33+
[Fact]
34+
public void IsColdStart_SecondInvocation_ReturnsFalse()
35+
{
36+
// Arrange - first access to trigger cold start
37+
_ = LambdaLifecycleTracker.IsColdStart;
38+
39+
// Clear just the AsyncLocal value to simulate new invocation in same container
40+
LambdaLifecycleTracker.Reset(resetContainer: false);
41+
42+
// Act - second invocation on same container
43+
var result = LambdaLifecycleTracker.IsColdStart;
44+
45+
// Assert
46+
Assert.False(result);
47+
}
48+
49+
[Fact]
50+
public void IsColdStart_WithProvisionedConcurrency_ReturnsFalse()
51+
{
52+
// Arrange
53+
Environment.SetEnvironmentVariable(Constants.AWSInitializationTypeEnv, "provisioned-concurrency");
54+
55+
// Act
56+
var result = LambdaLifecycleTracker.IsColdStart;
57+
58+
// Assert
59+
Assert.False(result);
60+
}
61+
62+
[Fact]
63+
public void IsColdStart_ReturnsSameValueWithinInvocation()
64+
{
65+
// Act - access multiple times in the same invocation
66+
var firstAccess = LambdaLifecycleTracker.IsColdStart;
67+
var secondAccess = LambdaLifecycleTracker.IsColdStart;
68+
var thirdAccess = LambdaLifecycleTracker.IsColdStart;
69+
70+
// Assert
71+
Assert.True(firstAccess);
72+
Assert.Equal(firstAccess, secondAccess);
73+
Assert.Equal(firstAccess, thirdAccess);
74+
}
75+
76+
[Fact]
77+
public void Reset_ResetsState()
78+
{
79+
// Arrange
80+
_ = LambdaLifecycleTracker.IsColdStart; // First invocation
81+
82+
// Act
83+
LambdaLifecycleTracker.Reset();
84+
var result = LambdaLifecycleTracker.IsColdStart;
85+
86+
// Assert
87+
Assert.True(result); // Should be true again after reset
88+
}
89+
90+
[Fact]
91+
public void Reset_ClearsEnvironmentSetting()
92+
{
93+
// Arrange
94+
Environment.SetEnvironmentVariable(Constants.AWSInitializationTypeEnv, "provisioned-concurrency");
95+
_ = LambdaLifecycleTracker.IsColdStart; // Load the environment variable
96+
97+
// Act
98+
LambdaLifecycleTracker.Reset();
99+
Environment.SetEnvironmentVariable(Constants.AWSInitializationTypeEnv, null); // Clear the environment
100+
var result = LambdaLifecycleTracker.IsColdStart;
101+
102+
// Assert
103+
Assert.True(result); // Should be true when env var is cleared
104+
}
105+
}

libraries/tests/AWS.Lambda.Powertools.Tracing.Tests/TracingAttributeTest.cs

+6
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@
1717
using System.Linq;
1818
using System.Text;
1919
using Amazon.XRay.Recorder.Core;
20+
using AWS.Lambda.Powertools.Common.Core;
2021
using AWS.Lambda.Powertools.Tracing.Internal;
2122
using Xunit;
2223

@@ -50,6 +51,8 @@ public void OnEntry_WhenFirstCall_CapturesColdStart()
5051
var subSegmentCold = segmentCold.Subsegments[0];
5152

5253
// Warm Start Execution
54+
// Clear just the AsyncLocal value to simulate new invocation in same container
55+
LambdaLifecycleTracker.Reset(resetContainer: false);
5356
// Start segment
5457
var segmentWarm = AWSXRayRecorder.Instance.TraceContext.GetEntity();
5558
_handler.Handle();
@@ -87,6 +90,9 @@ public void OnEntry_WhenFirstCall_And_Service_Not_Set_CapturesColdStart()
8790
var subSegmentCold = segmentCold.Subsegments[0];
8891

8992
// Warm Start Execution
93+
// Clear just the AsyncLocal value to simulate new invocation in same container
94+
LambdaLifecycleTracker.Reset(resetContainer: false);
95+
9096
// Start segment
9197
var segmentWarm = AWSXRayRecorder.Instance.TraceContext.GetEntity();
9298
_handler.Handle();

0 commit comments

Comments
 (0)