Add support for Lambda Managed Instances without changing the public API [Convenience + Example] (#623)

This PR builds on
https://github.com/awslabs/swift-aws-lambda-runtime/pull/629 to add
convenience structs (Handlers and Adapters) that are `Sendable`

**Changes**

- **Added Sendable adapter types**: Implemented `ClosureHandlerSendable`
- a thread-safe version of existing closure handler that enforces
`Sendable` conformance for concurrent execution environments - and added
conditional conformance to `Sendable` for other Adapters when the
Handler is `Sendable`

- **Enhanced handler protocols for concurrency**: Extended handler
protocols to support `Sendable` constraints and concurrent response
writing through `LambdaResponseStreamWriter & Sendable`, enabling safe
multi-threaded invocation processing

- **Created comprehensive Lambda Managed Instances examples**: Built
three demonstration functions showcasing concurrent execution
capabilities, streaming responses, and background processing patterns
specific to the new managed instances deployment model

**Context**
Lambda Managed Instances support multi-concurrent invocations where
multiple invocations execute simultaneously within the same execution
environment. The runtime now detects the configured concurrency level
and launches the appropriate number of RICs to handle concurrent
requests efficiently.

When `AWS_LAMBDA_MAX_CONCURRENCY` is 1 or unset, the runtime maintains
the existing single-threaded behaviour for optimal performance on
traditional Lambda deployments.

---------

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
Co-authored-by: Sebastien Stormacq <stormacq@amazon.lu>
This commit is contained in:
Sébastien Stormacq
2026-02-12 00:32:31 +01:00
committed by GitHub
parent d456396581
commit 190eb81876
15 changed files with 675 additions and 36 deletions
+1 -1
View File
@@ -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
+3
View File
@@ -0,0 +1,3 @@
response.json
samconfig.toml
Makefile
+48
View File
@@ -0,0 +1,48 @@
// 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)
// When using the below line, use LAMBDA_USE_LOCAL_DEPS=../.. for swift package archive command, e.g.
// `LAMBDA_USE_LOCAL_DEPS=../.. swift package archive --allow-network-connections docker`
.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"
),
]
)
+129
View File
@@ -0,0 +1,129 @@
# 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
# when compiling a standalone or new project
swift package archive --allow-network-connections docker
# When compiling the example in this repository
# LAMBDA_USE_LOCAL_DEPS=../.. swift package archive --allow-network-connections docker
# Change the values below to match your setup
REGION=us-west-2
CAPACITY_PROVIDER=arn:aws:lambda:us-west-2:<YOUR ACCOUNT ID>:capacity-provider:<YOUR CAPACITY PROVIDER NAME>
# 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}
```
@@ -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<Output>,
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 = LambdaCodableAdapter(handler: BackgroundProcessingHandler())
let runtime = LambdaManagedRuntime(handler: adapter)
try await runtime.run()
@@ -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()
@@ -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()
+81
View File
@@ -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"
+1 -1
View File
@@ -35,7 +35,7 @@ let package = Package(
.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"),
.package(url: "https://github.com/swift-server/swift-service-lifecycle.git", from: "2.9.1"),
],
targets: [
.target(
@@ -0,0 +1,152 @@
//===----------------------------------------------------------------------===//
//
// 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 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 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.
/// - 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<Event: Decodable, Output>(
decoder: JSONDecoder = JSONDecoder(),
encoder: JSONEncoder = JSONEncoder(),
logger: Logger = Logger(label: "LambdaManagedRuntime"),
body: @Sendable @escaping (Event, LambdaContext) async throws -> Output
)
where
Handler == LambdaCodableAdapter<
LambdaHandlerAdapter<Event, Output, ClosureHandlerSendable<Event, Output>>,
Event,
Output,
LambdaJSONEventDecoder,
LambdaJSONOutputEncoder<Output>
>
{
let handler = LambdaCodableAdapter(
encoder: encoder,
decoder: decoder,
handler: LambdaHandlerAdapter(handler: ClosureHandlerSendable(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<Event: Decodable>(
decoder: JSONDecoder = JSONDecoder(),
logger: Logger = Logger(label: "LambdaRuntime"),
body: @Sendable @escaping (Event, LambdaContext) async throws -> Void
)
where
Handler == LambdaCodableAdapter<
LambdaHandlerAdapter<Event, Void, ClosureHandlerSendable<Event, Void>>,
Event,
Void,
LambdaJSONEventDecoder,
VoidEncoder
>
{
let handler = LambdaCodableAdapter(
decoder: LambdaJSONEventDecoder(decoder),
handler: LambdaHandlerAdapter(handler: ClosureHandlerSendable(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` and `Sendable` protocols, whose `Event` is `Decodable` and `Output` is `Void`
public convenience init<Event: Decodable, LHandler: LambdaHandler & Sendable>(
decoder: JSONDecoder = JSONDecoder(),
logger: Logger = Logger(label: "LambdaRuntime"),
lambdaHandler: LHandler
)
where
Handler == LambdaCodableAdapter<
LambdaHandlerAdapter<Event, Void, LHandler>,
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` and `Sendable` protocols, whose `Event` is `Decodable` and `Output` is `Encodable`
public convenience init<Event: Decodable, Output, LHandler: LambdaHandler & Sendable>(
decoder: JSONDecoder = JSONDecoder(),
encoder: JSONEncoder = JSONEncoder(),
logger: Logger = Logger(label: "LambdaRuntime"),
lambdaHandler: LHandler
)
where
Handler == LambdaCodableAdapter<
LambdaHandlerAdapter<Event, Output, LHandler>,
Event,
Output,
LambdaJSONEventDecoder,
LambdaJSONOutputEncoder<Output>
>,
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
#endif // trait: ManagedRuntimeSupport
@@ -26,7 +26,7 @@ import class Foundation.JSONEncoder
import Logging
public struct LambdaJSONEventDecoder: LambdaEventDecoder {
public struct LambdaJSONEventDecoder: LambdaEventDecoder, Sendable {
@usableFromInline let jsonDecoder: JSONDecoder
@inlinable
@@ -46,7 +46,7 @@ public struct LambdaJSONEventDecoder: LambdaEventDecoder {
}
}
public struct LambdaJSONOutputEncoder<Output: Encodable>: LambdaOutputEncoder {
public struct LambdaJSONOutputEncoder<Output: Encodable>: LambdaOutputEncoder, Sendable {
@usableFromInline let jsonEncoder: JSONEncoder
@inlinable
@@ -0,0 +1,47 @@
//===----------------------------------------------------------------------===//
//
// 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 Logging
import NIOCore
#if ManagedRuntimeSupport
/// A ``LambdaHandler`` conforming handler object that can be constructed with a closure.
/// Allows for a handler to be defined in a clean manner, leveraging Swift's trailing closure syntax.
@available(LambdaSwift 2.0, *)
public struct ClosureHandlerSendable<Event: Decodable, Output>: 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
@@ -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() {}
@@ -81,6 +81,10 @@ public struct LambdaHandlerAdapter<
}
}
@available(LambdaSwift 2.0, *)
// Add Sendable conformance when components are Sendable
extension LambdaHandlerAdapter: Sendable where Handler: Sendable {}
/// Adapts a ``LambdaWithBackgroundProcessingHandler`` conforming handler to conform to ``StreamingLambdaHandler``.
@available(LambdaSwift 2.0, *)
public struct LambdaCodableAdapter<
@@ -139,6 +143,11 @@ public struct LambdaCodableAdapter<
}
}
@available(LambdaSwift 2.0, *)
// Add Sendable conformance when components are Sendable
extension LambdaCodableAdapter: Sendable
where Handler: Sendable, Encoder: Sendable, Decoder: Sendable {}
/// A ``LambdaResponseStreamWriter`` wrapper that conforms to ``LambdaResponseWriter``.
public struct LambdaCodableResponseWriter<Output, Encoder: LambdaOutputEncoder, Base: LambdaResponseStreamWriter>:
LambdaResponseWriter
@@ -80,40 +80,40 @@ struct LambdaManagedRuntimeTests {
}
// Test 3: Thread-Safe Adapter Tests
// @Test("Sendable adapters work with concurrent execution")
// @available(LambdaSwift 2.0, *)
// func testSendableAdapters() async throws {
// let decoder = LambdaJSONEventDecoderSendable(JSONDecoder())
// let encoder = LambdaJSONOutputEncoderSendable<String>(JSONEncoder())
@Test("Sendable adapters work with concurrent execution")
@available(LambdaSwift 2.0, *)
func testSendableAdapters() async throws {
let decoder = LambdaJSONEventDecoder(JSONDecoder())
let encoder = LambdaJSONOutputEncoder<String>(JSONEncoder())
// let concurrentTasks = 10
let concurrentTasks = 10
// let results = try await withThrowingTaskGroup(of: String.self) { group in
// for i in 0..<concurrentTasks {
// group.addTask {
// // Test concurrent decoding
// let inputBuffer = ByteBuffer(string: #"{"message": "test-\#(i)"}"#)
// let decoded = try decoder.decode(TestEvent.self, from: inputBuffer)
let results = try await withThrowingTaskGroup(of: String.self) { group in
for i in 0..<concurrentTasks {
group.addTask {
// Test concurrent decoding
let inputBuffer = ByteBuffer(string: #"{"message": "test-\#(i)"}"#)
let decoded = try decoder.decode(TestEvent.self, from: inputBuffer)
// // Test concurrent encoding
// let output = "response-\(i)"
// var encoded = ByteBuffer()
// try encoder.encode(output, into: &encoded)
// Test concurrent encoding
let output = "response-\(i)"
var encoded = ByteBuffer()
try encoder.encode(output, into: &encoded)
// return "\(decoded.message)-\(String(buffer: encoded))"
// }
// }
return "\(decoded.message)-\(String(buffer: encoded))"
}
}
// var collectedResults: [String] = []
// for try await result in group {
// collectedResults.append(result)
// }
// return collectedResults
// }
var collectedResults: [String] = []
for try await result in group {
collectedResults.append(result)
}
return collectedResults
}
// #expect(results.count == concurrentTasks)
// #expect(results.allSatisfy { $0.contains("test-") && $0.contains("response-") })
// }
#expect(results.count == concurrentTasks)
#expect(results.allSatisfy { $0.contains("test-") && $0.contains("response-") })
}
// Test 4: Concurrency Level Detection
@Test("Runtime detects AWS_LAMBDA_MAX_CONCURRENCY configuration")
+3 -3
View File
@@ -478,13 +478,13 @@ struct MyHandler: LambdaWithBackgroundProcessingHandler, Sendable {
}
}
// Use LambdaCodableAdapterSendable for struct handlers
let adapter = LambdaCodableAdapterSendable(handler: MyHandler())
// Just like with LambdaRuntime, use LambdaCodableAdapter to pass it to LambdaManagedruntime
let adapter = LambdaCodableAdapter(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.
For simple data structures, the Swift compiler automatically infers `Sendable` conformance, but we recommand declaring it explicitly for clarity and safety.
#### Key Benefits