Skip to content

Commit 20e5ba8

Browse files
committed
Task 3
1 parent 5e0f810 commit 20e5ba8

File tree

2 files changed

+297
-0
lines changed

2 files changed

+297
-0
lines changed
Lines changed: 109 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,109 @@
1+
/*
2+
* Copyright 2019 Amazon.com, Inc. or its affiliates. All Rights Reserved.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License").
5+
* You may not use this file except in compliance with the License.
6+
* A copy of the License is located at
7+
*
8+
* http://aws.amazon.com/apache2.0
9+
*
10+
* or in the "license" file accompanying this file. This file is distributed
11+
* on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either
12+
* express or implied. See the License for the specific language governing
13+
* permissions and limitations under the License.
14+
*/
15+
16+
using System;
17+
using System.Threading;
18+
19+
namespace Amazon.Lambda.RuntimeSupport
20+
{
21+
/// <summary>
22+
/// Factory for creating streaming responses in AWS Lambda functions.
23+
/// Call CreateStream() within your handler to opt into response streaming for that invocation.
24+
/// </summary>
25+
public static class ResponseStreamFactory
26+
{
27+
// For on-demand mode (single invocation at a time)
28+
private static ResponseStreamContext _onDemandContext;
29+
30+
// For multi-concurrency mode (multiple concurrent invocations)
31+
private static readonly AsyncLocal<ResponseStreamContext> _asyncLocalContext = new AsyncLocal<ResponseStreamContext>();
32+
33+
/// <summary>
34+
/// Creates a streaming response for the current invocation.
35+
/// Can only be called once per invocation.
36+
/// </summary>
37+
/// <returns>An IResponseStream for writing response data.</returns>
38+
/// <exception cref="InvalidOperationException">Thrown if called outside an invocation context.</exception>
39+
/// <exception cref="InvalidOperationException">Thrown if called more than once per invocation.</exception>
40+
public static IResponseStream CreateStream()
41+
{
42+
var context = GetCurrentContext();
43+
44+
if (context == null)
45+
{
46+
throw new InvalidOperationException(
47+
"ResponseStreamFactory.CreateStream() can only be called within a Lambda handler invocation.");
48+
}
49+
50+
if (context.StreamCreated)
51+
{
52+
throw new InvalidOperationException(
53+
"ResponseStreamFactory.CreateStream() can only be called once per invocation.");
54+
}
55+
56+
var stream = new ResponseStream(context.MaxResponseSize);
57+
context.Stream = stream;
58+
context.StreamCreated = true;
59+
60+
return stream;
61+
}
62+
63+
// Internal methods for LambdaBootstrap to manage state
64+
65+
internal static void InitializeInvocation(string awsRequestId, long maxResponseSize, bool isMultiConcurrency)
66+
{
67+
var context = new ResponseStreamContext
68+
{
69+
AwsRequestId = awsRequestId,
70+
MaxResponseSize = maxResponseSize,
71+
StreamCreated = false,
72+
Stream = null
73+
};
74+
75+
if (isMultiConcurrency)
76+
{
77+
_asyncLocalContext.Value = context;
78+
}
79+
else
80+
{
81+
_onDemandContext = context;
82+
}
83+
}
84+
85+
internal static ResponseStream GetStreamIfCreated(bool isMultiConcurrency)
86+
{
87+
var context = isMultiConcurrency ? _asyncLocalContext.Value : _onDemandContext;
88+
return context?.Stream;
89+
}
90+
91+
internal static void CleanupInvocation(bool isMultiConcurrency)
92+
{
93+
if (isMultiConcurrency)
94+
{
95+
_asyncLocalContext.Value = null;
96+
}
97+
else
98+
{
99+
_onDemandContext = null;
100+
}
101+
}
102+
103+
private static ResponseStreamContext GetCurrentContext()
104+
{
105+
// Check multi-concurrency first (AsyncLocal), then on-demand
106+
return _asyncLocalContext.Value ?? _onDemandContext;
107+
}
108+
}
109+
}
Lines changed: 188 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,188 @@
1+
/*
2+
* Copyright 2019 Amazon.com, Inc. or its affiliates. All Rights Reserved.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License").
5+
* You may not use this file except in compliance with the License.
6+
* A copy of the License is located at
7+
*
8+
* http://aws.amazon.com/apache2.0
9+
*
10+
* or in the "license" file accompanying this file. This file is distributed
11+
* on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either
12+
* express or implied. See the License for the specific language governing
13+
* permissions and limitations under the License.
14+
*/
15+
16+
using System;
17+
using System.Threading.Tasks;
18+
using Xunit;
19+
20+
namespace Amazon.Lambda.RuntimeSupport.UnitTests
21+
{
22+
public class ResponseStreamFactoryTests : IDisposable
23+
{
24+
private const long MaxResponseSize = 20 * 1024 * 1024;
25+
26+
public void Dispose()
27+
{
28+
// Clean up both modes to avoid test pollution
29+
ResponseStreamFactory.CleanupInvocation(isMultiConcurrency: false);
30+
ResponseStreamFactory.CleanupInvocation(isMultiConcurrency: true);
31+
}
32+
33+
// --- Task 3.3: CreateStream tests ---
34+
35+
/// <summary>
36+
/// Property 1: CreateStream Returns Valid Stream - on-demand mode.
37+
/// Validates: Requirements 1.3, 2.2, 2.3
38+
/// </summary>
39+
[Fact]
40+
public void CreateStream_OnDemandMode_ReturnsValidStream()
41+
{
42+
ResponseStreamFactory.InitializeInvocation("req-1", MaxResponseSize, isMultiConcurrency: false);
43+
44+
var stream = ResponseStreamFactory.CreateStream();
45+
46+
Assert.NotNull(stream);
47+
Assert.IsAssignableFrom<IResponseStream>(stream);
48+
}
49+
50+
/// <summary>
51+
/// Property 1: CreateStream Returns Valid Stream - multi-concurrency mode.
52+
/// Validates: Requirements 1.3, 2.2, 2.3
53+
/// </summary>
54+
[Fact]
55+
public void CreateStream_MultiConcurrencyMode_ReturnsValidStream()
56+
{
57+
ResponseStreamFactory.InitializeInvocation("req-2", MaxResponseSize, isMultiConcurrency: true);
58+
59+
var stream = ResponseStreamFactory.CreateStream();
60+
61+
Assert.NotNull(stream);
62+
Assert.IsAssignableFrom<IResponseStream>(stream);
63+
}
64+
65+
/// <summary>
66+
/// Property 4: Single Stream Per Invocation - calling CreateStream twice throws.
67+
/// Validates: Requirements 2.5, 2.6
68+
/// </summary>
69+
[Fact]
70+
public void CreateStream_CalledTwice_ThrowsInvalidOperationException()
71+
{
72+
ResponseStreamFactory.InitializeInvocation("req-3", MaxResponseSize, isMultiConcurrency: false);
73+
ResponseStreamFactory.CreateStream();
74+
75+
Assert.Throws<InvalidOperationException>(() => ResponseStreamFactory.CreateStream());
76+
}
77+
78+
[Fact]
79+
public void CreateStream_OutsideInvocationContext_ThrowsInvalidOperationException()
80+
{
81+
// No InitializeInvocation called
82+
Assert.Throws<InvalidOperationException>(() => ResponseStreamFactory.CreateStream());
83+
}
84+
85+
// --- Task 3.5: Internal methods tests ---
86+
87+
[Fact]
88+
public void InitializeInvocation_OnDemand_SetsUpContext()
89+
{
90+
ResponseStreamFactory.InitializeInvocation("req-4", MaxResponseSize, isMultiConcurrency: false);
91+
92+
// GetStreamIfCreated should return null since CreateStream hasn't been called
93+
Assert.Null(ResponseStreamFactory.GetStreamIfCreated(isMultiConcurrency: false));
94+
95+
// But CreateStream should work (proving context was set up)
96+
var stream = ResponseStreamFactory.CreateStream();
97+
Assert.NotNull(stream);
98+
}
99+
100+
[Fact]
101+
public void InitializeInvocation_MultiConcurrency_SetsUpContext()
102+
{
103+
ResponseStreamFactory.InitializeInvocation("req-5", MaxResponseSize, isMultiConcurrency: true);
104+
105+
Assert.Null(ResponseStreamFactory.GetStreamIfCreated(isMultiConcurrency: true));
106+
107+
var stream = ResponseStreamFactory.CreateStream();
108+
Assert.NotNull(stream);
109+
}
110+
111+
[Fact]
112+
public void GetStreamIfCreated_AfterCreateStream_ReturnsStream()
113+
{
114+
ResponseStreamFactory.InitializeInvocation("req-6", MaxResponseSize, isMultiConcurrency: false);
115+
var created = ResponseStreamFactory.CreateStream();
116+
117+
var retrieved = ResponseStreamFactory.GetStreamIfCreated(isMultiConcurrency: false);
118+
119+
Assert.NotNull(retrieved);
120+
}
121+
122+
[Fact]
123+
public void GetStreamIfCreated_NoContext_ReturnsNull()
124+
{
125+
Assert.Null(ResponseStreamFactory.GetStreamIfCreated(isMultiConcurrency: false));
126+
}
127+
128+
[Fact]
129+
public void CleanupInvocation_ClearsState()
130+
{
131+
ResponseStreamFactory.InitializeInvocation("req-7", MaxResponseSize, isMultiConcurrency: false);
132+
ResponseStreamFactory.CreateStream();
133+
134+
ResponseStreamFactory.CleanupInvocation(isMultiConcurrency: false);
135+
136+
Assert.Null(ResponseStreamFactory.GetStreamIfCreated(isMultiConcurrency: false));
137+
Assert.Throws<InvalidOperationException>(() => ResponseStreamFactory.CreateStream());
138+
}
139+
140+
/// <summary>
141+
/// Property 16: State Isolation Between Invocations - state from one invocation doesn't leak to the next.
142+
/// Validates: Requirements 6.5, 8.9
143+
/// </summary>
144+
[Fact]
145+
public void StateIsolation_SequentialInvocations_NoLeakage()
146+
{
147+
// First invocation - streaming
148+
ResponseStreamFactory.InitializeInvocation("req-8a", MaxResponseSize, isMultiConcurrency: false);
149+
var stream1 = ResponseStreamFactory.CreateStream();
150+
Assert.NotNull(stream1);
151+
ResponseStreamFactory.CleanupInvocation(isMultiConcurrency: false);
152+
153+
// Second invocation - should start fresh
154+
ResponseStreamFactory.InitializeInvocation("req-8b", MaxResponseSize, isMultiConcurrency: false);
155+
Assert.Null(ResponseStreamFactory.GetStreamIfCreated(isMultiConcurrency: false));
156+
157+
// Should be able to create a new stream
158+
var stream2 = ResponseStreamFactory.CreateStream();
159+
Assert.NotNull(stream2);
160+
ResponseStreamFactory.CleanupInvocation(isMultiConcurrency: false);
161+
}
162+
163+
/// <summary>
164+
/// Property 16: State Isolation - multi-concurrency mode uses AsyncLocal.
165+
/// Validates: Requirements 2.9, 2.10
166+
/// </summary>
167+
[Fact]
168+
public async Task StateIsolation_MultiConcurrency_UsesAsyncLocal()
169+
{
170+
// Initialize in multi-concurrency mode on main thread
171+
ResponseStreamFactory.InitializeInvocation("req-9", MaxResponseSize, isMultiConcurrency: true);
172+
var stream = ResponseStreamFactory.CreateStream();
173+
Assert.NotNull(stream);
174+
175+
// A separate task should not see the main thread's context
176+
// (AsyncLocal flows to child tasks, but a fresh Task.Run with new initialization should override)
177+
bool childSawNull = false;
178+
await Task.Run(() =>
179+
{
180+
// Clean up the flowed context first
181+
ResponseStreamFactory.CleanupInvocation(isMultiConcurrency: true);
182+
childSawNull = ResponseStreamFactory.GetStreamIfCreated(isMultiConcurrency: true) == null;
183+
});
184+
185+
Assert.True(childSawNull);
186+
}
187+
}
188+
}

0 commit comments

Comments
 (0)