diff --git a/Tools/LambdaTestTool-v2/src/Amazon.Lambda.TestTool/Components/Pages/Home.razor b/Tools/LambdaTestTool-v2/src/Amazon.Lambda.TestTool/Components/Pages/Home.razor index c12c34409..b072c3ef0 100644 --- a/Tools/LambdaTestTool-v2/src/Amazon.Lambda.TestTool/Components/Pages/Home.razor +++ b/Tools/LambdaTestTool-v2/src/Amazon.Lambda.TestTool/Components/Pages/Home.razor @@ -1,4 +1,4 @@ -@page "/" +@page "/" @using Amazon.Lambda.TestTool.Commands @using Amazon.Lambda.TestTool.Services @@ -9,7 +9,7 @@ @using Microsoft.AspNetCore.Http; @inject IHttpContextAccessor HttpContextAccessor -@inject IRuntimeApiDataStore RuntimeApiModel +@inject IRuntimeApiDataStoreManager DataStoreManager @inject IDirectoryManager DirectoryManager Lambda Function Tester @@ -62,33 +62,33 @@

Active Event:

- @if (RuntimeApiModel.ActiveEvent == null) + @if (DataStore.ActiveEvent == null) {

No active event

} else {
-
+
@((MarkupString)RebootIcon)
-

Request ID: @RuntimeApiModel.ActiveEvent.AwsRequestId

-

Status: @RuntimeApiModel.ActiveEvent.EventStatus

-

Last Updated: @RuntimeApiModel.ActiveEvent.LastUpdated

-

Event JSON:@CreateSnippet(RuntimeApiModel.ActiveEvent.EventJson)

- @if (RuntimeApiModel.ActiveEvent.EventStatus == EventContainer.Status.Failure) +

Request ID: @DataStore.ActiveEvent.AwsRequestId

+

Status: @DataStore.ActiveEvent.EventStatus

+

Last Updated: @DataStore.ActiveEvent.LastUpdated

+

Event JSON:@CreateSnippet(DataStore.ActiveEvent.EventJson)

+ @if (DataStore.ActiveEvent.EventStatus == EventContainer.Status.Failure) { -

Error Type: @RuntimeApiModel.ActiveEvent.ErrorType

+

Error Type: @DataStore.ActiveEvent.ErrorType

Error Response: -

@RuntimeApiModel.ActiveEvent.ErrorResponse
+
@DataStore.ActiveEvent.ErrorResponse

} else {

Response: -

@Utils.TryPrettyPrintJson(RuntimeApiModel.ActiveEvent.Response)
+
@Utils.TryPrettyPrintJson(DataStore.ActiveEvent.Response)

}
@@ -101,7 +101,7 @@

Queued Events:

- @foreach (var evnt in @RuntimeApiModel.QueuedEvents) + @foreach (var evnt in @DataStore.QueuedEvents) {
@@ -123,7 +123,7 @@

Executed Events:

- @foreach (var evnt in @RuntimeApiModel.ExecutedEvents.OrderByDescending(x => x.LastUpdated)) + @foreach (var evnt in @DataStore.ExecutedEvents.OrderByDescending(x => x.LastUpdated)) {
@@ -189,6 +189,8 @@ private IDictionary> SampleRequests { get; set; } = new Dictionary>(); + private IRuntimeApiDataStore DataStore => DataStoreManager.GetLambdaRuntimeDataStore(LambdaRuntimeApi.DefaultFunctionName); + string? _selectedSampleRequestName; string? SelectedSampleRequestName { @@ -218,19 +220,19 @@ protected override void OnInitialized() { - RuntimeApiModel.StateChange += RuntimeApiModelOnStateChange; + DataStore.StateChange += DataStoreOnStateChange; SampleRequestManager = new SampleRequestManager(DirectoryManager.GetCurrentDirectory()); SampleRequests = SampleRequestManager.GetSampleRequests(); } - private void RuntimeApiModelOnStateChange(object? sender, EventArgs e) + private void DataStoreOnStateChange(object? sender, EventArgs e) { InvokeAsync(this.StateHasChanged); } void OnAddEventClick() { - RuntimeApiModel.QueueEvent(FunctionInput); + DataStore.QueueEvent(FunctionInput, false); FunctionInput = ""; SelectedSampleRequestName = NoSampleSelectedId; StateHasChanged(); @@ -238,38 +240,38 @@ void OnClearQueued() { - RuntimeApiModel.ClearQueued(); + DataStore.ClearQueued(); StateHasChanged(); } void OnClearExecuted() { - RuntimeApiModel.ClearExecuted(); + DataStore.ClearExecuted(); StateHasChanged(); } void OnRequeue(string awsRequestId) { EventContainer? evnt = null; - if (string.Equals(RuntimeApiModel.ActiveEvent?.AwsRequestId, awsRequestId)) + if (string.Equals(DataStore.ActiveEvent?.AwsRequestId, awsRequestId)) { - evnt = RuntimeApiModel.ActiveEvent; + evnt = DataStore.ActiveEvent; } else { - evnt = RuntimeApiModel.ExecutedEvents.FirstOrDefault(x => string.Equals(x.AwsRequestId, awsRequestId)); + evnt = DataStore.ExecutedEvents.FirstOrDefault(x => string.Equals(x.AwsRequestId, awsRequestId)); } if (evnt == null) return; - RuntimeApiModel.QueueEvent(evnt.EventJson); + DataStore.QueueEvent(evnt.EventJson, false); StateHasChanged(); } void OnDeleteEvent(string awsRequestId) { - RuntimeApiModel.DeleteEvent(awsRequestId); + DataStore.DeleteEvent(awsRequestId); StateHasChanged(); } diff --git a/Tools/LambdaTestTool-v2/src/Amazon.Lambda.TestTool/Models/EventContainer.cs b/Tools/LambdaTestTool-v2/src/Amazon.Lambda.TestTool/Models/EventContainer.cs index bf7da1603..366cb2a17 100644 --- a/Tools/LambdaTestTool-v2/src/Amazon.Lambda.TestTool/Models/EventContainer.cs +++ b/Tools/LambdaTestTool-v2/src/Amazon.Lambda.TestTool/Models/EventContainer.cs @@ -1,11 +1,11 @@ -// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. // SPDX-License-Identifier: Apache-2.0 using Amazon.Lambda.TestTool.Services; namespace Amazon.Lambda.TestTool.Models; -public class EventContainer +public class EventContainer : IDisposable { public enum Status { Queued, Executing, Success, Failure } @@ -21,6 +21,9 @@ public enum Status { Queued, Executing, Success, Failure } public DateTime LastUpdated { get; private set; } private Status _status = Status.Queued; + + private ManualResetEventSlim? _resetEvent; + public Status EventStatus { get => _status; @@ -33,12 +36,17 @@ public Status EventStatus private readonly RuntimeApiDataStore _dataStore; - public EventContainer(RuntimeApiDataStore dataStore, int eventCount, string eventJson) + public EventContainer(RuntimeApiDataStore dataStore, int eventCount, string eventJson, bool isRequestResponseMode) { LastUpdated = DateTime.Now; _dataStore = dataStore; AwsRequestId = eventCount.ToString("D12"); EventJson = eventJson; + + if (isRequestResponseMode) + { + _resetEvent = new ManualResetEventSlim(false); + } } public string FunctionArn @@ -51,6 +59,12 @@ public void ReportSuccessResponse(string response) LastUpdated = DateTime.Now; Response = response; EventStatus = Status.Success; + + if (_resetEvent != null) + { + _resetEvent.Set(); + } + _dataStore.RaiseStateChanged(); } @@ -60,6 +74,50 @@ public void ReportErrorResponse(string errorType, string errorBody) ErrorType = errorType; ErrorResponse = errorBody; EventStatus = Status.Failure; + + if (_resetEvent != null) + { + _resetEvent.Set(); + } + _dataStore.RaiseStateChanged(); } + + public bool WaitForCompletion() + { + if (_resetEvent == null) + { + return false; + } + + // The 15 minutes is a fail safe so we at some point we unblock the thread. It is intentionally + // long to give the user time to debug the Lambda function. + return _resetEvent.Wait(TimeSpan.FromMinutes(15)); + } + + public void Dispose() + { + Dispose(true); + GC.SuppressFinalize(this); + } + + private bool _disposed = false; + protected virtual void Dispose(bool disposing) + { + if (_disposed) + { + return; + } + + if (disposing) + { + if (_resetEvent != null) + { + _resetEvent.Dispose(); + _resetEvent = null; + } + } + + _disposed = true; + } } diff --git a/Tools/LambdaTestTool-v2/src/Amazon.Lambda.TestTool/Processes/TestToolProcess.cs b/Tools/LambdaTestTool-v2/src/Amazon.Lambda.TestTool/Processes/TestToolProcess.cs index 2fd66beaf..d78027cb8 100644 --- a/Tools/LambdaTestTool-v2/src/Amazon.Lambda.TestTool/Processes/TestToolProcess.cs +++ b/Tools/LambdaTestTool-v2/src/Amazon.Lambda.TestTool/Processes/TestToolProcess.cs @@ -1,4 +1,4 @@ -// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. // SPDX-License-Identifier: Apache-2.0 using Amazon.Lambda.TestTool.Commands.Settings; @@ -37,7 +37,7 @@ public static TestToolProcess Startup(RunCommandSettings settings, CancellationT { var builder = WebApplication.CreateBuilder(); - builder.Services.AddSingleton(); + builder.Services.AddSingleton(); // Add services to the container. builder.Services.AddRazorComponents() @@ -65,7 +65,7 @@ public static TestToolProcess Startup(RunCommandSettings settings, CancellationT app.MapRazorComponents() .AddInteractiveServerRenderMode(); - _ = new LambdaRuntimeApi(app, app.Services.GetService()!); + LambdaRuntimeApi.SetupLambdaRuntimeApiEndpoints(app); var runTask = app.RunAsync(cancellationToken); diff --git a/Tools/LambdaTestTool-v2/src/Amazon.Lambda.TestTool/Services/LambdaRuntimeAPI.cs b/Tools/LambdaTestTool-v2/src/Amazon.Lambda.TestTool/Services/LambdaRuntimeAPI.cs index 3fc0ccfcc..fc088d186 100644 --- a/Tools/LambdaTestTool-v2/src/Amazon.Lambda.TestTool/Services/LambdaRuntimeAPI.cs +++ b/Tools/LambdaTestTool-v2/src/Amazon.Lambda.TestTool/Services/LambdaRuntimeAPI.cs @@ -1,28 +1,24 @@ -// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. // SPDX-License-Identifier: Apache-2.0 +using System.Collections.Concurrent; using System.Text; using Amazon.Lambda.TestTool.Models; using Microsoft.AspNetCore.Mvc; -// TODO: -// Make IRuntimeApiDataStore separate the events per function name -// When PostEvent being syncronous when X-Amz-Invocation-Type header is not set or set to RequestResponse. -// https://docs.aws.amazon.com/lambda/latest/api/API_Invoke.html#lambda-Invoke-request-InvocationType - - namespace Amazon.Lambda.TestTool.Services; public class LambdaRuntimeApi { - private const string DefaultFunctionName = "__DefaultFunction__"; + internal const string DefaultFunctionName = "__DefaultFunction__"; private const string HeaderBreak = "-----------------------------------"; - private readonly IRuntimeApiDataStore _runtimeApiDataStore; + private readonly IRuntimeApiDataStoreManager _runtimeApiDataStoreManager; - public LambdaRuntimeApi(WebApplication app, IRuntimeApiDataStore runtimeApiDataStore) + private LambdaRuntimeApi(WebApplication app) { - _runtimeApiDataStore = runtimeApiDataStore; + _runtimeApiDataStoreManager = app.Services.GetRequiredService(); + app.MapPost("/2015-03-31/functions/function/invocations", (Delegate)PostEventDefaultFunction); app.MapPost("/2015-03-31/functions/{functionName}/invocations", PostEvent); @@ -39,18 +35,51 @@ public LambdaRuntimeApi(WebApplication app, IRuntimeApiDataStore runtimeApiDataS app.MapPost("/{functionName}/2018-06-01/runtime/invocation/{awsRequestId}/error", PostError); } - public Task PostEventDefaultFunction(HttpContext ctx) + public static void SetupLambdaRuntimeApiEndpoints(WebApplication app) + { + _ = new LambdaRuntimeApi(app); + } + + public Task PostEventDefaultFunction(HttpContext ctx) { return PostEvent(ctx, DefaultFunctionName); } - public async Task PostEvent(HttpContext ctx, string functionName) + public async Task PostEvent(HttpContext ctx, string functionName) { + var runtimeDataStore = _runtimeApiDataStoreManager.GetLambdaRuntimeDataStore(functionName); + + // RequestResponse mode is the default when invoking with the Lambda service client. + var isRequestResponseMode = true; + if (ctx.Request.Headers.TryGetValue("X-Amz-Invocation-Type", out var invocationType)) + { + isRequestResponseMode = string.Equals(invocationType, "RequestResponse", StringComparison.InvariantCulture); + } + using var reader = new StreamReader(ctx.Request.Body); var testEvent = await reader.ReadToEndAsync(); - _runtimeApiDataStore.QueueEvent(testEvent); + var evnt = runtimeDataStore.QueueEvent(testEvent, isRequestResponseMode); - return Results.Accepted(); + if (isRequestResponseMode) + { + evnt.WaitForCompletion(); + var result = Results.Ok(evnt.Response); + ctx.Response.StatusCode = 200; + + if (!string.IsNullOrEmpty(evnt.Response)) + { + var responseData = Encoding.UTF8.GetBytes(evnt.Response); + ctx.Response.Headers.ContentType = "application/json"; + ctx.Response.Headers.ContentLength = responseData.Length; + + await ctx.Response.Body.WriteAsync(responseData); + } + evnt.Dispose(); + } + else + { + ctx.Response.StatusCode = 202; + } } public Task GetNextInvocationDefaultFunction(HttpContext ctx) @@ -60,8 +89,10 @@ public Task GetNextInvocationDefaultFunction(HttpContext ctx) public async Task GetNextInvocation(HttpContext ctx, string functionName) { + var runtimeDataStore = _runtimeApiDataStoreManager.GetLambdaRuntimeDataStore(functionName); + EventContainer? activeEvent; - while (!_runtimeApiDataStore.TryActivateEvent(out activeEvent)) + while (!runtimeDataStore.TryActivateEvent(out activeEvent)) { await Task.Delay(TimeSpan.FromMilliseconds(100)); } @@ -72,6 +103,7 @@ public async Task GetNextInvocation(HttpContext ctx, string functionName) Console.WriteLine(HeaderBreak); Console.WriteLine($"Next invocation returned: {activeEvent.AwsRequestId}"); + ctx.Response.Headers["Lambda-Runtime-Aws-Request-Id"] = activeEvent.AwsRequestId; ctx.Response.Headers["Lambda-Runtime-Trace-Id"] = Guid.NewGuid().ToString(); ctx.Response.Headers["Lambda-Runtime-Invoked-Function-Arn"] = activeEvent.FunctionArn; @@ -109,10 +141,12 @@ public Task PostInvocationResponseDefaultFunction(HttpContext ctx, stri public async Task PostInvocationResponse(HttpContext ctx, string functionName, string awsRequestId) { + var runtimeDataStore = _runtimeApiDataStoreManager.GetLambdaRuntimeDataStore(functionName); + using var reader = new StreamReader(ctx.Request.Body); var response = await reader.ReadToEndAsync(); - _runtimeApiDataStore.ReportSuccess(awsRequestId, response); + runtimeDataStore.ReportSuccess(awsRequestId, response); Console.WriteLine(HeaderBreak); Console.WriteLine($"Response for request {awsRequestId}"); @@ -128,10 +162,12 @@ public Task PostErrorDefaultFunction(HttpContext ctx, string awsRequest public async Task PostError(HttpContext ctx, string functionName, string awsRequestId, [FromHeader(Name = "Lambda-Runtime-Function-Error-Type")] string errorType) { + var runtimeDataStore = _runtimeApiDataStoreManager.GetLambdaRuntimeDataStore(functionName); + using var reader = new StreamReader(ctx.Request.Body); var errorBody = await reader.ReadToEndAsync(); - _runtimeApiDataStore.ReportError(awsRequestId, errorType, errorBody); + runtimeDataStore.ReportError(awsRequestId, errorType, errorBody); await Console.Error.WriteLineAsync(HeaderBreak); await Console.Error.WriteLineAsync($"Request {awsRequestId} Error Type: {errorType}"); await Console.Error.WriteLineAsync(errorBody); diff --git a/Tools/LambdaTestTool-v2/src/Amazon.Lambda.TestTool/Services/RuntimeApiDataStore.cs b/Tools/LambdaTestTool-v2/src/Amazon.Lambda.TestTool/Services/RuntimeApiDataStore.cs index 7b2172b30..1329c6e35 100644 --- a/Tools/LambdaTestTool-v2/src/Amazon.Lambda.TestTool/Services/RuntimeApiDataStore.cs +++ b/Tools/LambdaTestTool-v2/src/Amazon.Lambda.TestTool/Services/RuntimeApiDataStore.cs @@ -6,43 +6,91 @@ namespace Amazon.Lambda.TestTool.Services; +/// +/// The runtime API data store is used to hold the queued, executed and active Lambda events for Lambda function. +/// public interface IRuntimeApiDataStore { - EventContainer QueueEvent(string eventBody); - + /// + /// Queue the event for a Lambda function to process. + /// + /// The Lambda event body. + /// If the event is for a request response mode thread syncronization code will be activated to all the response to be return for the request. + /// + EventContainer QueueEvent(string eventBody, bool isRequestResponseMode); + + /// + /// The list of queued events. + /// IReadOnlyList QueuedEvents { get; } + /// + /// The list of executed events. + /// IReadOnlyList ExecutedEvents { get; } + /// + /// Clear the list of queued events. + /// void ClearQueued(); + /// + /// Clear the list of executed events. + /// void ClearExecuted(); void DeleteEvent(string awsRequestId); - + /// + /// The active event a Lambda function has pulled from the queue and is currently processing. + /// EventContainer? ActiveEvent { get; } + /// + /// An event that some event or event collection has changed. This is used by the UI to + /// know when it should refresh. + /// event EventHandler? StateChange; + /// + /// Try to activate an event by grabbing the latest event from the queue. + /// + /// + /// bool TryActivateEvent(out EventContainer? activeEvent); + /// + /// Report the event was successfully processed. Used by the Lambda Runtime API when it gets + /// notification from the Lambda function. + /// + /// + /// void ReportSuccess(string awsRequestId, string response); + + /// + /// Report the processing the event failed. Used by the Lambda Runtime API when it gets + /// notification from the Lambda function. + /// + /// + /// void ReportError(string awsRequestId, string errorType, string errorBody); } +/// public class RuntimeApiDataStore : IRuntimeApiDataStore { - private IList _queuedEvents = new List(); - private IList _executedEvents = new List(); + private readonly IList _queuedEvents = new List(); + private readonly IList _executedEvents = new List(); private int _eventCounter = 1; - private object _lock = new object(); + private readonly object _lock = new object(); + /// public event EventHandler? StateChange; - public EventContainer QueueEvent(string eventBody) + /// + public EventContainer QueueEvent(string eventBody, bool isRequestResponseMode) { - var evnt = new EventContainer(this, _eventCounter++, eventBody); + var evnt = new EventContainer(this, _eventCounter++, eventBody, isRequestResponseMode); lock (_lock) { _queuedEvents.Add(evnt); @@ -52,6 +100,7 @@ public EventContainer QueueEvent(string eventBody) return evnt; } + /// public bool TryActivateEvent(out EventContainer? activeEvent) { activeEvent = null; @@ -81,8 +130,10 @@ public bool TryActivateEvent(out EventContainer? activeEvent) } } + /// public EventContainer? ActiveEvent { get; private set; } + /// public IReadOnlyList QueuedEvents { get @@ -94,6 +145,7 @@ public IReadOnlyList QueuedEvents } } + /// public IReadOnlyList ExecutedEvents { get @@ -105,6 +157,7 @@ public IReadOnlyList ExecutedEvents } } + /// public void ReportSuccess(string awsRequestId, string response) { lock(_lock) @@ -120,6 +173,7 @@ public void ReportSuccess(string awsRequestId, string response) RaiseStateChanged(); } + /// public void ReportError(string awsRequestId, string errorType, string errorBody) { lock(_lock) @@ -135,6 +189,7 @@ public void ReportError(string awsRequestId, string errorType, string errorBody) RaiseStateChanged(); } + /// public void ClearQueued() { lock(_lock) @@ -144,6 +199,7 @@ public void ClearQueued() RaiseStateChanged(); } + /// public void ClearExecuted() { lock(_lock) @@ -153,6 +209,7 @@ public void ClearExecuted() RaiseStateChanged(); } + /// public void DeleteEvent(string awsRequestId) { lock(_lock) diff --git a/Tools/LambdaTestTool-v2/src/Amazon.Lambda.TestTool/Services/RuntimeApiDataStoreManager.cs b/Tools/LambdaTestTool-v2/src/Amazon.Lambda.TestTool/Services/RuntimeApiDataStoreManager.cs new file mode 100644 index 000000000..c78377ecf --- /dev/null +++ b/Tools/LambdaTestTool-v2/src/Amazon.Lambda.TestTool/Services/RuntimeApiDataStoreManager.cs @@ -0,0 +1,41 @@ +using System.Collections.Concurrent; + +namespace Amazon.Lambda.TestTool.Services; + +/// +/// The runtime api data store manager is used to partition the Lambda events per Lambda function using the Lambda runtime api. +/// +public interface IRuntimeApiDataStoreManager +{ + /// + /// Gets the IRuntimeApiDataStore for the Lambda function + /// + /// + /// + IRuntimeApiDataStore GetLambdaRuntimeDataStore(string functionName); + + /// + /// Gets the list of lambda functions. For each Lambda function that calls into Lambda runtime api the GetLambdaRuntimeDataStore is + /// called creating a data store. This method returns that list of functions that have had a data store created. + /// + /// + string[] GetListOfFunctionNames(); +} + +/// +internal class RuntimeApiDataStoreManager : IRuntimeApiDataStoreManager +{ + private readonly ConcurrentDictionary _dataStores = new ConcurrentDictionary(); + + /// + public IRuntimeApiDataStore GetLambdaRuntimeDataStore(string functionName) + { + return _dataStores.GetOrAdd(functionName, name => new RuntimeApiDataStore()); + } + + /// + public string[] GetListOfFunctionNames() + { + return _dataStores.Keys.ToArray(); + } +} diff --git a/Tools/LambdaTestTool-v2/tests/Amazon.Lambda.TestTool.UnitTests/Properties/AssemblyInfo.cs b/Tools/LambdaTestTool-v2/tests/Amazon.Lambda.TestTool.UnitTests/Properties/AssemblyInfo.cs new file mode 100644 index 000000000..a821f1dfb --- /dev/null +++ b/Tools/LambdaTestTool-v2/tests/Amazon.Lambda.TestTool.UnitTests/Properties/AssemblyInfo.cs @@ -0,0 +1,2 @@ + +[assembly: Xunit.CollectionBehavior(DisableTestParallelization = true)] \ No newline at end of file diff --git a/Tools/LambdaTestTool-v2/tests/Amazon.Lambda.TestTool.UnitTests/RuntimeApiTests.cs b/Tools/LambdaTestTool-v2/tests/Amazon.Lambda.TestTool.UnitTests/RuntimeApiTests.cs index d9346e0c2..8befe6e0d 100644 --- a/Tools/LambdaTestTool-v2/tests/Amazon.Lambda.TestTool.UnitTests/RuntimeApiTests.cs +++ b/Tools/LambdaTestTool-v2/tests/Amazon.Lambda.TestTool.UnitTests/RuntimeApiTests.cs @@ -9,20 +9,17 @@ using Amazon.Lambda.Core; using Amazon.Lambda.TestTool.Processes; using Amazon.Lambda.TestTool.Commands.Settings; +using Microsoft.Extensions.DependencyInjection; namespace Amazon.Lambda.TestTool.UnitTests; public class RuntimeApiTests { - public RuntimeApiTests() - { - // Set this environment variable so anytime we start the LambdaBootstrap from RuntimeSupport it exists after processing an event. - System.Environment.SetEnvironmentVariable("AWS_LAMBDA_DOTNET_DEBUG_RUN_ONCE", "true"); - } - [Fact] public async Task AddEventToDataStore() { + const string functionName = "FunctionFoo"; + var cancellationTokenSource = new CancellationTokenSource(); var options = new RunCommandSettings(); options.Port = 9000; @@ -32,15 +29,19 @@ public async Task AddEventToDataStore() var lambdaClient = ConstructLambdaServiceClient(testToolProcess.ServiceUrl); var invokeFunction = new InvokeRequest { - FunctionName = "FunctionFoo", - Payload = "\"hello\"" + FunctionName = functionName, + Payload = "\"hello\"", + InvocationType = InvocationType.Event }; await lambdaClient.InvokeAsync(invokeFunction); - var dataStore = testToolProcess.Services.GetService(typeof(IRuntimeApiDataStore)) as IRuntimeApiDataStore; + var dataStoreManager = testToolProcess.Services.GetRequiredService(); + var dataStore = dataStoreManager.GetLambdaRuntimeDataStore(functionName); Assert.NotNull(dataStore); Assert.Single(dataStore.QueuedEvents); + Assert.Single(dataStoreManager.GetListOfFunctionNames()); + Assert.Equal(functionName, dataStoreManager.GetListOfFunctionNames().First()); var handlerCalled = false; var handler = (string input, ILambdaContext context) => @@ -49,25 +50,80 @@ public async Task AddEventToDataStore() return input.ToUpper(); }; - System.Environment.SetEnvironmentVariable("AWS_LAMBDA_RUNTIME_API", $"{options.Host}:{options.Port}"); - await LambdaBootstrapBuilder.Create(handler, new DefaultLambdaJsonSerializer()) + System.Environment.SetEnvironmentVariable("AWS_LAMBDA_RUNTIME_API", $"{options.Host}:{options.Port}/{functionName}"); + _ = LambdaBootstrapBuilder.Create(handler, new DefaultLambdaJsonSerializer()) .Build() - .RunAsync(); + .RunAsync(cancellationTokenSource.Token); + await Task.Delay(2000); Assert.True(handlerCalled); } finally { await cancellationTokenSource.CancelAsync(); - await testToolProcess.RunningTask; } } + [Fact] + public async Task InvokeRequestResponse() + { + const string functionName = "FunctionFoo"; + + var cancellationTokenSource = new CancellationTokenSource(); + var options = new RunCommandSettings(); + options.Port = 9001; + var testToolProcess = TestToolProcess.Startup(options, cancellationTokenSource.Token); + try + { + var handler = (string input, ILambdaContext context) => + { + Thread.Sleep(1000); // Add a sleep to prove the LambdaRuntimeApi waited for the completion. + return input.ToUpper(); + }; + + System.Environment.SetEnvironmentVariable("AWS_LAMBDA_RUNTIME_API", $"{options.Host}:{options.Port}/{functionName}"); + _ = LambdaBootstrapBuilder.Create(handler, new DefaultLambdaJsonSerializer()) + .Build() + .RunAsync(cancellationTokenSource.Token); + + var lambdaClient = ConstructLambdaServiceClient(testToolProcess.ServiceUrl); + + // Test with relying on the default value of InvocationType + var invokeFunction = new InvokeRequest + { + FunctionName = functionName, + Payload = "\"hello\"" + }; + + var response = await lambdaClient.InvokeAsync(invokeFunction); + var responsePayloadString = System.Text.Encoding.Default.GetString(response.Payload.ToArray()); + Assert.Equal("\"HELLO\"", responsePayloadString); + + // Test with InvocationType explicilty set + invokeFunction = new InvokeRequest + { + FunctionName = functionName, + Payload = "\"hello\"", + InvocationType = InvocationType.RequestResponse + }; + + response = await lambdaClient.InvokeAsync(invokeFunction); + responsePayloadString = System.Text.Encoding.Default.GetString(response.Payload.ToArray()); + Assert.Equal("\"HELLO\"", responsePayloadString); + } + finally + { + await cancellationTokenSource.CancelAsync(); + } + + } + private IAmazonLambda ConstructLambdaServiceClient(string url) { var config = new AmazonLambdaConfig { - ServiceURL = url + ServiceURL = url, + MaxErrorRetry = 0 }; // We don't need real credentials because we are not calling the real Lambda service.