diff --git a/Package.swift b/Package.swift index 99ea9667..36a0e467 100644 --- a/Package.swift +++ b/Package.swift @@ -18,11 +18,13 @@ let package = Package( .plugin(name: "AWSLambdaPackager", targets: ["AWSLambdaPackager"]), ], traits: [ + "ManagedRuntimeSupport", "FoundationJSONSupport", "ServiceLifecycleSupport", "LocalServerSupport", .default( enabledTraits: [ + "ManagedRuntimeSupport", "FoundationJSONSupport", "ServiceLifecycleSupport", "LocalServerSupport", @@ -30,10 +32,10 @@ let package = Package( ), ], dependencies: [ - .package(url: "https://github.com/apple/swift-nio.git", from: "2.81.0"), - .package(url: "https://github.com/apple/swift-log.git", from: "1.5.4"), - .package(url: "https://github.com/apple/swift-collections.git", from: "1.1.4"), - .package(url: "https://github.com/swift-server/swift-service-lifecycle.git", from: "2.8.0"), + .package(url: "https://github.com/apple/swift-nio.git", from: "2.92.0"), + .package(url: "https://github.com/apple/swift-log.git", from: "1.8.0"), + .package(url: "https://github.com/apple/swift-collections.git", from: "1.3.0"), + .package(url: "https://github.com/swift-server/swift-service-lifecycle.git", from: "2.9.0"), ], targets: [ .target( diff --git a/Package@swift-6.0.swift b/Package@swift-6.0.swift index 5f4021d4..be01537f 100644 --- a/Package@swift-6.0.swift +++ b/Package@swift-6.0.swift @@ -6,6 +6,7 @@ let defaultSwiftSettings: [SwiftSetting] = [ .define("FoundationJSONSupport"), .define("ServiceLifecycleSupport"), .define("LocalServerSupport"), + .define("ManagedRuntimeSupport"), .enableExperimentalFeature( "AvailabilityMacro=LambdaSwift 2.0:macOS 15.0" ), @@ -20,10 +21,10 @@ let package = Package( .plugin(name: "AWSLambdaPackager", targets: ["AWSLambdaPackager"]), ], dependencies: [ - .package(url: "https://github.com/apple/swift-nio.git", from: "2.81.0"), - .package(url: "https://github.com/apple/swift-log.git", from: "1.5.4"), - .package(url: "https://github.com/apple/swift-collections.git", from: "1.1.4"), - .package(url: "https://github.com/swift-server/swift-service-lifecycle.git", from: "2.8.0"), + .package(url: "https://github.com/apple/swift-nio.git", from: "2.92.0"), + .package(url: "https://github.com/apple/swift-log.git", from: "1.8.0"), + .package(url: "https://github.com/apple/swift-collections.git", from: "1.3.0"), + .package(url: "https://github.com/swift-server/swift-service-lifecycle.git", from: "2.9.0"), ], targets: [ .target( diff --git a/Sources/AWSLambdaRuntime/ManagedRuntime/LambdaManagedRuntime+ServiceLifecycle.swift b/Sources/AWSLambdaRuntime/ManagedRuntime/LambdaManagedRuntime+ServiceLifecycle.swift new file mode 100644 index 00000000..a47bcd3a --- /dev/null +++ b/Sources/AWSLambdaRuntime/ManagedRuntime/LambdaManagedRuntime+ServiceLifecycle.swift @@ -0,0 +1,31 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the SwiftAWSLambdaRuntime open source project +// +// Copyright SwiftAWSLambdaRuntime project authors +// Copyright (c) Amazon.com, Inc. or its affiliates. +// Licensed under Apache License v2.0 +// +// See LICENSE.txt for license information +// See CONTRIBUTORS.txt for the list of SwiftAWSLambdaRuntime project authors +// +// SPDX-License-Identifier: Apache-2.0 +// +//===----------------------------------------------------------------------===// + +#if ManagedRuntimeSupport + +#if ServiceLifecycleSupport +import ServiceLifecycle + +@available(LambdaSwift 2.0, *) +extension LambdaManagedRuntime: Service { + public func run() async throws { + try await cancelWhenGracefulShutdown { + try await self._run() + } + } +} +#endif + +#endif diff --git a/Sources/AWSLambdaRuntime/ManagedRuntime/LambdaManagedRuntime.swift b/Sources/AWSLambdaRuntime/ManagedRuntime/LambdaManagedRuntime.swift new file mode 100644 index 00000000..2b163865 --- /dev/null +++ b/Sources/AWSLambdaRuntime/ManagedRuntime/LambdaManagedRuntime.swift @@ -0,0 +1,128 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the SwiftAWSLambdaRuntime open source project +// +// Copyright SwiftAWSLambdaRuntime project authors +// Copyright (c) Amazon.com, Inc. or its affiliates. +// Licensed under Apache License v2.0 +// +// See LICENSE.txt for license information +// See CONTRIBUTORS.txt for the list of SwiftAWSLambdaRuntime project authors +// +// SPDX-License-Identifier: Apache-2.0 +// +//===----------------------------------------------------------------------===// + +#if ManagedRuntimeSupport + +import Logging +import NIOCore +import Synchronization + +@available(LambdaSwift 2.0, *) +public final class LambdaManagedRuntime: Sendable where Handler: StreamingLambdaHandler & Sendable { + + @usableFromInline + let logger: Logger + + @usableFromInline + let eventLoop: EventLoop + + @usableFromInline + let handler: Handler + + public init( + handler: Handler, + eventLoop: EventLoop = Lambda.defaultEventLoop, + logger: Logger = Logger(label: "LambdaManagedRuntime") + ) { + self.handler = handler + self.eventLoop = eventLoop + + // by setting the log level here, we understand it can not be changed dynamically at runtime + // developers have to wait for AWS Lambda to dispose and recreate a runtime environment to pickup a change + // this approach is less flexible but more performant than reading the value of the environment variable at each invocation + var log = logger + + // use the LOG_LEVEL environment variable to set the log level. + // if the environment variable is not set, use the default log level from the logger provided + log.logLevel = Lambda.env("LOG_LEVEL").flatMap { .init(rawValue: $0) } ?? logger.logLevel + + self.logger = log + self.logger.debug("LambdaManagedRuntime initialized") + } + + #if !ServiceLifecycleSupport + public func run() async throws { + try await self._run() + } + #endif + + /// Starts the Runtime Interface Client (RIC), i.e. the loop that will poll events, + /// dispatch them to the Handler and push back results or errors. + /// This function makes sure only one run() is called at a time + internal func _run() async throws { + + 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..(false) +// Shared guard helper +@available(LambdaSwift 2.0, *) +internal func withRuntimeGuard(_ 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: Sendable where Handler: StreamingLambdaHandler { @usableFromInline @@ -64,49 +80,36 @@ public final class LambdaRuntime: 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 + ) + } } } diff --git a/Tests/AWSLambdaRuntimeTests/LambdaManagedRuntimeTests.swift b/Tests/AWSLambdaRuntimeTests/LambdaManagedRuntimeTests.swift new file mode 100644 index 00000000..effeefbc --- /dev/null +++ b/Tests/AWSLambdaRuntimeTests/LambdaManagedRuntimeTests.swift @@ -0,0 +1,200 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the SwiftAWSLambdaRuntime open source project +// +// Copyright SwiftAWSLambdaRuntime project authors +// Copyright (c) Amazon.com, Inc. or its affiliates. +// Licensed under Apache License v2.0 +// +// See LICENSE.txt for license information +// See CONTRIBUTORS.txt for the list of SwiftAWSLambdaRuntime project authors +// +// SPDX-License-Identifier: Apache-2.0 +// +//===----------------------------------------------------------------------===// + +#if ManagedRuntimeSupport + +import Foundation +import Logging +import NIOCore +import Synchronization +import Testing + +@testable import AWSLambdaRuntime + +@Suite(.serialized) +struct LambdaManagedRuntimeTests { + + // Test 1: Concurrent Handler Execution + @Test("LambdaManagedRuntime handler handles concurrent invocations") + @available(LambdaSwift 2.0, *) + func testConcurrentHandlerExecution() async throws { + let handler = ConcurrentMockHandler() + + let invocationCount = 5 + + let results = try await withThrowingTaskGroup(of: String.self) { group in + // Simulate concurrent invocations + for i in 0..(JSONEncoder()) + + // let concurrentTasks = 10 + + // let results = try await withThrowingTaskGroup(of: String.self) { group in + // for i in 0.. LambdaContext { + LambdaContext.__forTestsOnly( + requestID: "test-request-id", + traceID: "test-trace-id", + tenantID: "test-tenant-id", + invokedFunctionARN: "arn:aws:lambda:us-east-1:123456789012:function:test", + timeout: .seconds(30), + logger: Logger(label: "MockedLambdaContext") + ) + } +} diff --git a/readme.md b/readme.md index d543bc73..d24f6055 100644 --- a/readme.md +++ b/readme.md @@ -430,6 +430,80 @@ struct LambdaFunction { You can see a complete working example in the [ServiceLifecycle+Postgres example](Examples/ServiceLifecycle+Postgres/README.md), which demonstrates how to manage a PostgreSQL client alongside the Lambda runtime using ServiceLifecycle. +### Lambda Managed Instances + +[Lambda Managed Instances](https://docs.aws.amazon.com/lambda/latest/dg/lambda-managed-instances.html) enables you to run Lambda functions on your current-generation Amazon EC2 instances while maintaining serverless simplicity. This deployment model provides EC2 flexibility and cost optimization by running your functions on customer-owned EC2 instances, while AWS handles all infrastructure management tasks including instance lifecycle, OS and runtime patching, routing, load balancing, and auto-scaling. + +To deploy a Swift Lambda function to Lambda Managed Instances, you need to make two key changes to your code: + +#### 1. Use `LambdaManagedRuntime` instead of `LambdaRuntime` + +Replace your standard `LambdaRuntime` initialization with `LambdaManagedRuntime`: + +```swift +import AWSLambdaRuntime + +// Standard Lambda function - change this: +// let runtime = LambdaRuntime { ... } + +// Lambda Managed Instances - to this: +let runtime = LambdaManagedRuntime { + (event: HelloRequest, context: LambdaContext) in + + HelloResponse(greetings: "Hello \(event.name)!") +} + +try await runtime.run() +``` + +#### 2. Ensure Handler Functions and Structs are `Sendable` + +Because Lambda Managed Instances can run functions concurrently on the same EC2 host, your handler functions or the structs containing them must conform to the `Sendable` protocol: + +```swift +import AWSLambdaRuntime + +// For struct-based handlers, explicitly conform to Sendable +struct MyHandler: LambdaWithBackgroundProcessingHandler, Sendable { + typealias Event = MyRequest + typealias Output = MyResponse + + func handle( + _ event: Event, + outputWriter: some LambdaResponseWriter, + context: LambdaContext + ) async throws { + // Your handler logic here + try await outputWriter.write(MyResponse(message: "Processed")) + } +} + +// Use LambdaCodableAdapterSendable for struct handlers +let adapter = LambdaCodableAdapterSendable(handler: MyHandler()) +let runtime = LambdaManagedRuntime(handler: adapter) +try await runtime.run() +``` + +For simple data structures, the Swift compiler automatically infers `Sendable` conformance, but you should explicitly declare it for clarity and safety. + +#### Key Benefits + +- **EC2 Flexibility**: Run on specialized EC2 instance types including Graviton4 and network-optimized instances +- **Cost Optimization**: Better cost efficiency for sustained workloads +- **Serverless Simplicity**: AWS manages all infrastructure concerns while you focus on code +- **Concurrent Execution**: Functions can run concurrently on the same host for improved throughput + +#### Prerequisites + +Before deploying to Lambda Managed Instances, you need to: + +1. Create a [Lambda Managed Instances capacity provider](https://docs.aws.amazon.com/lambda/latest/dg/lambda-managed-instances-capacity-providers.html) +2. Configure your deployment to reference the capacity provider ARN + +You can see complete working examples in the [ManagedInstances example directory](Examples/ManagedInstances/README.md), which demonstrates deploying HelloJSON, Streaming, and BackgroundTasks functions to Lambda Managed Instances using AWS SAM. + +For more information, see the [AWS Lambda Managed Instances documentation](https://docs.aws.amazon.com/lambda/latest/dg/lambda-managed-instances.html) and the [execution environment guide](https://docs.aws.amazon.com/lambda/latest/dg/lambda-managed-instances-execution-environment.html). + ### Use Lambda Background Tasks Background tasks allow code to execute asynchronously after the main response has been returned, enabling additional processing without affecting response latency. This approach is ideal for scenarios like logging, data updates, or notifications that can be deferred. The code leverages Lambda's "Response Streaming" feature, which is effective for balancing real-time user responsiveness with the ability to perform extended tasks post-response. For more information about Lambda background tasks, see [this AWS blog post](https://aws.amazon.com/blogs/compute/running-code-after-returning-a-response-from-an-aws-lambda-function/).