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 [CORE] (#629)
AWS launched [Lambda Managed Instances](https://docs.aws.amazon.com/lambda/latest/dg/lambda-managed-instances.html), i.e Lambda functions running on EC2 instances. This comes with [a major change in the programming model](https://docs.aws.amazon.com/lambda/latest/dg/lambda-managed-instances.html#lambda-managed-instances-concurrency-model) as function handlers are now allowed to run concurrently on the same machine (multiple in flight events being processed in parallel in the same execution environment). The maximum concurrency per runtime environment is controlled by the user. This PR adds support for running multiple Runtime Interface Clients (RICs) concurrently when deployed on Lambda Managed Instances, enabling the runtime to handle multiple invocations simultaneously within a single execution environment. This PR is a followup to https://github.com/awslabs/swift-aws-lambda-runtime/pull/617 which used another approach to support Lambda Managed Instances by changing the public API and requiring that all handlers must conform to `Sendable`. The original PR was closed as we agreed that only a fraction of the Lambda functions will be deployed on EC2 and it was not worth adding a `Sendable` requirement for all. **Changes** - **Introduced thread-safe LambdaManagedRuntime**: Created new Sendable-conforming runtime class that supports concurrent handler execution with atomic guards to prevent multiple runtime instances and thread-safe handler requirements (`Handler: StreamingLambdaHandler & Sendable`) - **Implemented ServiceLifecycle integration**: Added managed runtime support for structured concurrency lifecycle management, allowing proper startup/shutdown coordination in multi-concurrent environments This PR contains only changes to the core runtime, convenience functions, handlers, adapters, and a comprehensive example will be added in a follow up PR. **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
ba48cf70b0
commit
d456396581
+6
-4
@@ -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(
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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
|
||||
@@ -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 ManagedRuntimeSupport
|
||||
|
||||
import Logging
|
||||
import NIOCore
|
||||
import Synchronization
|
||||
|
||||
@available(LambdaSwift 2.0, *)
|
||||
public final class LambdaManagedRuntime<Handler>: 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 {
|
||||
|
||||
try await withRuntimeGuard {
|
||||
|
||||
// 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..<maxConcurrency {
|
||||
|
||||
group.addTask {
|
||||
var logger = self.logger
|
||||
logger[metadataKey: "RIC"] = "\(i)"
|
||||
try await LambdaRuntime.startRuntimeInterfaceClient(
|
||||
endpoint: runtimeEndpoint,
|
||||
handler: self.handler,
|
||||
eventLoop: self.eventLoop,
|
||||
logger: logger
|
||||
)
|
||||
}
|
||||
}
|
||||
// Wait for all tasks to complete and propagate any errors
|
||||
try await group.waitForAll()
|
||||
}
|
||||
}
|
||||
|
||||
} else {
|
||||
|
||||
self.logger.trace("Starting the local test HTTP server")
|
||||
try await LambdaRuntime.startLocalServer(
|
||||
handler: self.handler,
|
||||
eventLoop: self.eventLoop,
|
||||
logger: self.logger
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
#endif
|
||||
@@ -22,6 +22,22 @@ import Synchronization
|
||||
@available(LambdaSwift 2.0, *)
|
||||
private let _isLambdaRuntimeRunning = Atomic<Bool>(false)
|
||||
|
||||
// Shared guard helper
|
||||
@available(LambdaSwift 2.0, *)
|
||||
internal func withRuntimeGuard<T>(_ body: () async throws -> T) async throws -> T {
|
||||
// we use an atomic global variable to ensure only one LambdaRuntime is running at the time
|
||||
let (_, original) = _isLambdaRuntimeRunning.compareExchange(
|
||||
expected: false,
|
||||
desired: true,
|
||||
ordering: .acquiringAndReleasing
|
||||
)
|
||||
guard !original else { throw LambdaRuntimeError(code: .runtimeCanOnlyBeStartedOnce) }
|
||||
|
||||
defer { _isLambdaRuntimeRunning.store(false, ordering: .releasing) }
|
||||
|
||||
return try await body()
|
||||
}
|
||||
|
||||
@available(LambdaSwift 2.0, *)
|
||||
public final class LambdaRuntime<Handler>: Sendable where Handler: StreamingLambdaHandler {
|
||||
@usableFromInline
|
||||
@@ -64,49 +80,36 @@ public final class LambdaRuntime<Handler>: Sendable where Handler: StreamingLamb
|
||||
/// 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) = _isLambdaRuntimeRunning.compareExchange(
|
||||
expected: false,
|
||||
desired: true,
|
||||
ordering: .acquiringAndReleasing
|
||||
)
|
||||
try await withRuntimeGuard {
|
||||
|
||||
// if the original value was already true, run() is already running
|
||||
if original {
|
||||
throw LambdaRuntimeError(code: .runtimeCanOnlyBeStartedOnce)
|
||||
}
|
||||
// The handler can be non-sendable, we want to ensure we only ever have one copy of it
|
||||
let handler = try? self.handlerStorage.get()
|
||||
guard let handler else {
|
||||
throw LambdaRuntimeError(code: .handlerCanOnlyBeGetOnce)
|
||||
}
|
||||
|
||||
defer {
|
||||
_isLambdaRuntimeRunning.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") {
|
||||
|
||||
// The handler can be non-sendable, we want to ensure we only ever have one copy of it
|
||||
let handler = try? self.handlerStorage.get()
|
||||
guard let handler else {
|
||||
throw LambdaRuntimeError(code: .handlerCanOnlyBeGetOnce)
|
||||
}
|
||||
self.logger.trace("Starting the Runtime Interface Client")
|
||||
try await LambdaRuntime.startRuntimeInterfaceClient(
|
||||
endpoint: runtimeEndpoint,
|
||||
handler: handler,
|
||||
eventLoop: self.eventLoop,
|
||||
logger: self.logger
|
||||
)
|
||||
|
||||
// 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") {
|
||||
} else {
|
||||
|
||||
self.logger.trace("Starting the Runtime Interface Client")
|
||||
try await LambdaRuntime.startRuntimeInterfaceClient(
|
||||
endpoint: runtimeEndpoint,
|
||||
handler: handler,
|
||||
eventLoop: self.eventLoop,
|
||||
logger: self.logger
|
||||
)
|
||||
|
||||
} else {
|
||||
|
||||
self.logger.trace("Starting the local test HTTP server")
|
||||
try await LambdaRuntime.startLocalServer(
|
||||
handler: handler,
|
||||
eventLoop: self.eventLoop,
|
||||
logger: self.logger
|
||||
)
|
||||
self.logger.trace("Starting the local test HTTP server")
|
||||
try await LambdaRuntime.startLocalServer(
|
||||
handler: handler,
|
||||
eventLoop: self.eventLoop,
|
||||
logger: self.logger
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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..<invocationCount {
|
||||
group.addTask {
|
||||
let buffer = ByteBuffer(string: "test-\(i)")
|
||||
let writer = MockResponseWriter()
|
||||
let context = LambdaContext.makeTest()
|
||||
|
||||
var mutableHandler = handler
|
||||
try await mutableHandler.handle(buffer, responseWriter: writer, context: context)
|
||||
|
||||
return "completed-\(i)"
|
||||
}
|
||||
}
|
||||
|
||||
var collectedResults: [String] = []
|
||||
for try await result in group {
|
||||
collectedResults.append(result)
|
||||
}
|
||||
return collectedResults
|
||||
}
|
||||
|
||||
#expect(results.count == invocationCount)
|
||||
}
|
||||
|
||||
// Test 2: Sendable Constraint Enforcement (Compilation Test)
|
||||
@Test("LambdaManagedRuntime enforces Sendable handler requirements")
|
||||
@available(LambdaSwift 2.0, *)
|
||||
func testSendableHandlerRequirement() {
|
||||
// This test verifies that only Sendable handlers compile
|
||||
let sendableHandler = SendableMockHandler()
|
||||
|
||||
// This should compile successfully
|
||||
let _ = LambdaManagedRuntime(
|
||||
handler: sendableHandler,
|
||||
eventLoop: Lambda.defaultEventLoop
|
||||
)
|
||||
|
||||
// Non-Sendable handlers would fail at compile time
|
||||
// Uncomment to verify compilation failure:
|
||||
// let nonSendableHandler = NonSendableMockHandler()
|
||||
// let _ = LambdaManagedRuntime(handler: nonSendableHandler) // Should not compile
|
||||
|
||||
}
|
||||
|
||||
// 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())
|
||||
|
||||
// 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)
|
||||
|
||||
// // Test concurrent encoding
|
||||
// let output = "response-\(i)"
|
||||
// var encoded = ByteBuffer()
|
||||
// try encoder.encode(output, into: &encoded)
|
||||
|
||||
// return "\(decoded.message)-\(String(buffer: encoded))"
|
||||
// }
|
||||
// }
|
||||
|
||||
// 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-") })
|
||||
// }
|
||||
|
||||
// Test 4: Concurrency Level Detection
|
||||
@Test("Runtime detects AWS_LAMBDA_MAX_CONCURRENCY configuration")
|
||||
@available(LambdaSwift 2.0, *)
|
||||
func testConcurrencyLevelDetection() async throws {
|
||||
// Test with concurrency = 1 (should behave like traditional runtime)
|
||||
setenv("AWS_LAMBDA_MAX_CONCURRENCY", "1", 1)
|
||||
defer { unsetenv("AWS_LAMBDA_MAX_CONCURRENCY") }
|
||||
|
||||
#expect(throws: Never.self) {
|
||||
let handler = ConcurrentMockHandler()
|
||||
let _ = LambdaManagedRuntime(
|
||||
handler: handler,
|
||||
eventLoop: Lambda.defaultEventLoop,
|
||||
logger: Logger(label: "ConcurrencyTest")
|
||||
)
|
||||
|
||||
// Test with higher concurrency
|
||||
setenv("AWS_LAMBDA_MAX_CONCURRENCY", "8", 1)
|
||||
|
||||
let _ = LambdaManagedRuntime(
|
||||
handler: handler,
|
||||
eventLoop: Lambda.defaultEventLoop,
|
||||
logger: Logger(label: "HighConcurrencyTest")
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Mock Types
|
||||
|
||||
@available(LambdaSwift 2.0, *)
|
||||
struct ConcurrentMockHandler: StreamingLambdaHandler, Sendable {
|
||||
mutating func handle(
|
||||
_ event: ByteBuffer,
|
||||
responseWriter: some LambdaResponseStreamWriter,
|
||||
context: LambdaContext
|
||||
) async throws {
|
||||
// Simulate some async work
|
||||
try await Task.sleep(for: .milliseconds(10))
|
||||
|
||||
let response = ByteBuffer(string: "processed: \(String(buffer: event))")
|
||||
try await responseWriter.writeAndFinish(response)
|
||||
}
|
||||
}
|
||||
|
||||
@available(LambdaSwift 2.0, *)
|
||||
struct SendableMockHandler: StreamingLambdaHandler, Sendable {
|
||||
mutating func handle(
|
||||
_ event: ByteBuffer,
|
||||
responseWriter: some LambdaResponseStreamWriter,
|
||||
context: LambdaContext
|
||||
) async throws {
|
||||
let response = ByteBuffer(string: "sendable response")
|
||||
try await responseWriter.writeAndFinish(response)
|
||||
}
|
||||
}
|
||||
|
||||
// Non-Sendable handler for compilation test
|
||||
@available(LambdaSwift 2.0, *)
|
||||
struct NonSendableMockHandler: StreamingLambdaHandler {
|
||||
var nonSendableProperty = NSMutableArray() // Not Sendable
|
||||
|
||||
mutating func handle(
|
||||
_ event: ByteBuffer,
|
||||
responseWriter: some LambdaResponseStreamWriter,
|
||||
context: LambdaContext
|
||||
) async throws {
|
||||
let response = ByteBuffer(string: "non-sendable response")
|
||||
try await responseWriter.writeAndFinish(response)
|
||||
}
|
||||
}
|
||||
|
||||
struct TestEvent: Codable {
|
||||
let message: String
|
||||
}
|
||||
|
||||
struct MockResponseWriter: LambdaResponseStreamWriter, Sendable {
|
||||
func write(_ buffer: ByteBuffer, hasCustomHeaders: Bool = false) async throws {}
|
||||
func finish() async throws {}
|
||||
func writeAndFinish(_ buffer: ByteBuffer) async throws {}
|
||||
}
|
||||
|
||||
#endif
|
||||
@@ -13,6 +13,10 @@
|
||||
//
|
||||
//===----------------------------------------------------------------------===//
|
||||
|
||||
import Logging
|
||||
|
||||
@testable import AWSLambdaRuntime
|
||||
|
||||
#if canImport(FoundationEssentials)
|
||||
import FoundationEssentials
|
||||
#else
|
||||
@@ -24,3 +28,18 @@ extension Date {
|
||||
Int64(self.timeIntervalSince1970 * 1000)
|
||||
}
|
||||
}
|
||||
// MARK: - Test Helpers
|
||||
|
||||
@available(LambdaSwift 2.0, *)
|
||||
extension LambdaContext {
|
||||
public static func makeTest() -> 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")
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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<Output>,
|
||||
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/).
|
||||
|
||||
Reference in New Issue
Block a user