/* * Copyright 2019 Amazon.com, Inc. or its affiliates. All Rights Reserved. * * Licensed under the Apache License, Version 2.0 (the "License"). * You may not use this file except in compliance with the License. * A copy of the License is located at * * http://aws.amazon.com/apache2.0 * * or in the "license" file accompanying this file. This file is distributed * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either * express or implied. See the License for the specific language governing * permissions and limitations under the License. */ using System; using System.IO; using System.Net.Http; using System.Threading; using System.Threading.Tasks; using Amazon.Lambda.RuntimeSupport.Bootstrap; using Amazon.Lambda.RuntimeSupport.Helpers; namespace Amazon.Lambda.RuntimeSupport { public delegate Task LambdaBootstrapHandler(InvocationRequest invocation); public delegate Task LambdaBootstrapInitializer(); /// /// Class to communicate with the Lambda Runtime API, handle initialization, /// and run the invoke loop for an AWS Lambda function /// public class LambdaBootstrap : IDisposable { /// /// The Lambda container freezes the process at a point where an HTTP request is in progress. /// We need to make sure we don't timeout waiting for the next invocation. /// private static readonly TimeSpan RuntimeApiHttpTimeout = TimeSpan.FromHours(12); private LambdaBootstrapInitializer _initializer; private LambdaBootstrapHandler _handler; private bool _ownsHttpClient; private InternalLogger _logger = InternalLogger.GetDefaultLogger(); private HttpClient _httpClient; internal IRuntimeApiClient Client { get; set; } /// /// Create a LambdaBootstrap that will call the given initializer and handler. /// /// The HTTP client to use with the Lambda runtime. /// Delegate called for each invocation of the Lambda function. /// Delegate called to initialize the Lambda function. If not provided the initialization step is skipped. /// public LambdaBootstrap(HttpClient httpClient, LambdaBootstrapHandler handler, LambdaBootstrapInitializer initializer = null) : this(httpClient, handler, initializer, ownsHttpClient: false) { } /// /// Create a LambdaBootstrap that will call the given initializer and handler. /// /// Delegate called for each invocation of the Lambda function. /// Delegate called to initialize the Lambda function. If not provided the initialization step is skipped. /// public LambdaBootstrap(LambdaBootstrapHandler handler, LambdaBootstrapInitializer initializer = null) : this(ConstructHttpClient(), handler, initializer, ownsHttpClient: true) { } /// /// Create a LambdaBootstrap that will call the given initializer and handler. /// /// The HandlerWrapper to call for each invocation of the Lambda function. /// Delegate called to initialize the Lambda function. If not provided the initialization step is skipped. /// public LambdaBootstrap(HandlerWrapper handlerWrapper, LambdaBootstrapInitializer initializer = null) : this(handlerWrapper.Handler, initializer) { } /// /// Create a LambdaBootstrap that will call the given initializer and handler. /// /// The HTTP client to use with the Lambda runtime. /// The HandlerWrapper to call for each invocation of the Lambda function. /// Delegate called to initialize the Lambda function. If not provided the initialization step is skipped. /// public LambdaBootstrap(HttpClient httpClient, HandlerWrapper handlerWrapper, LambdaBootstrapInitializer initializer = null) : this(httpClient, handlerWrapper.Handler, initializer, ownsHttpClient: false) { } /// /// Create a LambdaBootstrap that will call the given initializer and handler. /// /// The HTTP client to use with the Lambda runtime. /// Delegate called for each invocation of the Lambda function. /// Delegate called to initialize the Lambda function. If not provided the initialization step is skipped. /// Whether the instance owns the HTTP client and should dispose of it. /// private LambdaBootstrap(HttpClient httpClient, LambdaBootstrapHandler handler, LambdaBootstrapInitializer initializer, bool ownsHttpClient) { _httpClient = httpClient ?? throw new ArgumentNullException(nameof(httpClient)); _handler = handler ?? throw new ArgumentNullException(nameof(handler)); _ownsHttpClient = ownsHttpClient; _initializer = initializer; _httpClient.Timeout = RuntimeApiHttpTimeout; Client = new RuntimeApiClient(new SystemEnvironmentVariables(), _httpClient); } /// /// Run the initialization Func if provided. /// Then run the invoke loop, calling the handler for each invocation. /// /// /// A Task that represents the operation. public async Task RunAsync(CancellationToken cancellationToken = default(CancellationToken)) { if(UserCodeInit.IsCallPreJit()) { this._logger.LogInformation("PreJit: CultureInfo"); UserCodeInit.LoadStringCultureInfo(); this._logger.LogInformation("PreJit: Amazon.Lambda.Core"); UserCodeInit.PreJitAssembly(typeof(Amazon.Lambda.Core.ILambdaContext).Assembly); } // For local debugging purposes this environment variable can be set to run a Lambda executable assembly and process one event // and then shut down cleanly. Useful for profiling or running local tests with the .NET Lambda Test Tool. This environment // variable should never be set when function is deployed to Lambda. var runOnce = string.Equals(Environment.GetEnvironmentVariable(Constants.ENVIRONMENT_VARIABLE_AWS_LAMBDA_DOTNET_DEBUG_RUN_ONCE), "true", StringComparison.OrdinalIgnoreCase); bool doStartInvokeLoop = _initializer == null || await InitializeAsync(); while (doStartInvokeLoop && !cancellationToken.IsCancellationRequested) { try { await InvokeOnceAsync(cancellationToken); if(runOnce) { _logger.LogInformation("Exiting Lambda processing loop because the run once environment variable was set."); return; } } catch (OperationCanceledException) when (cancellationToken.IsCancellationRequested) { // Loop cancelled } } } internal async Task InitializeAsync() { try { return await _initializer(); } catch (Exception exception) { WriteUnhandledExceptionToLog(exception); await Client.ReportInitializationErrorAsync(exception); throw; } } internal async Task InvokeOnceAsync(CancellationToken cancellationToken = default) { this._logger.LogInformation($"Starting InvokeOnceAsync"); using (var invocation = await Client.GetNextInvocationAsync(cancellationToken)) { InvocationResponse response = null; bool invokeSucceeded = false; try { this._logger.LogInformation($"Starting invoking handler"); response = await _handler(invocation); invokeSucceeded = true; } catch (Exception exception) { WriteUnhandledExceptionToLog(exception); await Client.ReportInvocationErrorAsync(invocation.LambdaContext.AwsRequestId, exception); } finally { this._logger.LogInformation($"Finished invoking handler"); } if (invokeSucceeded) { this._logger.LogInformation($"Starting sending response"); try { await Client.SendResponseAsync(invocation.LambdaContext.AwsRequestId, response?.OutputStream); } finally { if (response != null && response.DisposeOutputStream) { response.OutputStream?.Dispose(); } this._logger.LogInformation($"Finished sending response"); } } this._logger.LogInformation($"Finished InvokeOnceAsync"); } } /// /// Utility method for creating an HttpClient used by LambdaBootstrap to interact with the Lambda Runtime API. /// /// public static HttpClient ConstructHttpClient() { var dotnetRuntimeVersion = new DirectoryInfo(System.Runtime.InteropServices.RuntimeEnvironment.GetRuntimeDirectory()).Name; if (dotnetRuntimeVersion == "/") { dotnetRuntimeVersion = "unknown"; } var amazonLambdaRuntimeSupport = typeof(LambdaBootstrap).Assembly.GetName().Version; #if NET6_0_OR_GREATER // Create the SocketsHttpHandler directly to avoid spending cold start time creating the wrapper HttpClientHandler var handler = new SocketsHttpHandler { // Fix for https://github.com/aws/aws-lambda-dotnet/issues/1231. HttpClient by default supports only ASCII characters in headers. Changing it to allow UTF8 characters. RequestHeaderEncodingSelector = delegate { return System.Text.Encoding.UTF8; } }; // If we are running in an AOT environment, mark it as such. var userAgentString = NativeAotHelper.IsRunningNativeAot() ? $"aws-lambda-dotnet/{dotnetRuntimeVersion}-{amazonLambdaRuntimeSupport}-aot" : $"aws-lambda-dotnet/{dotnetRuntimeVersion}-{amazonLambdaRuntimeSupport}"; var client = new HttpClient(handler); #else var userAgentString = $"aws-lambda-dotnet/{dotnetRuntimeVersion}-{amazonLambdaRuntimeSupport}"; var client = new HttpClient(); #endif client.DefaultRequestHeaders.Add("User-Agent", userAgentString); return client; } private void WriteUnhandledExceptionToLog(Exception exception) { // Console.Error.WriteLine are redirected to the IConsoleLoggerWriter which // will take care of writing to the function's log stream. Console.Error.WriteLine(exception); } #region IDisposable Support private bool disposedValue = false; // To detect redundant calls protected virtual void Dispose(bool disposing) { if (!disposedValue) { if (disposing && _ownsHttpClient) { _httpClient?.Dispose(); } disposedValue = true; } } // This code added to correctly implement the disposable pattern. public void Dispose() { // Do not change this code. Put cleanup code in Dispose(bool disposing) above. Dispose(true); } #endregion } }