diff --git a/.github/workflows/pull_request.yml b/.github/workflows/pull_request.yml index 7964fef3..5c17f54e 100644 --- a/.github/workflows/pull_request.yml +++ b/.github/workflows/pull_request.yml @@ -40,7 +40,7 @@ jobs: # We pass the list of examples here, but we can't pass an array as argument # Instead, we pass a String with a valid JSON array. # The workaround is mentioned here https://github.com/orgs/community/discussions/11692 - examples: "[ 'APIGatewayV1', 'APIGatewayV2', 'APIGatewayV2+LambdaAuthorizer', 'BackgroundTasks', 'HelloJSON', 'HelloWorld', 'HelloWorldNoTraits', 'HummingbirdLambda', 'MultiSourceAPI', 'MultiTenant', 'ResourcesPackaging', 'S3EventNotifier', 'S3_AWSSDK', 'S3_Soto', 'Streaming+APIGateway', 'Streaming+FunctionUrl', 'Streaming+Codable', 'ServiceLifecycle+Postgres', 'Testing', 'Tutorial' ]" + examples: "[ 'APIGatewayV1', 'APIGatewayV2', 'APIGatewayV2+LambdaAuthorizer', 'BackgroundTasks', 'HelloJSON', 'HelloWorld', 'HelloWorldNoTraits', 'HummingbirdLambda', 'ManagedInstances', 'MultiSourceAPI', 'MultiTenant', 'ResourcesPackaging', 'S3EventNotifier', 'S3_AWSSDK', 'S3_Soto', 'Streaming+APIGateway', 'Streaming+FunctionUrl', 'Streaming+Codable', 'ServiceLifecycle+Postgres', 'Testing', 'Tutorial' ]" archive_plugin_examples: "[ 'HelloWorld', 'ResourcesPackaging' ]" archive_plugin_enabled: true diff --git a/Examples/ManagedInstances/.gitignore b/Examples/ManagedInstances/.gitignore new file mode 100644 index 00000000..a03a102d --- /dev/null +++ b/Examples/ManagedInstances/.gitignore @@ -0,0 +1,3 @@ +response.json +samconfig.toml +Makefile diff --git a/Examples/ManagedInstances/Package.swift b/Examples/ManagedInstances/Package.swift new file mode 100644 index 00000000..2868d5bb --- /dev/null +++ b/Examples/ManagedInstances/Package.swift @@ -0,0 +1,46 @@ +// swift-tools-version:6.2 + +import PackageDescription + +let package = Package( + name: "swift-aws-lambda-runtime-example", + platforms: [.macOS(.v15)], + products: [ + .executable(name: "HelloJSON", targets: ["HelloJSON"]), + .executable(name: "Streaming", targets: ["Streaming"]), + .executable(name: "BackgroundTasks", targets: ["BackgroundTasks"]), + ], + dependencies: [ + // For local development (default) + .package(name: "swift-aws-lambda-runtime", path: "../.."), + + // For standalone usage, comment the line above and uncomment below: + // .package(url: "https://github.com/awslabs/swift-aws-lambda-runtime.git", from: "2.0.0"), + + .package(url: "https://github.com/awslabs/swift-aws-lambda-events.git", from: "1.0.0"), + ], + targets: [ + .executableTarget( + name: "HelloJSON", + dependencies: [ + .product(name: "AWSLambdaRuntime", package: "swift-aws-lambda-runtime") + ], + path: "Sources/HelloJSON" + ), + .executableTarget( + name: "Streaming", + dependencies: [ + .product(name: "AWSLambdaRuntime", package: "swift-aws-lambda-runtime"), + .product(name: "AWSLambdaEvents", package: "swift-aws-lambda-events"), + ], + path: "Sources/Streaming" + ), + .executableTarget( + name: "BackgroundTasks", + dependencies: [ + .product(name: "AWSLambdaRuntime", package: "swift-aws-lambda-runtime") + ], + path: "Sources/BackgroundTasks" + ), + ] +) diff --git a/Examples/ManagedInstances/README.md b/Examples/ManagedInstances/README.md new file mode 100644 index 00000000..7e8b0fdb --- /dev/null +++ b/Examples/ManagedInstances/README.md @@ -0,0 +1,126 @@ +# Lambda Managed Instances Example + +This example demonstrates deploying Swift Lambda functions to Lambda Managed Instances using AWS SAM. Lambda Managed Instances provide serverless simplicity with EC2 flexibility and cost optimization by running your functions on customer-owned EC2 instances. + +## Functions Included + +1. **HelloJSON** - JSON input/output with structured data types +2. **Streaming** - Demonstrates response streaming capabilities +3. **BackgroundTasks** - Handles long-running background processing + +## Prerequisites + +- AWS CLI configured with appropriate permissions +- SAM CLI installed +- Swift 6.0+ installed +- An existing [Lambda Managed Instances capacity provider](https://docs.aws.amazon.com/lambda/latest/dg/lambda-managed-instances-capacity-providers.html) + +## Capacity Provider Configuration + +[Create your own capacity provider](https://docs.aws.amazon.com/lambda/latest/dg/lambda-managed-instances-capacity-providers.html#lambda-managed-instances-creating-capacity-provider) before deploying this example. + +This example uses a pre-configured capacity provider with the ARN: +``` +arn:aws:lambda:us-west-2:486652066693:capacity-provider:TestEC2 +``` + +## Deployment + +```bash +# Build the Swift packages +swift package archive --allow-network-access docker + +# Change the values below to match your setup +REGION=us-west-2 +CAPACITY_PROVIDER=arn:aws:lambda:us-west-2::capacity-provider: + +# Deploy using SAM +sam deploy \ + --resolve-s3 \ + --template-file template.yaml \ + --stack-name swift-lambda-managed-instances \ + --capabilities CAPABILITY_IAM \ + --region ${REGION} \ + --parameter-overrides \ + CapacityProviderArn=${CAPACITY_PROVIDER} +``` + +## Function Details + +### HelloJSON Function +- **Timeout**: 15 seconds (default) +- **Concurrency**: 8 per execution environment (default) +- **Input**: JSON `{"name": "string", "age": number}` +- **Output**: JSON `{"greetings": "string"}` + +### Streaming Function +- **Timeout**: 60 seconds +- **Concurrency**: 8 per execution environment (default) +- **Features**: Response streaming enabled +- **Output**: Streams numbers with pauses + +### BackgroundTasks Function +- **Timeout**: 300 seconds (5 minutes) +- **Concurrency**: 8 per execution environment (default) +- **Input**: JSON `{"message": "string"}` +- **Features**: Long-running background processing after response + +## Testing with AWS CLI + +After deployment, invoke each function with the AWS CLI: + +### Test HelloJSON Function +```bash +REGION=us-west-2 +aws lambda invoke \ +--region ${REGION} \ +--function-name swift-lambda-managed-instances-HelloJSON \ +--payload $(echo '{ "name" : "Swift Developer", "age" : 50 }' | base64) \ +out.txt && cat out.txt && rm out.txt + +# Expected output: {"greetings": "Hello Swift Developer. You look older than your age."} +``` + +### Test Streaming Function +```bash +# Get the Streaming URL +REGION=us-west-2 +STREAMING_URL=$(aws cloudformation describe-stacks \ + --stack-name swift-lambda-managed-instances \ + --region ${REGION} \ + --query 'Stacks[0].Outputs[?OutputKey==`StreamingFunctionUrl`].OutputValue' \ + --output text) + +# Set the AWS_ACCESS_KEY_ID, AWS_SECRET_ACCESS_KEY, and AWS_SESSION_TOKEN environment variables +eval $(aws configure export-credentials --format env) + +# Test with curl (streaming response) +curl "$STREAMING_URL" \ + --user "${AWS_ACCESS_KEY_ID}":"${AWS_SECRET_ACCESS_KEY}" \ + --aws-sigv4 "aws:amz:${REGION}:lambda" \ + -H "x-amz-security-token: ${AWS_SESSION_TOKEN}" \ + --no-buffer + +# Expected output: Numbers streaming with pauses +``` + +### Test BackgroundTasks Function +```bash +# Test with AWS CLI +REGION=us-west-2 +aws lambda invoke \ +--region ${REGION} \ +--function-name swift-lambda-managed-instances-BackgroundTasks \ +--payload $(echo '{ "message" : "Additional processing in the background" }' | base64) \ +out.txt && cat out.txt && rm out.txt + +# Expected output: {"echoedMessage": "Additional processing in the background"} +# Note: Background processing continues after response is sent +``` + +## Cleanup + +To remove all resources: +```bash +sam delete --stack-name swift-lambda-managed-instances --region ${REGION} +``` \ No newline at end of file diff --git a/Examples/ManagedInstances/Sources/BackgroundTasks/main.swift b/Examples/ManagedInstances/Sources/BackgroundTasks/main.swift new file mode 100644 index 00000000..f9fc81ed --- /dev/null +++ b/Examples/ManagedInstances/Sources/BackgroundTasks/main.swift @@ -0,0 +1,60 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the SwiftAWSLambdaRuntime open source project +// +// Copyright SwiftAWSLambdaRuntime project authors +// Copyright (c) Amazon.com, Inc. or its affiliates. +// Licensed under Apache License v2.0 +// +// See LICENSE.txt for license information +// See CONTRIBUTORS.txt for the list of SwiftAWSLambdaRuntime project authors +// +// SPDX-License-Identifier: Apache-2.0 +// +//===----------------------------------------------------------------------===// + +import AWSLambdaRuntime + +#if canImport(FoundationEssentials) +import FoundationEssentials +#else +import Foundation +#endif + +// for a simple struct as this one, the compiler automatically infers Sendable +// With Lambda Managed Instances, your handler struct MUST be Sendable +struct BackgroundProcessingHandler: LambdaWithBackgroundProcessingHandler, Sendable { + struct Input: Decodable { + let message: String + } + + struct Greeting: Encodable { + let echoedMessage: String + } + + typealias Event = Input + typealias Output = Greeting + + func handle( + _ event: Event, + outputWriter: some LambdaResponseWriter, + context: LambdaContext + ) async throws { + // Return result to the Lambda control plane + context.logger.debug("BackgroundProcessingHandler - message received") + try await outputWriter.write(Greeting(echoedMessage: event.message)) + + // Perform some background work, e.g: + context.logger.debug("BackgroundProcessingHandler - response sent. Performing background tasks.") + try await Task.sleep(for: .seconds(10)) + + // Exit the function. All asynchronous work has been executed before exiting the scope of this function. + // Follows structured concurrency principles. + context.logger.debug("BackgroundProcessingHandler - Background tasks completed. Returning") + return + } +} + +let adapter = LambdaCodableAdapterSendable(handler: BackgroundProcessingHandler()) +let runtime = LambdaManagedRuntime(handler: adapter) +try await runtime.run() diff --git a/Examples/ManagedInstances/Sources/HelloJSON/main.swift b/Examples/ManagedInstances/Sources/HelloJSON/main.swift new file mode 100644 index 00000000..f166c02f --- /dev/null +++ b/Examples/ManagedInstances/Sources/HelloJSON/main.swift @@ -0,0 +1,41 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the SwiftAWSLambdaRuntime open source project +// +// Copyright SwiftAWSLambdaRuntime project authors +// Copyright (c) Amazon.com, Inc. or its affiliates. +// Licensed under Apache License v2.0 +// +// See LICENSE.txt for license information +// See CONTRIBUTORS.txt for the list of SwiftAWSLambdaRuntime project authors +// +// SPDX-License-Identifier: Apache-2.0 +// +//===----------------------------------------------------------------------===// + +import AWSLambdaRuntime + +// in this example we are receiving and responding with JSON structures + +// the data structure to represent the input parameter +struct HelloRequest: Decodable { + let name: String + let age: Int +} + +// the data structure to represent the output response +struct HelloResponse: Encodable { + let greetings: String +} + +// the Lambda runtime +let runtime = LambdaManagedRuntime { + (event: HelloRequest, context: LambdaContext) in + + HelloResponse( + greetings: "Hello \(event.name). You look \(event.age > 30 ? "younger" : "older") than your age." + ) +} + +// start the loop +try await runtime.run() diff --git a/Examples/ManagedInstances/Sources/Streaming/main.swift b/Examples/ManagedInstances/Sources/Streaming/main.swift new file mode 100644 index 00000000..3109d55f --- /dev/null +++ b/Examples/ManagedInstances/Sources/Streaming/main.swift @@ -0,0 +1,69 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the SwiftAWSLambdaRuntime open source project +// +// Copyright SwiftAWSLambdaRuntime project authors +// Copyright (c) Amazon.com, Inc. or its affiliates. +// Licensed under Apache License v2.0 +// +// See LICENSE.txt for license information +// See CONTRIBUTORS.txt for the list of SwiftAWSLambdaRuntime project authors +// +// SPDX-License-Identifier: Apache-2.0 +// +//===----------------------------------------------------------------------===// + +import AWSLambdaEvents +import AWSLambdaRuntime +import NIOCore + +#if canImport(FoundationEssentials) +import FoundationEssentials +#else +import Foundation +#endif + +// for a simple struct as this one, the compiler automatically infers Sendable +// With Lambda Managed Instances, your handler struct MUST be Sendable +struct SendNumbersWithPause: StreamingLambdaHandler, Sendable { + func handle( + _ event: ByteBuffer, + responseWriter: some LambdaResponseStreamWriter, + context: LambdaContext + ) async throws { + + // The payload here is a Lambda Function URL request + // Check the body of the Function URL request to extract the business event + let payload = try JSONDecoder().decode(FunctionURLRequest.self, from: Data(event.readableBytesView)) + let _ = payload.body + + // Send HTTP status code and headers before streaming the response body + try await responseWriter.writeStatusAndHeaders( + StreamingLambdaStatusAndHeadersResponse( + statusCode: 418, // I'm a tea pot + headers: [ + "Content-Type": "text/plain", + "x-my-custom-header": "streaming-example", + ] + ) + ) + + // Stream numbers with pauses to demonstrate streaming functionality + for i in 1...3 { + // Send partial data + try await responseWriter.write(ByteBuffer(string: "Number: \(i)\n")) + + // Perform some long asynchronous work to simulate processing + try await Task.sleep(for: .milliseconds(1000)) + } + + // Send final message + try await responseWriter.write(ByteBuffer(string: "Streaming complete!\n")) + + // All data has been sent. Close off the response stream. + try await responseWriter.finish() + } +} + +let runtime = LambdaManagedRuntime(handler: SendNumbersWithPause()) +try await runtime.run() diff --git a/Examples/ManagedInstances/template.yaml b/Examples/ManagedInstances/template.yaml new file mode 100644 index 00000000..ba1307c5 --- /dev/null +++ b/Examples/ManagedInstances/template.yaml @@ -0,0 +1,81 @@ +AWSTemplateFormatVersion: '2010-09-09' +Transform: AWS::Serverless-2016-10-31 +Description: SAM Template for Lambda Managed Instances Example + +# This template deploys three Lambda functions to Lambda Managed Instances +# using a pre-existing capacity provider. + +Parameters: + CapacityProviderArn: + Type: String + Default: arn:aws:lambda:us-west-2:${AWS::AccountId}:capacity-provider:MyCapacityProvider # TODO : CHANGE The Name! + Description: ARN of the existing capacity provider for Lambda Managed Instances + +Globals: + Function: + Handler: swift.bootstrap # ignored by the Swift runtime + Runtime: provided.al2023 + Architectures: + - arm64 + Timeout: 15 + CapacityProviderConfig: + Arn: !Ref CapacityProviderArn + PerExecutionEnvironmentMaxConcurrency: 8 + Environment: + Variables: + LOG_LEVEL: trace + +Resources: + # HelloJSON Function - JSON input/output with structured data + HelloJSONFunction: + Type: AWS::Serverless::Function + Properties: + CodeUri: .build/plugins/AWSLambdaPackager/outputs/AWSLambdaPackager/HelloJSON/HelloJSON.zip + FunctionName: !Sub "${AWS::StackName}-HelloJSON" + + # Streaming Function - Demonstrates response streaming capabilities + StreamingFunction: + Type: AWS::Serverless::Function + Properties: + CodeUri: .build/plugins/AWSLambdaPackager/outputs/AWSLambdaPackager/Streaming/Streaming.zip + FunctionName: !Sub "${AWS::StackName}-Streaming" + Timeout: 60 # Longer timeout for streaming operations + FunctionUrlConfig: + AuthType: AWS_IAM + InvokeMode: RESPONSE_STREAM + + # BackgroundTasks Function - Handles long-running background processing + BackgroundTasksFunction: + Type: AWS::Serverless::Function + Properties: + CodeUri: .build/plugins/AWSLambdaPackager/outputs/AWSLambdaPackager/BackgroundTasks/BackgroundTasks.zip + FunctionName: !Sub "${AWS::StackName}-BackgroundTasks" + Timeout: 300 # 5 minutes for background processing + Environment: + Variables: + LOG_LEVEL: debug + +Outputs: + # Function URL for reference + StreamingFunctionUrl: + Description: Streaming Function URL + Value: !GetAtt StreamingFunctionUrl.FunctionUrl + + # Function ARNs for reference + HelloJSONFunctionArn: + Description: "HelloJSON Function ARN" + Value: !GetAtt HelloJSONFunction.Arn + Export: + Name: !Sub "${AWS::StackName}-HelloJSONArn" + + StreamingFunctionArn: + Description: "Streaming Function ARN" + Value: !GetAtt StreamingFunction.Arn + Export: + Name: !Sub "${AWS::StackName}-StreamingArn" + + BackgroundTasksFunctionArn: + Description: "BackgroundTasks Function ARN" + Value: !GetAtt BackgroundTasksFunction.Arn + Export: + Name: !Sub "${AWS::StackName}-BackgroundTasksArn" diff --git a/Examples/Streaming+Codable/Tests/LambdaStreamingCodableTests.swift b/Examples/Streaming+Codable/Tests/LambdaStreamingCodableTests.swift index ca25ed34..3754c2e4 100644 --- a/Examples/Streaming+Codable/Tests/LambdaStreamingCodableTests.swift +++ b/Examples/Streaming+Codable/Tests/LambdaStreamingCodableTests.swift @@ -336,7 +336,7 @@ extension LambdaContext { traceID: "test-trace-id", invokedFunctionARN: "arn:aws:lambda:us-east-1:123456789012:function:test", timeout: .seconds(30), - logger: Logger(label: "test") + logger: Logger(label: "MockedLambdaContext") ) } } diff --git a/Package.swift b/Package.swift index 99ea9667..36a0e467 100644 --- a/Package.swift +++ b/Package.swift @@ -18,11 +18,13 @@ let package = Package( .plugin(name: "AWSLambdaPackager", targets: ["AWSLambdaPackager"]), ], traits: [ + "ManagedRuntimeSupport", "FoundationJSONSupport", "ServiceLifecycleSupport", "LocalServerSupport", .default( enabledTraits: [ + "ManagedRuntimeSupport", "FoundationJSONSupport", "ServiceLifecycleSupport", "LocalServerSupport", @@ -30,10 +32,10 @@ let package = Package( ), ], dependencies: [ - .package(url: "https://github.com/apple/swift-nio.git", from: "2.81.0"), - .package(url: "https://github.com/apple/swift-log.git", from: "1.5.4"), - .package(url: "https://github.com/apple/swift-collections.git", from: "1.1.4"), - .package(url: "https://github.com/swift-server/swift-service-lifecycle.git", from: "2.8.0"), + .package(url: "https://github.com/apple/swift-nio.git", from: "2.92.0"), + .package(url: "https://github.com/apple/swift-log.git", from: "1.8.0"), + .package(url: "https://github.com/apple/swift-collections.git", from: "1.3.0"), + .package(url: "https://github.com/swift-server/swift-service-lifecycle.git", from: "2.9.0"), ], targets: [ .target( diff --git a/Package@swift-6.0.swift b/Package@swift-6.0.swift index 5f4021d4..be01537f 100644 --- a/Package@swift-6.0.swift +++ b/Package@swift-6.0.swift @@ -6,6 +6,7 @@ let defaultSwiftSettings: [SwiftSetting] = [ .define("FoundationJSONSupport"), .define("ServiceLifecycleSupport"), .define("LocalServerSupport"), + .define("ManagedRuntimeSupport"), .enableExperimentalFeature( "AvailabilityMacro=LambdaSwift 2.0:macOS 15.0" ), @@ -20,10 +21,10 @@ let package = Package( .plugin(name: "AWSLambdaPackager", targets: ["AWSLambdaPackager"]), ], dependencies: [ - .package(url: "https://github.com/apple/swift-nio.git", from: "2.81.0"), - .package(url: "https://github.com/apple/swift-log.git", from: "1.5.4"), - .package(url: "https://github.com/apple/swift-collections.git", from: "1.1.4"), - .package(url: "https://github.com/swift-server/swift-service-lifecycle.git", from: "2.8.0"), + .package(url: "https://github.com/apple/swift-nio.git", from: "2.92.0"), + .package(url: "https://github.com/apple/swift-log.git", from: "1.8.0"), + .package(url: "https://github.com/apple/swift-collections.git", from: "1.3.0"), + .package(url: "https://github.com/swift-server/swift-service-lifecycle.git", from: "2.9.0"), ], targets: [ .target( diff --git a/Sources/AWSLambdaRuntime/FoundationSupport/LambdaHandler+JSON.swift b/Sources/AWSLambdaRuntime/FoundationSupport/LambdaHandler+JSON.swift new file mode 100644 index 00000000..19313df6 --- /dev/null +++ b/Sources/AWSLambdaRuntime/FoundationSupport/LambdaHandler+JSON.swift @@ -0,0 +1,128 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the SwiftAWSLambdaRuntime open source project +// +// Copyright SwiftAWSLambdaRuntime project authors +// Copyright (c) Amazon.com, Inc. or its affiliates. +// Licensed under Apache License v2.0 +// +// See LICENSE.txt for license information +// See CONTRIBUTORS.txt for the list of SwiftAWSLambdaRuntime project authors +// +// SPDX-License-Identifier: Apache-2.0 +// +//===----------------------------------------------------------------------===// + +#if FoundationJSONSupport +import NIOCore + +#if canImport(FoundationEssentials) +import FoundationEssentials +#else +import struct Foundation.Data +import class Foundation.JSONDecoder +import class Foundation.JSONEncoder +#endif + +import Logging + +public struct LambdaJSONEventDecoder: LambdaEventDecoder { + @usableFromInline let jsonDecoder: JSONDecoder + + @inlinable + public init(_ jsonDecoder: JSONDecoder) { + self.jsonDecoder = jsonDecoder + } + + @inlinable + public func decode(_ type: Event.Type, from buffer: NIOCore.ByteBuffer) throws -> Event + where Event: Decodable { + try buffer.getJSONDecodable( + Event.self, + decoder: self.jsonDecoder, + at: buffer.readerIndex, + length: buffer.readableBytes + )! // must work, enough readable bytes + } +} + +public struct LambdaJSONOutputEncoder: LambdaOutputEncoder { + @usableFromInline let jsonEncoder: JSONEncoder + + @inlinable + public init(_ jsonEncoder: JSONEncoder) { + self.jsonEncoder = jsonEncoder + } + + @inlinable + public func encode(_ value: Output, into buffer: inout ByteBuffer) throws { + try buffer.writeJSONEncodable(value, encoder: self.jsonEncoder) + } +} + +@available(LambdaSwift 2.0, *) +extension LambdaCodableAdapter { + /// Initializes an instance given an encoder, decoder, and a handler with a non-`Void` output. + /// - Parameters: + /// - encoder: The encoder object that will be used to encode the generic `Output` obtained from the `handler`'s `outputWriter` into a `ByteBuffer`. By default, a JSONEncoder is used. + /// - decoder: The decoder object that will be used to decode the received `ByteBuffer` event into the generic `Event` type served to the `handler`. By default, a JSONDecoder is used. + /// - handler: The handler object. + public init( + encoder: JSONEncoder = JSONEncoder(), + decoder: JSONDecoder = JSONDecoder(), + handler: sending Handler + ) + where + Output: Encodable, + Output == Handler.Output, + Encoder == LambdaJSONOutputEncoder, + Decoder == LambdaJSONEventDecoder + { + self.init( + encoder: LambdaJSONOutputEncoder(encoder), + decoder: LambdaJSONEventDecoder(decoder), + handler: handler + ) + } + + /// Initializes an instance given a decoder, and a handler with a `Void` output. + /// - Parameters: + /// - decoder: The decoder object that will be used to decode the received `ByteBuffer` event into the generic `Event` type served to the `handler`. By default, a JSONDecoder is used. + /// - handler: The handler object. + public init( + decoder: JSONDecoder = JSONDecoder(), + handler: sending Handler + ) + where + Output == Void, + Handler.Output == Void, + Decoder == LambdaJSONEventDecoder, + Encoder == VoidEncoder + { + self.init( + decoder: LambdaJSONEventDecoder(decoder), + handler: handler + ) + } +} + +@available(LambdaSwift 2.0, *) +extension LambdaResponseStreamWriter { + /// Writes the HTTP status code and headers to the response stream. + /// + /// This method serializes the status and headers as JSON and writes them to the stream, + /// followed by eight null bytes as a separator before the response body. + /// + /// - Parameters: + /// - response: The status and headers response to write + /// - encoder: The encoder to use for serializing the response, use JSONEncoder by default + /// - Throws: An error if JSON serialization or writing fails + public func writeStatusAndHeaders( + _ response: StreamingLambdaStatusAndHeadersResponse, + encoder: JSONEncoder = JSONEncoder() + ) async throws { + encoder.outputFormatting = .withoutEscapingSlashes + try await self.writeStatusAndHeaders(response, encoder: LambdaJSONOutputEncoder(encoder)) + } +} +#endif // trait: FoundationJSONSupport diff --git a/Sources/AWSLambdaRuntime/FoundationSupport/Lambda+JSON.swift b/Sources/AWSLambdaRuntime/FoundationSupport/LambdaManagedRuntime+JSON.swift similarity index 68% rename from Sources/AWSLambdaRuntime/FoundationSupport/Lambda+JSON.swift rename to Sources/AWSLambdaRuntime/FoundationSupport/LambdaManagedRuntime+JSON.swift index 646e77f9..26159467 100644 --- a/Sources/AWSLambdaRuntime/FoundationSupport/Lambda+JSON.swift +++ b/Sources/AWSLambdaRuntime/FoundationSupport/LambdaManagedRuntime+JSON.swift @@ -13,6 +13,8 @@ // //===----------------------------------------------------------------------===// +#if ManagedRuntimeSupport + #if FoundationJSONSupport import NIOCore @@ -26,7 +28,7 @@ import class Foundation.JSONEncoder import Logging -public struct LambdaJSONEventDecoder: LambdaEventDecoder { +public struct LambdaJSONEventDecoderSendable: LambdaEventDecoder & Sendable { @usableFromInline let jsonDecoder: JSONDecoder @inlinable @@ -46,7 +48,7 @@ public struct LambdaJSONEventDecoder: LambdaEventDecoder { } } -public struct LambdaJSONOutputEncoder: LambdaOutputEncoder { +public struct LambdaJSONOutputEncoderSendable: LambdaOutputEncoder & Sendable { @usableFromInline let jsonEncoder: JSONEncoder @inlinable @@ -61,7 +63,7 @@ public struct LambdaJSONOutputEncoder: LambdaOutputEncoder { } @available(LambdaSwift 2.0, *) -extension LambdaCodableAdapter { +extension LambdaCodableAdapterSendable { /// Initializes an instance given an encoder, decoder, and a handler with a non-`Void` output. /// - Parameters: /// - encoder: The encoder object that will be used to encode the generic `Output` obtained from the `handler`'s `outputWriter` into a `ByteBuffer`. By default, a JSONEncoder is used. @@ -70,17 +72,17 @@ extension LambdaCodableAdapter { public init( encoder: JSONEncoder = JSONEncoder(), decoder: JSONDecoder = JSONDecoder(), - handler: sending Handler + handler: Handler ) where Output: Encodable, Output == Handler.Output, - Encoder == LambdaJSONOutputEncoder, - Decoder == LambdaJSONEventDecoder + Encoder == LambdaJSONOutputEncoderSendable, + Decoder == LambdaJSONEventDecoderSendable { self.init( - encoder: LambdaJSONOutputEncoder(encoder), - decoder: LambdaJSONEventDecoder(decoder), + encoder: LambdaJSONOutputEncoderSendable(encoder), + decoder: LambdaJSONEventDecoderSendable(decoder), handler: handler ) } @@ -96,37 +98,18 @@ extension LambdaCodableAdapter { where Output == Void, Handler.Output == Void, - Decoder == LambdaJSONEventDecoder, + Decoder == LambdaJSONEventDecoderSendable, Encoder == VoidEncoder { self.init( - decoder: LambdaJSONEventDecoder(decoder), + decoder: LambdaJSONEventDecoderSendable(decoder), handler: handler ) } } @available(LambdaSwift 2.0, *) -extension LambdaResponseStreamWriter { - /// Writes the HTTP status code and headers to the response stream. - /// - /// This method serializes the status and headers as JSON and writes them to the stream, - /// followed by eight null bytes as a separator before the response body. - /// - /// - Parameters: - /// - response: The status and headers response to write - /// - encoder: The encoder to use for serializing the response, use JSONEncoder by default - /// - Throws: An error if JSON serialization or writing fails - public func writeStatusAndHeaders( - _ response: StreamingLambdaStatusAndHeadersResponse, - encoder: JSONEncoder = JSONEncoder() - ) async throws { - encoder.outputFormatting = .withoutEscapingSlashes - try await self.writeStatusAndHeaders(response, encoder: LambdaJSONOutputEncoder(encoder)) - } -} -@available(LambdaSwift 2.0, *) -extension LambdaRuntime { +extension LambdaManagedRuntime { /// Initialize an instance with a `LambdaHandler` defined in the form of a closure **with a non-`Void` return type**. /// - Parameters: /// - decoder: The decoder object that will be used to decode the incoming `ByteBuffer` event into the generic `Event` type. `JSONDecoder()` used as default. @@ -136,22 +119,22 @@ extension LambdaRuntime { public convenience init( decoder: JSONDecoder = JSONDecoder(), encoder: JSONEncoder = JSONEncoder(), - logger: Logger = Logger(label: "LambdaRuntime"), - body: sending @escaping (Event, LambdaContext) async throws -> Output + logger: Logger = Logger(label: "LambdaManagedRuntime"), + body: @Sendable @escaping (Event, LambdaContext) async throws -> Output ) where - Handler == LambdaCodableAdapter< - LambdaHandlerAdapter>, + Handler == LambdaCodableAdapterSendable< + LambdaHandlerAdapterSendable>, Event, Output, - LambdaJSONEventDecoder, - LambdaJSONOutputEncoder + LambdaJSONEventDecoderSendable, + LambdaJSONOutputEncoderSendable > { - let handler = LambdaCodableAdapter( + let handler = LambdaCodableAdapterSendable( encoder: encoder, decoder: decoder, - handler: LambdaHandlerAdapter(handler: ClosureHandler(body: body)) + handler: LambdaHandlerAdapterSendable(handler: ClosureHandlerSendable(body: body)) ) self.init(handler: handler, logger: logger) @@ -164,20 +147,20 @@ extension LambdaRuntime { public convenience init( decoder: JSONDecoder = JSONDecoder(), logger: Logger = Logger(label: "LambdaRuntime"), - body: sending @escaping (Event, LambdaContext) async throws -> Void + body: @Sendable @escaping (Event, LambdaContext) async throws -> Void ) where - Handler == LambdaCodableAdapter< - LambdaHandlerAdapter>, + Handler == LambdaCodableAdapterSendable< + LambdaHandlerAdapterSendable>, Event, Void, - LambdaJSONEventDecoder, + LambdaJSONEventDecoderSendable, VoidEncoder > { - let handler = LambdaCodableAdapter( - decoder: LambdaJSONEventDecoder(decoder), - handler: LambdaHandlerAdapter(handler: ClosureHandler(body: body)) + let handler = LambdaCodableAdapterSendable( + decoder: LambdaJSONEventDecoderSendable(decoder), + handler: LambdaHandlerAdapterSendable(handler: ClosureHandlerSendable(body: body)) ) self.init(handler: handler, logger: logger) @@ -187,26 +170,26 @@ extension LambdaRuntime { /// - Parameters: /// - decoder: The decoder object that will be used to decode the incoming `ByteBuffer` event into the generic `Event` type. `JSONDecoder()` used as default. /// - logger: The logger to use for the runtime. Defaults to a logger with label "LambdaRuntime". - /// - lambdaHandler: A type that conforms to the `LambdaHandler` protocol, whose `Event` is `Decodable` and `Output` is `Void` - public convenience init( + /// - lambdaHandler: A type that conforms to the `LambdaHandler` and `Sendable` protocols, whose `Event` is `Decodable` and `Output` is `Void` + public convenience init( decoder: JSONDecoder = JSONDecoder(), logger: Logger = Logger(label: "LambdaRuntime"), - lambdaHandler: sending LHandler + lambdaHandler: LHandler ) where - Handler == LambdaCodableAdapter< - LambdaHandlerAdapter, + Handler == LambdaCodableAdapterSendable< + LambdaHandlerAdapterSendable, Event, Void, - LambdaJSONEventDecoder, + LambdaJSONEventDecoderSendable, VoidEncoder >, LHandler.Event == Event, LHandler.Output == Void { - let handler = LambdaCodableAdapter( - decoder: LambdaJSONEventDecoder(decoder), - handler: LambdaHandlerAdapter(handler: lambdaHandler) + let handler = LambdaCodableAdapterSendable( + decoder: LambdaJSONEventDecoderSendable(decoder), + handler: LambdaHandlerAdapterSendable(handler: lambdaHandler) ) self.init(handler: handler, logger: logger) @@ -217,31 +200,33 @@ extension LambdaRuntime { /// - decoder: The decoder object that will be used to decode the incoming `ByteBuffer` event into the generic `Event` type. `JSONDecoder()` used as default. /// - encoder: The encoder object that will be used to encode the generic `Output` into a `ByteBuffer`. `JSONEncoder()` used as default. /// - logger: The logger to use for the runtime. Defaults to a logger with label "LambdaRuntime". - /// - lambdaHandler: A type that conforms to the `LambdaHandler` protocol, whose `Event` is `Decodable` and `Output` is `Encodable` - public convenience init( + /// - lambdaHandler: A type that conforms to the `LambdaHandler` and `Sendable` protocols, whose `Event` is `Decodable` and `Output` is `Encodable` + public convenience init( decoder: JSONDecoder = JSONDecoder(), encoder: JSONEncoder = JSONEncoder(), logger: Logger = Logger(label: "LambdaRuntime"), - lambdaHandler: sending LHandler + lambdaHandler: LHandler ) where - Handler == LambdaCodableAdapter< - LambdaHandlerAdapter, + Handler == LambdaCodableAdapterSendable< + LambdaHandlerAdapterSendable, Event, Output, - LambdaJSONEventDecoder, - LambdaJSONOutputEncoder + LambdaJSONEventDecoderSendable, + LambdaJSONOutputEncoderSendable >, LHandler.Event == Event, LHandler.Output == Output { - let handler = LambdaCodableAdapter( + let handler = LambdaCodableAdapterSendable( encoder: encoder, decoder: decoder, - handler: LambdaHandlerAdapter(handler: lambdaHandler) + handler: LambdaHandlerAdapterSendable(handler: lambdaHandler) ) self.init(handler: handler, logger: logger) } } #endif // trait: FoundationJSONSupport + +#endif // trait: ManagedRuntimeSupport diff --git a/Sources/AWSLambdaRuntime/FoundationSupport/LambdaRuntime+JSON.swift b/Sources/AWSLambdaRuntime/FoundationSupport/LambdaRuntime+JSON.swift new file mode 100644 index 00000000..50a9c5a7 --- /dev/null +++ b/Sources/AWSLambdaRuntime/FoundationSupport/LambdaRuntime+JSON.swift @@ -0,0 +1,148 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the SwiftAWSLambdaRuntime open source project +// +// Copyright SwiftAWSLambdaRuntime project authors +// Copyright (c) Amazon.com, Inc. or its affiliates. +// Licensed under Apache License v2.0 +// +// See LICENSE.txt for license information +// See CONTRIBUTORS.txt for the list of SwiftAWSLambdaRuntime project authors +// +// SPDX-License-Identifier: Apache-2.0 +// +//===----------------------------------------------------------------------===// + +#if FoundationJSONSupport +import NIOCore + +#if canImport(FoundationEssentials) +import FoundationEssentials +#else +import struct Foundation.Data +import class Foundation.JSONDecoder +import class Foundation.JSONEncoder +#endif + +import Logging + +@available(LambdaSwift 2.0, *) +extension LambdaRuntime { + /// Initialize an instance with a `LambdaHandler` defined in the form of a closure **with a non-`Void` return type**. + /// - Parameters: + /// - decoder: The decoder object that will be used to decode the incoming `ByteBuffer` event into the generic `Event` type. `JSONDecoder()` used as default. + /// - encoder: The encoder object that will be used to encode the generic `Output` into a `ByteBuffer`. `JSONEncoder()` used as default. + /// - logger: The logger to use for the runtime. Defaults to a logger with label "LambdaRuntime". + /// - body: The handler in the form of a closure. + public convenience init( + decoder: JSONDecoder = JSONDecoder(), + encoder: JSONEncoder = JSONEncoder(), + logger: Logger = Logger(label: "LambdaRuntime"), + body: sending @escaping (Event, LambdaContext) async throws -> Output + ) + where + Handler == LambdaCodableAdapter< + LambdaHandlerAdapter>, + Event, + Output, + LambdaJSONEventDecoder, + LambdaJSONOutputEncoder + > + { + let handler = LambdaCodableAdapter( + encoder: encoder, + decoder: decoder, + handler: LambdaHandlerAdapter(handler: ClosureHandler(body: body)) + ) + + self.init(handler: handler, logger: logger) + } + + /// Initialize an instance with a `LambdaHandler` defined in the form of a closure **with a `Void` return type**. + /// - Parameter body: The handler in the form of a closure. + /// - Parameter decoder: The decoder object that will be used to decode the incoming `ByteBuffer` event into the generic `Event` type. `JSONDecoder()` used as default. + /// - Parameter logger: The logger to use for the runtime. Defaults to a logger with label "LambdaRuntime". + public convenience init( + decoder: JSONDecoder = JSONDecoder(), + logger: Logger = Logger(label: "LambdaRuntime"), + body: sending @escaping (Event, LambdaContext) async throws -> Void + ) + where + Handler == LambdaCodableAdapter< + LambdaHandlerAdapter>, + Event, + Void, + LambdaJSONEventDecoder, + VoidEncoder + > + { + let handler = LambdaCodableAdapter( + decoder: LambdaJSONEventDecoder(decoder), + handler: LambdaHandlerAdapter(handler: ClosureHandler(body: body)) + ) + + self.init(handler: handler, logger: logger) + } + + /// Initialize an instance directly with a `LambdaHandler`, when `Event` is `Decodable` and `Output` is `Void`. + /// - Parameters: + /// - decoder: The decoder object that will be used to decode the incoming `ByteBuffer` event into the generic `Event` type. `JSONDecoder()` used as default. + /// - logger: The logger to use for the runtime. Defaults to a logger with label "LambdaRuntime". + /// - lambdaHandler: A type that conforms to the `LambdaHandler` protocol, whose `Event` is `Decodable` and `Output` is `Void` + public convenience init( + decoder: JSONDecoder = JSONDecoder(), + logger: Logger = Logger(label: "LambdaRuntime"), + lambdaHandler: sending LHandler + ) + where + Handler == LambdaCodableAdapter< + LambdaHandlerAdapter, + Event, + Void, + LambdaJSONEventDecoder, + VoidEncoder + >, + LHandler.Event == Event, + LHandler.Output == Void + { + let handler = LambdaCodableAdapter( + decoder: LambdaJSONEventDecoder(decoder), + handler: LambdaHandlerAdapter(handler: lambdaHandler) + ) + + self.init(handler: handler, logger: logger) + } + + /// Initialize an instance directly with a `LambdaHandler`, when `Event` is `Decodable` and `Output` is `Encodable`. + /// - Parameters: + /// - decoder: The decoder object that will be used to decode the incoming `ByteBuffer` event into the generic `Event` type. `JSONDecoder()` used as default. + /// - encoder: The encoder object that will be used to encode the generic `Output` into a `ByteBuffer`. `JSONEncoder()` used as default. + /// - logger: The logger to use for the runtime. Defaults to a logger with label "LambdaRuntime". + /// - lambdaHandler: A type that conforms to the `LambdaHandler` protocol, whose `Event` is `Decodable` and `Output` is `Encodable` + public convenience init( + decoder: JSONDecoder = JSONDecoder(), + encoder: JSONEncoder = JSONEncoder(), + logger: Logger = Logger(label: "LambdaRuntime"), + lambdaHandler: sending LHandler + ) + where + Handler == LambdaCodableAdapter< + LambdaHandlerAdapter, + Event, + Output, + LambdaJSONEventDecoder, + LambdaJSONOutputEncoder + >, + LHandler.Event == Event, + LHandler.Output == Output + { + let handler = LambdaCodableAdapter( + encoder: encoder, + decoder: decoder, + handler: LambdaHandlerAdapter(handler: lambdaHandler) + ) + + self.init(handler: handler, logger: logger) + } +} +#endif // trait: FoundationJSONSupport diff --git a/Sources/AWSLambdaRuntime/ManagedRuntime/LambdaManagedRuntime+Codable.swift b/Sources/AWSLambdaRuntime/ManagedRuntime/LambdaManagedRuntime+Codable.swift new file mode 100644 index 00000000..28d3115b --- /dev/null +++ b/Sources/AWSLambdaRuntime/ManagedRuntime/LambdaManagedRuntime+Codable.swift @@ -0,0 +1,111 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the SwiftAWSLambdaRuntime open source project +// +// Copyright SwiftAWSLambdaRuntime project authors +// Copyright (c) Amazon.com, Inc. or its affiliates. +// Licensed under Apache License v2.0 +// +// See LICENSE.txt for license information +// See CONTRIBUTORS.txt for the list of SwiftAWSLambdaRuntime project authors +// +// SPDX-License-Identifier: Apache-2.0 +// +//===----------------------------------------------------------------------===// + +#if ManagedRuntimeSupport + +import NIOCore + +/// Adapts a ``LambdaHandler`` conforming handler to conform to ``LambdaWithBackgroundProcessingHandler``. +@available(LambdaSwift 2.0, *) +public struct LambdaHandlerAdapterSendable< + Event: Decodable, + Output, + Handler: LambdaHandler & Sendable +>: Sendable, LambdaWithBackgroundProcessingHandler where Handler.Event == Event, Handler.Output == Output { + @usableFromInline let handler: Handler + + /// Initializes an instance given a concrete handler. + /// - Parameter handler: The ``LambdaHandler`` conforming handler that is to be adapted to ``LambdaWithBackgroundProcessingHandler``. + @inlinable + public init(handler: sending Handler) { + self.handler = handler + } + + /// Passes the generic `Event` object to the ``LambdaHandler/handle(_:context:)`` function, and + /// the resulting output is then written to ``LambdaWithBackgroundProcessingHandler``'s `outputWriter`. + /// - Parameters: + /// - event: The received event. + /// - outputWriter: The writer to write the computed response to. + /// - context: The ``LambdaContext`` containing the invocation's metadata. + @inlinable + public func handle( + _ event: Event, + outputWriter: some LambdaResponseWriter, + context: LambdaContext + ) async throws { + let output = try await self.handler.handle(event, context: context) + try await outputWriter.write(output) + } +} + +/// Adapts a ``LambdaWithBackgroundProcessingHandler`` conforming handler to conform to ``StreamingLambdaHandler``. +@available(LambdaSwift 2.0, *) +public struct LambdaCodableAdapterSendable< + Handler: LambdaWithBackgroundProcessingHandler & Sendable, + Event: Decodable, + Output, + Decoder: LambdaEventDecoder & Sendable, + Encoder: LambdaOutputEncoder & Sendable +>: Sendable, StreamingLambdaHandler where Handler.Event == Event, Handler.Output == Output, Encoder.Output == Output { + @usableFromInline let handler: Handler + @usableFromInline let encoder: Encoder + @usableFromInline let decoder: Decoder + @usableFromInline var byteBuffer: ByteBuffer = .init() + + /// Initializes an instance given an encoder, decoder, and a handler with a non-`Void` output. + /// - Parameters: + /// - encoder: The encoder object that will be used to encode the generic `Output` obtained from the `handler`'s `outputWriter` into a `ByteBuffer`. + /// - decoder: The decoder object that will be used to decode the received `ByteBuffer` event into the generic `Event` type served to the `handler`. + /// - handler: The handler object. + @inlinable + public init(encoder: Encoder, decoder: Decoder, handler: Handler) where Output: Encodable { + self.encoder = encoder + self.decoder = decoder + self.handler = handler + } + + /// Initializes an instance given a decoder, and a handler with a `Void` output. + /// - Parameters: + /// - decoder: The decoder object that will be used to decode the received `ByteBuffer` event into the generic `Event` type served to the `handler`. + /// - handler: The handler object. + @inlinable + public init(decoder: Decoder, handler: Handler) where Output == Void, Encoder == VoidEncoder { + self.encoder = VoidEncoder() + self.decoder = decoder + self.handler = handler + } + + /// A ``StreamingLambdaHandler/handle(_:responseWriter:context:)`` wrapper. + /// - Parameters: + /// - request: The received event. + /// - responseWriter: The writer to write the computed response to. + /// - context: The ``LambdaContext`` containing the invocation's metadata. + @inlinable + public mutating func handle( + _ request: ByteBuffer, + responseWriter: Writer, + context: LambdaContext + ) async throws { + let event = try self.decoder.decode(Event.self, from: request) + + let writer = LambdaCodableResponseWriter( + encoder: self.encoder, + streamWriter: responseWriter + ) + try await self.handler.handle(event, outputWriter: writer, context: context) + } +} + +#endif diff --git a/Sources/AWSLambdaRuntime/ManagedRuntime/LambdaManagedRuntime+ServiceLifecycle.swift b/Sources/AWSLambdaRuntime/ManagedRuntime/LambdaManagedRuntime+ServiceLifecycle.swift new file mode 100644 index 00000000..a47bcd3a --- /dev/null +++ b/Sources/AWSLambdaRuntime/ManagedRuntime/LambdaManagedRuntime+ServiceLifecycle.swift @@ -0,0 +1,31 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the SwiftAWSLambdaRuntime open source project +// +// Copyright SwiftAWSLambdaRuntime project authors +// Copyright (c) Amazon.com, Inc. or its affiliates. +// Licensed under Apache License v2.0 +// +// See LICENSE.txt for license information +// See CONTRIBUTORS.txt for the list of SwiftAWSLambdaRuntime project authors +// +// SPDX-License-Identifier: Apache-2.0 +// +//===----------------------------------------------------------------------===// + +#if ManagedRuntimeSupport + +#if ServiceLifecycleSupport +import ServiceLifecycle + +@available(LambdaSwift 2.0, *) +extension LambdaManagedRuntime: Service { + public func run() async throws { + try await cancelWhenGracefulShutdown { + try await self._run() + } + } +} +#endif + +#endif diff --git a/Sources/AWSLambdaRuntime/ManagedRuntime/LambdaManagedRuntime.swift b/Sources/AWSLambdaRuntime/ManagedRuntime/LambdaManagedRuntime.swift new file mode 100644 index 00000000..257e117a --- /dev/null +++ b/Sources/AWSLambdaRuntime/ManagedRuntime/LambdaManagedRuntime.swift @@ -0,0 +1,146 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the SwiftAWSLambdaRuntime open source project +// +// Copyright SwiftAWSLambdaRuntime project authors +// Copyright (c) Amazon.com, Inc. or its affiliates. +// Licensed under Apache License v2.0 +// +// See LICENSE.txt for license information +// See CONTRIBUTORS.txt for the list of SwiftAWSLambdaRuntime project authors +// +// SPDX-License-Identifier: Apache-2.0 +// +//===----------------------------------------------------------------------===// + +#if ManagedRuntimeSupport + +import Logging +import NIOCore +import Synchronization + +// This is our guardian to ensure only one LambdaManagedRuntime is running at the time +// We use an Atomic here to ensure thread safety +@available(LambdaSwift 2.0, *) +private let _isLambdaManagedRuntimeRunning = Atomic(false) + +@available(LambdaSwift 2.0, *) +public final class LambdaManagedRuntime: Sendable where Handler: StreamingLambdaHandler & Sendable { + + @usableFromInline + let logger: Logger + + @usableFromInline + let eventLoop: EventLoop + + @usableFromInline + let handler: Handler + + public init( + handler: Handler, + eventLoop: EventLoop = Lambda.defaultEventLoop, + logger: Logger = Logger(label: "LambdaManagedRuntime") + ) { + self.handler = handler + self.eventLoop = eventLoop + + // by setting the log level here, we understand it can not be changed dynamically at runtime + // developers have to wait for AWS Lambda to dispose and recreate a runtime environment to pickup a change + // this approach is less flexible but more performant than reading the value of the environment variable at each invocation + var log = logger + + // use the LOG_LEVEL environment variable to set the log level. + // if the environment variable is not set, use the default log level from the logger provided + log.logLevel = Lambda.env("LOG_LEVEL").flatMap { .init(rawValue: $0) } ?? logger.logLevel + + self.logger = log + self.logger.debug("LambdaManagedRuntime initialized") + } + + #if !ServiceLifecycleSupport + public func run() async throws { + try await self._run() + } + #endif + + /// Starts the Runtime Interface Client (RIC), i.e. the loop that will poll events, + /// dispatch them to the Handler and push back results or errors. + /// This function makes sure only one run() is called at a time + internal func _run() async throws { + + // we use an atomic global variable to ensure only one LambdaRuntime is running at the time + let (_, original) = _isLambdaManagedRuntimeRunning.compareExchange( + expected: false, + desired: true, + ordering: .acquiringAndReleasing + ) + + // if the original value was already true, run() is already running + if original { + throw LambdaRuntimeError(code: .runtimeCanOnlyBeStartedOnce) + } + + defer { + _isLambdaManagedRuntimeRunning.store(false, ordering: .releasing) + } + + // are we running inside an AWS Lambda runtime environment ? + // AWS_LAMBDA_RUNTIME_API is set when running on Lambda + // https://docs.aws.amazon.com/lambda/latest/dg/runtimes-api.html + if let runtimeEndpoint = Lambda.env("AWS_LAMBDA_RUNTIME_API") { + + // Get the max concurrency authorized by user when running on + // Lambda Managed Instances + // See: + // - https://docs.aws.amazon.com/lambda/latest/dg/lambda-managed-instances.html#lambda-managed-instances-concurrency-model + // - https://docs.aws.amazon.com/lambda/latest/dg/configuration-envvars.html + // + // and the NodeJS implementation + // https://github.com/aws/aws-lambda-nodejs-runtime-interface-client/blob/a4560c87426fa0a34756296a30d7add1388e575c/src/utils/env.ts#L34 + // https://github.com/aws/aws-lambda-nodejs-runtime-interface-client/blob/a4560c87426fa0a34756296a30d7add1388e575c/src/worker/ignition.ts#L12 + let maxConcurrency = Int(Lambda.env("AWS_LAMBDA_MAX_CONCURRENCY") ?? "1") ?? 1 + + // when max concurrency is 1, do not pay the overhead of launching a Task + if maxConcurrency <= 1 { + self.logger.trace("Starting the Runtime Interface Client") + try await LambdaRuntime.startRuntimeInterfaceClient( + endpoint: runtimeEndpoint, + handler: self.handler, + eventLoop: self.eventLoop, + logger: self.logger + ) + } else { + + try await withThrowingTaskGroup(of: Void.self) { group in + + self.logger.trace("Starting \(maxConcurrency) Runtime Interface Clients") + for i in 0..: LambdaHandler, Sendable { + let body: @Sendable (Event, LambdaContext) async throws -> Output + + /// Initialize with a closure handler over generic `Input` and `Output` types. + /// - Parameter body: The handler function written as a closure. + public init(body: @Sendable @escaping (Event, LambdaContext) async throws -> Output) where Output: Encodable { + self.body = body + } + + /// Initialize with a closure handler over a generic `Input` type, and a `Void` `Output`. + /// - Parameter body: The handler function written as a closure. + public init(body: @Sendable @escaping (Event, LambdaContext) async throws -> Void) where Output == Void { + self.body = body + } + + /// Calls the provided `self.body` closure with the generic `Event` object representing the incoming event, and the ``LambdaContext`` + /// - Parameters: + /// - event: The generic `Event` object representing the invocation's input data. + /// - context: The ``LambdaContext`` containing the invocation's metadata. + public func handle(_ event: Event, context: LambdaContext) async throws -> Output { + try await self.body(event, context) + } +} +#endif diff --git a/Sources/AWSLambdaRuntime/Runtime/LambdaRuntime+Codable.swift b/Sources/AWSLambdaRuntime/Runtime/LambdaRuntime+Codable.swift index f20f5a47..27a7c3ac 100644 --- a/Sources/AWSLambdaRuntime/Runtime/LambdaRuntime+Codable.swift +++ b/Sources/AWSLambdaRuntime/Runtime/LambdaRuntime+Codable.swift @@ -39,7 +39,7 @@ public protocol LambdaOutputEncoder { func encode(_ value: Output, into buffer: inout ByteBuffer) throws } -public struct VoidEncoder: LambdaOutputEncoder { +public struct VoidEncoder: LambdaOutputEncoder, Sendable { public typealias Output = Void public init() {} diff --git a/Sources/AWSLambdaRuntime/Runtime/LambdaRuntime.swift b/Sources/AWSLambdaRuntime/Runtime/LambdaRuntime.swift index c7465b09..63bb1c95 100644 --- a/Sources/AWSLambdaRuntime/Runtime/LambdaRuntime.swift +++ b/Sources/AWSLambdaRuntime/Runtime/LambdaRuntime.swift @@ -20,7 +20,7 @@ import Synchronization // This is our guardian to ensure only one LambdaRuntime is running at the time // We use an Atomic here to ensure thread safety @available(LambdaSwift 2.0, *) -private let _isRunning = Atomic(false) +private let _isLambdaRuntimeRunning = Atomic(false) @available(LambdaSwift 2.0, *) public final class LambdaRuntime: Sendable where Handler: StreamingLambdaHandler { @@ -59,11 +59,17 @@ public final class LambdaRuntime: Sendable where Handler: StreamingLamb } #endif - /// Make sure only one run() is called at a time + /// Starts the Runtime Interface Client (RIC), i.e. the loop that will poll events, + /// dispatch them to the Handler and push back results or errors. + /// This function makes sure only one run() is called at a time internal func _run() async throws { // we use an atomic global variable to ensure only one LambdaRuntime is running at the time - let (_, original) = _isRunning.compareExchange(expected: false, desired: true, ordering: .acquiringAndReleasing) + let (_, original) = _isLambdaRuntimeRunning.compareExchange( + expected: false, + desired: true, + ordering: .acquiringAndReleasing + ) // if the original value was already true, run() is already running if original { @@ -71,7 +77,7 @@ public final class LambdaRuntime: Sendable where Handler: StreamingLamb } defer { - _isRunning.store(false, ordering: .releasing) + _isLambdaRuntimeRunning.store(false, ordering: .releasing) } // The handler can be non-sendable, we want to ensure we only ever have one copy of it @@ -85,71 +91,101 @@ public final class LambdaRuntime: Sendable where Handler: StreamingLamb // https://docs.aws.amazon.com/lambda/latest/dg/runtimes-api.html if let runtimeEndpoint = Lambda.env("AWS_LAMBDA_RUNTIME_API") { - let ipAndPort = runtimeEndpoint.split(separator: ":", maxSplits: 1) - let ip = String(ipAndPort[0]) - guard let port = Int(ipAndPort[1]) else { throw LambdaRuntimeError(code: .invalidPort) } - - do { - try await LambdaRuntimeClient.withRuntimeClient( - configuration: .init(ip: ip, port: port), - eventLoop: self.eventLoop, - logger: self.logger - ) { runtimeClient in - try await Lambda.runLoop( - runtimeClient: runtimeClient, - handler: handler, - logger: self.logger - ) - } - } catch { - // catch top level errors that have not been handled until now - // this avoids the runtime to crash and generate a backtrace - if let error = error as? LambdaRuntimeError, - error.code != .connectionToControlPlaneLost - { - // if the error is a LambdaRuntimeError but not a connection error, - // we rethrow it to preserve existing behaviour - self.logger.error("LambdaRuntime.run() failed with error", metadata: ["error": "\(error)"]) - throw error - } else { - self.logger.trace("LambdaRuntime.run() connection lost") - } - } + self.logger.trace("Starting the Runtime Interface Client") + try await LambdaRuntime.startRuntimeInterfaceClient( + endpoint: runtimeEndpoint, + handler: handler, + eventLoop: self.eventLoop, + logger: self.logger + ) } else { - #if LocalServerSupport - - // we're not running on Lambda and we're compiled in DEBUG mode, - // let's start a local server for testing + self.logger.trace("Starting the local test HTTP server") + try await LambdaRuntime.startLocalServer( + handler: handler, + eventLoop: self.eventLoop, + logger: self.logger + ) + } + } - let host = Lambda.env("LOCAL_LAMBDA_HOST") ?? "127.0.0.1" - let port = Lambda.env("LOCAL_LAMBDA_PORT").flatMap(Int.init) ?? 7000 - let endpoint = Lambda.env("LOCAL_LAMBDA_INVOCATION_ENDPOINT") + internal static func startRuntimeInterfaceClient( + endpoint: String, + handler: Handler, + eventLoop: EventLoop, + logger: Logger + ) async throws { + + let ipAndPort = endpoint.split(separator: ":", maxSplits: 1) + let ip = String(ipAndPort[0]) + guard let port = Int(ipAndPort[1]) else { throw LambdaRuntimeError(code: .invalidPort) } + + do { + try await LambdaRuntimeClient.withRuntimeClient( + configuration: .init(ip: ip, port: port), + eventLoop: eventLoop, + logger: logger + ) { runtimeClient in + try await Lambda.runLoop( + runtimeClient: runtimeClient, + handler: handler, + logger: logger + ) + } + } catch { + // catch top level errors that have not been handled until now + // this avoids the runtime to crash and generate a backtrace + if let error = error as? LambdaRuntimeError, + error.code != .connectionToControlPlaneLost + { + // if the error is a LambdaRuntimeError but not a connection error, + // we rethrow it to preserve existing behaviour + logger.error("LambdaRuntime.run() failed with error", metadata: ["error": "\(error)"]) + throw error + } else { + logger.trace("LambdaRuntime.run() connection lost") + } + } + } - try await Lambda.withLocalServer( - host: host, - port: port, - invocationEndpoint: endpoint, - logger: self.logger - ) { - - try await LambdaRuntimeClient.withRuntimeClient( - configuration: .init(ip: host, port: port), - eventLoop: self.eventLoop, - logger: self.logger - ) { runtimeClient in - try await Lambda.runLoop( - runtimeClient: runtimeClient, - handler: handler, - logger: self.logger - ) - } + internal static func startLocalServer( + handler: sending Handler, + eventLoop: EventLoop, + logger: Logger + ) async throws { + #if LocalServerSupport + + // we're not running on Lambda and we're compiled in DEBUG mode, + // let's start a local server for testing + + let host = Lambda.env("LOCAL_LAMBDA_HOST") ?? "127.0.0.1" + let port = Lambda.env("LOCAL_LAMBDA_PORT").flatMap(Int.init) ?? 7000 + let endpoint = Lambda.env("LOCAL_LAMBDA_INVOCATION_ENDPOINT") + + try await Lambda.withLocalServer( + host: host, + port: port, + invocationEndpoint: endpoint, + logger: logger + ) { + + try await LambdaRuntimeClient.withRuntimeClient( + configuration: .init(ip: host, port: port), + eventLoop: eventLoop, + logger: logger + ) { runtimeClient in + try await Lambda.runLoop( + runtimeClient: runtimeClient, + handler: handler, + logger: logger + ) } - #else - // When the LocalServerSupport trait is disabled, we can't start a local server because the local server code is not compiled. - throw LambdaRuntimeError(code: .missingLambdaRuntimeAPIEnvironmentVariable) - #endif } + #else + // When the LocalServerSupport trait is disabled, we can't start a local server because the local server code is not compiled. + throw LambdaRuntimeError(code: .missingLambdaRuntimeAPIEnvironmentVariable) + #endif } + } diff --git a/Tests/AWSLambdaRuntimeTests/LambdaManagedRuntimeTests.swift b/Tests/AWSLambdaRuntimeTests/LambdaManagedRuntimeTests.swift new file mode 100644 index 00000000..a948d4af --- /dev/null +++ b/Tests/AWSLambdaRuntimeTests/LambdaManagedRuntimeTests.swift @@ -0,0 +1,200 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the SwiftAWSLambdaRuntime open source project +// +// Copyright SwiftAWSLambdaRuntime project authors +// Copyright (c) Amazon.com, Inc. or its affiliates. +// Licensed under Apache License v2.0 +// +// See LICENSE.txt for license information +// See CONTRIBUTORS.txt for the list of SwiftAWSLambdaRuntime project authors +// +// SPDX-License-Identifier: Apache-2.0 +// +//===----------------------------------------------------------------------===// + +#if ManagedRuntimeSupport + +import Foundation +import Logging +import NIOCore +import Synchronization +import Testing + +@testable import AWSLambdaRuntime + +@Suite(.serialized) +struct LambdaManagedRuntimeTests { + + // Test 1: Concurrent Handler Execution + @Test("LambdaManagedRuntime handler handles concurrent invocations") + @available(LambdaSwift 2.0, *) + func testConcurrentHandlerExecution() async throws { + let handler = ConcurrentMockHandler() + + let invocationCount = 5 + + let results = try await withThrowingTaskGroup(of: String.self) { group in + // Simulate concurrent invocations + for i in 0..(JSONEncoder()) + + let concurrentTasks = 10 + + let results = try await withThrowingTaskGroup(of: String.self) { group in + for i in 0.. LambdaContext { + LambdaContext.__forTestsOnly( + requestID: "test-request-id", + traceID: "test-trace-id", + tenantID: "test-tenant-id", + invokedFunctionARN: "arn:aws:lambda:us-east-1:123456789012:function:test", + timeout: .seconds(30), + logger: Logger(label: "MockedLambdaContext") + ) + } +} diff --git a/readme.md b/readme.md index d543bc73..d24f6055 100644 --- a/readme.md +++ b/readme.md @@ -430,6 +430,80 @@ struct LambdaFunction { You can see a complete working example in the [ServiceLifecycle+Postgres example](Examples/ServiceLifecycle+Postgres/README.md), which demonstrates how to manage a PostgreSQL client alongside the Lambda runtime using ServiceLifecycle. +### Lambda Managed Instances + +[Lambda Managed Instances](https://docs.aws.amazon.com/lambda/latest/dg/lambda-managed-instances.html) enables you to run Lambda functions on your current-generation Amazon EC2 instances while maintaining serverless simplicity. This deployment model provides EC2 flexibility and cost optimization by running your functions on customer-owned EC2 instances, while AWS handles all infrastructure management tasks including instance lifecycle, OS and runtime patching, routing, load balancing, and auto-scaling. + +To deploy a Swift Lambda function to Lambda Managed Instances, you need to make two key changes to your code: + +#### 1. Use `LambdaManagedRuntime` instead of `LambdaRuntime` + +Replace your standard `LambdaRuntime` initialization with `LambdaManagedRuntime`: + +```swift +import AWSLambdaRuntime + +// Standard Lambda function - change this: +// let runtime = LambdaRuntime { ... } + +// Lambda Managed Instances - to this: +let runtime = LambdaManagedRuntime { + (event: HelloRequest, context: LambdaContext) in + + HelloResponse(greetings: "Hello \(event.name)!") +} + +try await runtime.run() +``` + +#### 2. Ensure Handler Functions and Structs are `Sendable` + +Because Lambda Managed Instances can run functions concurrently on the same EC2 host, your handler functions or the structs containing them must conform to the `Sendable` protocol: + +```swift +import AWSLambdaRuntime + +// For struct-based handlers, explicitly conform to Sendable +struct MyHandler: LambdaWithBackgroundProcessingHandler, Sendable { + typealias Event = MyRequest + typealias Output = MyResponse + + func handle( + _ event: Event, + outputWriter: some LambdaResponseWriter, + context: LambdaContext + ) async throws { + // Your handler logic here + try await outputWriter.write(MyResponse(message: "Processed")) + } +} + +// Use LambdaCodableAdapterSendable for struct handlers +let adapter = LambdaCodableAdapterSendable(handler: MyHandler()) +let runtime = LambdaManagedRuntime(handler: adapter) +try await runtime.run() +``` + +For simple data structures, the Swift compiler automatically infers `Sendable` conformance, but you should explicitly declare it for clarity and safety. + +#### Key Benefits + +- **EC2 Flexibility**: Run on specialized EC2 instance types including Graviton4 and network-optimized instances +- **Cost Optimization**: Better cost efficiency for sustained workloads +- **Serverless Simplicity**: AWS manages all infrastructure concerns while you focus on code +- **Concurrent Execution**: Functions can run concurrently on the same host for improved throughput + +#### Prerequisites + +Before deploying to Lambda Managed Instances, you need to: + +1. Create a [Lambda Managed Instances capacity provider](https://docs.aws.amazon.com/lambda/latest/dg/lambda-managed-instances-capacity-providers.html) +2. Configure your deployment to reference the capacity provider ARN + +You can see complete working examples in the [ManagedInstances example directory](Examples/ManagedInstances/README.md), which demonstrates deploying HelloJSON, Streaming, and BackgroundTasks functions to Lambda Managed Instances using AWS SAM. + +For more information, see the [AWS Lambda Managed Instances documentation](https://docs.aws.amazon.com/lambda/latest/dg/lambda-managed-instances.html) and the [execution environment guide](https://docs.aws.amazon.com/lambda/latest/dg/lambda-managed-instances-execution-environment.html). + ### Use Lambda Background Tasks Background tasks allow code to execute asynchronously after the main response has been returned, enabling additional processing without affecting response latency. This approach is ideal for scenarios like logging, data updates, or notifications that can be deferred. The code leverages Lambda's "Response Streaming" feature, which is effective for balancing real-time user responsiveness with the ability to perform extended tasks post-response. For more information about Lambda background tasks, see [this AWS blog post](https://aws.amazon.com/blogs/compute/running-code-after-returning-a-response-from-an-aws-lambda-function/). diff --git a/scripts/extract_aws_credentials.sh b/scripts/extract_aws_credentials.sh deleted file mode 100755 index e26c23f3..00000000 --- a/scripts/extract_aws_credentials.sh +++ /dev/null @@ -1,123 +0,0 @@ -#!/bin/bash -##===----------------------------------------------------------------------===## -## -## This source file is part of the SwiftAWSLambdaRuntime open source project -## -## Copyright SwiftAWSLambdaRuntime project authors -## Copyright (c) Amazon.com, Inc. or its affiliates. -## Licensed under Apache License v2.0 -## -## See LICENSE.txt for license information -## See CONTRIBUTORS.txt for the list of SwiftAWSLambdaRuntime project authors -## -## SPDX-License-Identifier: Apache-2.0 -## -##===----------------------------------------------------------------------===## - -# Extract AWS credentials from ~/.aws/credentials and ~/.aws/config (default profile) -# and set environment variables - -set -e - -# Default profile name -PROFILE="default" - -# Check if a different profile is specified as argument -if [ $# -eq 1 ]; then - PROFILE="$1" -fi - -# AWS credentials file path -CREDENTIALS_FILE="$HOME/.aws/credentials" -CONFIG_FILE="$HOME/.aws/config" - -# Check if credentials file exists -if [ ! -f "$CREDENTIALS_FILE" ]; then - echo "Error: AWS credentials file not found at $CREDENTIALS_FILE" - exit 1 -fi - -# Function to extract value from AWS config files -extract_value() { - local file="$1" - local profile="$2" - local key="$3" - - # Use awk to extract the value for the specified profile and key - awk -v profile="[$profile]" -v key="$key" ' - BEGIN { in_profile = 0 } - $0 == profile { in_profile = 1; next } - /^\[/ && $0 != profile { in_profile = 0 } - in_profile && $0 ~ "^" key " *= *" { - gsub("^" key " *= *", "") - gsub(/^[ \t]+|[ \t]+$/, "") # trim whitespace - print $0 - exit - } - ' "$file" -} - -# Extract credentials -AWS_ACCESS_KEY_ID=$(extract_value "$CREDENTIALS_FILE" "$PROFILE" "aws_access_key_id") -AWS_SECRET_ACCESS_KEY=$(extract_value "$CREDENTIALS_FILE" "$PROFILE" "aws_secret_access_key") -AWS_SESSION_TOKEN=$(extract_value "$CREDENTIALS_FILE" "$PROFILE" "aws_session_token") - -# Extract region from config file (try both credentials and config files) -AWS_REGION=$(extract_value "$CREDENTIALS_FILE" "$PROFILE" "region") -if [ -z "$AWS_REGION" ] && [ -f "$CONFIG_FILE" ]; then - # Try config file with profile prefix for non-default profiles - if [ "$PROFILE" = "default" ]; then - AWS_REGION=$(extract_value "$CONFIG_FILE" "$PROFILE" "region") - else - AWS_REGION=$(extract_value "$CONFIG_FILE" "profile $PROFILE" "region") - fi -fi - -# Validate required credentials -if [ -z "$AWS_ACCESS_KEY_ID" ]; then - echo "Error: aws_access_key_id not found for profile '$PROFILE'" - exit 1 -fi - -if [ -z "$AWS_SECRET_ACCESS_KEY" ]; then - echo "Error: aws_secret_access_key not found for profile '$PROFILE'" - exit 1 -fi - -# Set default region if not found -if [ -z "$AWS_REGION" ]; then - AWS_REGION="us-east-1" - echo "Warning: No region found for profile '$PROFILE', defaulting to us-east-1" -fi - -# Export environment variables -export AWS_REGION="$AWS_REGION" -export AWS_ACCESS_KEY_ID="$AWS_ACCESS_KEY_ID" -export AWS_SECRET_ACCESS_KEY="$AWS_SECRET_ACCESS_KEY" - -# Only export session token if it exists (for temporary credentials) -if [ -n "$AWS_SESSION_TOKEN" ]; then - export AWS_SESSION_TOKEN="$AWS_SESSION_TOKEN" -fi - -# Print confirmation (without sensitive values) -echo "AWS credentials loaded for profile: $PROFILE" -echo "AWS_REGION: $AWS_REGION" -echo "AWS_ACCESS_KEY_ID: ${AWS_ACCESS_KEY_ID:0:4}****" -echo "AWS_SECRET_ACCESS_KEY: ****" -if [ -n "$AWS_SESSION_TOKEN" ]; then - echo "AWS_SESSION_TOKEN: ****" -fi - -# Optional: Print export commands for manual sourcing -echo "" -echo "To use these credentials in your current shell, run:" -echo "source $(basename "$0")" -echo "" -echo "Or copy and paste these export commands:" -echo "export AWS_REGION='$AWS_REGION'" -echo "export AWS_ACCESS_KEY_ID='$AWS_ACCESS_KEY_ID'" -echo "export AWS_SECRET_ACCESS_KEY='$AWS_SECRET_ACCESS_KEY'" -if [ -n "$AWS_SESSION_TOKEN" ]; then - echo "export AWS_SESSION_TOKEN='$AWS_SESSION_TOKEN'" -fi \ No newline at end of file