mirror of
https://github.com/swift-server/swift-aws-lambda-runtime.git
synced 2026-05-03 07:22:27 +00:00
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:
committed by
GitHub
parent
d456396581
commit
190eb81876
@@ -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
|
||||
|
||||
|
||||
@@ -0,0 +1,3 @@
|
||||
response.json
|
||||
samconfig.toml
|
||||
Makefile
|
||||
@@ -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"
|
||||
),
|
||||
]
|
||||
)
|
||||
@@ -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()
|
||||
@@ -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
@@ -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")
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
Reference in New Issue
Block a user