From 2db6f9ae29a995a0f63b8b587d08a24db59d4095 Mon Sep 17 00:00:00 2001 From: Sebastien Stormacq Date: Sun, 4 Jan 2026 09:36:12 -0500 Subject: [PATCH 01/21] remove unused script --- scripts/extract_aws_credentials.sh | 123 ----------------------------- 1 file changed, 123 deletions(-) delete mode 100755 scripts/extract_aws_credentials.sh diff --git a/scripts/extract_aws_credentials.sh b/scripts/extract_aws_credentials.sh deleted file mode 100755 index e26c23f3b..000000000 --- a/scripts/extract_aws_credentials.sh +++ /dev/null @@ -1,123 +0,0 @@ -#!/bin/bash -##===----------------------------------------------------------------------===## -## -## This source file is part of the SwiftAWSLambdaRuntime open source project -## -## Copyright SwiftAWSLambdaRuntime project authors -## Copyright (c) Amazon.com, Inc. or its affiliates. -## Licensed under Apache License v2.0 -## -## See LICENSE.txt for license information -## See CONTRIBUTORS.txt for the list of SwiftAWSLambdaRuntime project authors -## -## SPDX-License-Identifier: Apache-2.0 -## -##===----------------------------------------------------------------------===## - -# Extract AWS credentials from ~/.aws/credentials and ~/.aws/config (default profile) -# and set environment variables - -set -e - -# Default profile name -PROFILE="default" - -# Check if a different profile is specified as argument -if [ $# -eq 1 ]; then - PROFILE="$1" -fi - -# AWS credentials file path -CREDENTIALS_FILE="$HOME/.aws/credentials" -CONFIG_FILE="$HOME/.aws/config" - -# Check if credentials file exists -if [ ! -f "$CREDENTIALS_FILE" ]; then - echo "Error: AWS credentials file not found at $CREDENTIALS_FILE" - exit 1 -fi - -# Function to extract value from AWS config files -extract_value() { - local file="$1" - local profile="$2" - local key="$3" - - # Use awk to extract the value for the specified profile and key - awk -v profile="[$profile]" -v key="$key" ' - BEGIN { in_profile = 0 } - $0 == profile { in_profile = 1; next } - /^\[/ && $0 != profile { in_profile = 0 } - in_profile && $0 ~ "^" key " *= *" { - gsub("^" key " *= *", "") - gsub(/^[ \t]+|[ \t]+$/, "") # trim whitespace - print $0 - exit - } - ' "$file" -} - -# Extract credentials -AWS_ACCESS_KEY_ID=$(extract_value "$CREDENTIALS_FILE" "$PROFILE" "aws_access_key_id") -AWS_SECRET_ACCESS_KEY=$(extract_value "$CREDENTIALS_FILE" "$PROFILE" "aws_secret_access_key") -AWS_SESSION_TOKEN=$(extract_value "$CREDENTIALS_FILE" "$PROFILE" "aws_session_token") - -# Extract region from config file (try both credentials and config files) -AWS_REGION=$(extract_value "$CREDENTIALS_FILE" "$PROFILE" "region") -if [ -z "$AWS_REGION" ] && [ -f "$CONFIG_FILE" ]; then - # Try config file with profile prefix for non-default profiles - if [ "$PROFILE" = "default" ]; then - AWS_REGION=$(extract_value "$CONFIG_FILE" "$PROFILE" "region") - else - AWS_REGION=$(extract_value "$CONFIG_FILE" "profile $PROFILE" "region") - fi -fi - -# Validate required credentials -if [ -z "$AWS_ACCESS_KEY_ID" ]; then - echo "Error: aws_access_key_id not found for profile '$PROFILE'" - exit 1 -fi - -if [ -z "$AWS_SECRET_ACCESS_KEY" ]; then - echo "Error: aws_secret_access_key not found for profile '$PROFILE'" - exit 1 -fi - -# Set default region if not found -if [ -z "$AWS_REGION" ]; then - AWS_REGION="us-east-1" - echo "Warning: No region found for profile '$PROFILE', defaulting to us-east-1" -fi - -# Export environment variables -export AWS_REGION="$AWS_REGION" -export AWS_ACCESS_KEY_ID="$AWS_ACCESS_KEY_ID" -export AWS_SECRET_ACCESS_KEY="$AWS_SECRET_ACCESS_KEY" - -# Only export session token if it exists (for temporary credentials) -if [ -n "$AWS_SESSION_TOKEN" ]; then - export AWS_SESSION_TOKEN="$AWS_SESSION_TOKEN" -fi - -# Print confirmation (without sensitive values) -echo "AWS credentials loaded for profile: $PROFILE" -echo "AWS_REGION: $AWS_REGION" -echo "AWS_ACCESS_KEY_ID: ${AWS_ACCESS_KEY_ID:0:4}****" -echo "AWS_SECRET_ACCESS_KEY: ****" -if [ -n "$AWS_SESSION_TOKEN" ]; then - echo "AWS_SESSION_TOKEN: ****" -fi - -# Optional: Print export commands for manual sourcing -echo "" -echo "To use these credentials in your current shell, run:" -echo "source $(basename "$0")" -echo "" -echo "Or copy and paste these export commands:" -echo "export AWS_REGION='$AWS_REGION'" -echo "export AWS_ACCESS_KEY_ID='$AWS_ACCESS_KEY_ID'" -echo "export AWS_SECRET_ACCESS_KEY='$AWS_SECRET_ACCESS_KEY'" -if [ -n "$AWS_SESSION_TOKEN" ]; then - echo "export AWS_SESSION_TOKEN='$AWS_SESSION_TOKEN'" -fi \ No newline at end of file From e01ef937e700a450fc01781deb2e52215f22fc29 Mon Sep 17 00:00:00 2001 From: Sebastien Stormacq Date: Sun, 4 Jan 2026 09:40:33 -0500 Subject: [PATCH 02/21] refcator files on Lambda runtime --- .../LambdaHandler+JSON.swift | 128 ++++++++++++++ ...da+JSON.swift => LambdaRuntime+JSON.swift} | 99 ----------- Sources/AWSLambdaRuntime/Lambda+Codable.swift | 2 +- Sources/AWSLambdaRuntime/LambdaRuntime.swift | 162 +++++++++++------- ...lers.swift => LambdaRuntimeHandlers.swift} | 0 5 files changed, 228 insertions(+), 163 deletions(-) create mode 100644 Sources/AWSLambdaRuntime/FoundationSupport/LambdaHandler+JSON.swift rename Sources/AWSLambdaRuntime/FoundationSupport/{Lambda+JSON.swift => LambdaRuntime+JSON.swift} (62%) rename Sources/AWSLambdaRuntime/{LambdaHandlers.swift => LambdaRuntimeHandlers.swift} (100%) diff --git a/Sources/AWSLambdaRuntime/FoundationSupport/LambdaHandler+JSON.swift b/Sources/AWSLambdaRuntime/FoundationSupport/LambdaHandler+JSON.swift new file mode 100644 index 000000000..19313df6b --- /dev/null +++ b/Sources/AWSLambdaRuntime/FoundationSupport/LambdaHandler+JSON.swift @@ -0,0 +1,128 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the SwiftAWSLambdaRuntime open source project +// +// Copyright SwiftAWSLambdaRuntime project authors +// Copyright (c) Amazon.com, Inc. or its affiliates. +// Licensed under Apache License v2.0 +// +// See LICENSE.txt for license information +// See CONTRIBUTORS.txt for the list of SwiftAWSLambdaRuntime project authors +// +// SPDX-License-Identifier: Apache-2.0 +// +//===----------------------------------------------------------------------===// + +#if FoundationJSONSupport +import NIOCore + +#if canImport(FoundationEssentials) +import FoundationEssentials +#else +import struct Foundation.Data +import class Foundation.JSONDecoder +import class Foundation.JSONEncoder +#endif + +import Logging + +public struct LambdaJSONEventDecoder: LambdaEventDecoder { + @usableFromInline let jsonDecoder: JSONDecoder + + @inlinable + public init(_ jsonDecoder: JSONDecoder) { + self.jsonDecoder = jsonDecoder + } + + @inlinable + public func decode(_ type: Event.Type, from buffer: NIOCore.ByteBuffer) throws -> Event + where Event: Decodable { + try buffer.getJSONDecodable( + Event.self, + decoder: self.jsonDecoder, + at: buffer.readerIndex, + length: buffer.readableBytes + )! // must work, enough readable bytes + } +} + +public struct LambdaJSONOutputEncoder: LambdaOutputEncoder { + @usableFromInline let jsonEncoder: JSONEncoder + + @inlinable + public init(_ jsonEncoder: JSONEncoder) { + self.jsonEncoder = jsonEncoder + } + + @inlinable + public func encode(_ value: Output, into buffer: inout ByteBuffer) throws { + try buffer.writeJSONEncodable(value, encoder: self.jsonEncoder) + } +} + +@available(LambdaSwift 2.0, *) +extension LambdaCodableAdapter { + /// Initializes an instance given an encoder, decoder, and a handler with a non-`Void` output. + /// - Parameters: + /// - encoder: The encoder object that will be used to encode the generic `Output` obtained from the `handler`'s `outputWriter` into a `ByteBuffer`. By default, a JSONEncoder is used. + /// - decoder: The decoder object that will be used to decode the received `ByteBuffer` event into the generic `Event` type served to the `handler`. By default, a JSONDecoder is used. + /// - handler: The handler object. + public init( + encoder: JSONEncoder = JSONEncoder(), + decoder: JSONDecoder = JSONDecoder(), + handler: sending Handler + ) + where + Output: Encodable, + Output == Handler.Output, + Encoder == LambdaJSONOutputEncoder, + Decoder == LambdaJSONEventDecoder + { + self.init( + encoder: LambdaJSONOutputEncoder(encoder), + decoder: LambdaJSONEventDecoder(decoder), + handler: handler + ) + } + + /// Initializes an instance given a decoder, and a handler with a `Void` output. + /// - Parameters: + /// - decoder: The decoder object that will be used to decode the received `ByteBuffer` event into the generic `Event` type served to the `handler`. By default, a JSONDecoder is used. + /// - handler: The handler object. + public init( + decoder: JSONDecoder = JSONDecoder(), + handler: sending Handler + ) + where + Output == Void, + Handler.Output == Void, + Decoder == LambdaJSONEventDecoder, + Encoder == VoidEncoder + { + self.init( + decoder: LambdaJSONEventDecoder(decoder), + handler: handler + ) + } +} + +@available(LambdaSwift 2.0, *) +extension LambdaResponseStreamWriter { + /// Writes the HTTP status code and headers to the response stream. + /// + /// This method serializes the status and headers as JSON and writes them to the stream, + /// followed by eight null bytes as a separator before the response body. + /// + /// - Parameters: + /// - response: The status and headers response to write + /// - encoder: The encoder to use for serializing the response, use JSONEncoder by default + /// - Throws: An error if JSON serialization or writing fails + public func writeStatusAndHeaders( + _ response: StreamingLambdaStatusAndHeadersResponse, + encoder: JSONEncoder = JSONEncoder() + ) async throws { + encoder.outputFormatting = .withoutEscapingSlashes + try await self.writeStatusAndHeaders(response, encoder: LambdaJSONOutputEncoder(encoder)) + } +} +#endif // trait: FoundationJSONSupport diff --git a/Sources/AWSLambdaRuntime/FoundationSupport/Lambda+JSON.swift b/Sources/AWSLambdaRuntime/FoundationSupport/LambdaRuntime+JSON.swift similarity index 62% rename from Sources/AWSLambdaRuntime/FoundationSupport/Lambda+JSON.swift rename to Sources/AWSLambdaRuntime/FoundationSupport/LambdaRuntime+JSON.swift index 646e77f93..50a9c5a70 100644 --- a/Sources/AWSLambdaRuntime/FoundationSupport/Lambda+JSON.swift +++ b/Sources/AWSLambdaRuntime/FoundationSupport/LambdaRuntime+JSON.swift @@ -26,105 +26,6 @@ import class Foundation.JSONEncoder import Logging -public struct LambdaJSONEventDecoder: LambdaEventDecoder { - @usableFromInline let jsonDecoder: JSONDecoder - - @inlinable - public init(_ jsonDecoder: JSONDecoder) { - self.jsonDecoder = jsonDecoder - } - - @inlinable - public func decode(_ type: Event.Type, from buffer: NIOCore.ByteBuffer) throws -> Event - where Event: Decodable { - try buffer.getJSONDecodable( - Event.self, - decoder: self.jsonDecoder, - at: buffer.readerIndex, - length: buffer.readableBytes - )! // must work, enough readable bytes - } -} - -public struct LambdaJSONOutputEncoder: LambdaOutputEncoder { - @usableFromInline let jsonEncoder: JSONEncoder - - @inlinable - public init(_ jsonEncoder: JSONEncoder) { - self.jsonEncoder = jsonEncoder - } - - @inlinable - public func encode(_ value: Output, into buffer: inout ByteBuffer) throws { - try buffer.writeJSONEncodable(value, encoder: self.jsonEncoder) - } -} - -@available(LambdaSwift 2.0, *) -extension LambdaCodableAdapter { - /// Initializes an instance given an encoder, decoder, and a handler with a non-`Void` output. - /// - Parameters: - /// - encoder: The encoder object that will be used to encode the generic `Output` obtained from the `handler`'s `outputWriter` into a `ByteBuffer`. By default, a JSONEncoder is used. - /// - decoder: The decoder object that will be used to decode the received `ByteBuffer` event into the generic `Event` type served to the `handler`. By default, a JSONDecoder is used. - /// - handler: The handler object. - public init( - encoder: JSONEncoder = JSONEncoder(), - decoder: JSONDecoder = JSONDecoder(), - handler: sending Handler - ) - where - Output: Encodable, - Output == Handler.Output, - Encoder == LambdaJSONOutputEncoder, - Decoder == LambdaJSONEventDecoder - { - self.init( - encoder: LambdaJSONOutputEncoder(encoder), - decoder: LambdaJSONEventDecoder(decoder), - handler: handler - ) - } - - /// Initializes an instance given a decoder, and a handler with a `Void` output. - /// - Parameters: - /// - decoder: The decoder object that will be used to decode the received `ByteBuffer` event into the generic `Event` type served to the `handler`. By default, a JSONDecoder is used. - /// - handler: The handler object. - public init( - decoder: JSONDecoder = JSONDecoder(), - handler: sending Handler - ) - where - Output == Void, - Handler.Output == Void, - Decoder == LambdaJSONEventDecoder, - Encoder == VoidEncoder - { - self.init( - decoder: LambdaJSONEventDecoder(decoder), - handler: handler - ) - } -} - -@available(LambdaSwift 2.0, *) -extension LambdaResponseStreamWriter { - /// Writes the HTTP status code and headers to the response stream. - /// - /// This method serializes the status and headers as JSON and writes them to the stream, - /// followed by eight null bytes as a separator before the response body. - /// - /// - Parameters: - /// - response: The status and headers response to write - /// - encoder: The encoder to use for serializing the response, use JSONEncoder by default - /// - Throws: An error if JSON serialization or writing fails - public func writeStatusAndHeaders( - _ response: StreamingLambdaStatusAndHeadersResponse, - encoder: JSONEncoder = JSONEncoder() - ) async throws { - encoder.outputFormatting = .withoutEscapingSlashes - try await self.writeStatusAndHeaders(response, encoder: LambdaJSONOutputEncoder(encoder)) - } -} @available(LambdaSwift 2.0, *) extension LambdaRuntime { /// Initialize an instance with a `LambdaHandler` defined in the form of a closure **with a non-`Void` return type**. diff --git a/Sources/AWSLambdaRuntime/Lambda+Codable.swift b/Sources/AWSLambdaRuntime/Lambda+Codable.swift index f20f5a476..27a7c3ace 100644 --- a/Sources/AWSLambdaRuntime/Lambda+Codable.swift +++ b/Sources/AWSLambdaRuntime/Lambda+Codable.swift @@ -39,7 +39,7 @@ public protocol LambdaOutputEncoder { func encode(_ value: Output, into buffer: inout ByteBuffer) throws } -public struct VoidEncoder: LambdaOutputEncoder { +public struct VoidEncoder: LambdaOutputEncoder, Sendable { public typealias Output = Void public init() {} diff --git a/Sources/AWSLambdaRuntime/LambdaRuntime.swift b/Sources/AWSLambdaRuntime/LambdaRuntime.swift index c7465b097..67d7f0177 100644 --- a/Sources/AWSLambdaRuntime/LambdaRuntime.swift +++ b/Sources/AWSLambdaRuntime/LambdaRuntime.swift @@ -20,7 +20,7 @@ import Synchronization // This is our guardian to ensure only one LambdaRuntime is running at the time // We use an Atomic here to ensure thread safety @available(LambdaSwift 2.0, *) -private let _isRunning = Atomic(false) +private let _isLambdaRuntimeRunning = Atomic(false) @available(LambdaSwift 2.0, *) public final class LambdaRuntime: Sendable where Handler: StreamingLambdaHandler { @@ -59,11 +59,17 @@ public final class LambdaRuntime: Sendable where Handler: StreamingLamb } #endif - /// Make sure only one run() is called at a time + /// Starts the Runtime Interface Client (RIC), i.e. the loop that will poll events, + /// dispatch them to the Handler and push back results or errors. + /// Thus function make sure only one run() is called at a time internal func _run() async throws { // we use an atomic global variable to ensure only one LambdaRuntime is running at the time - let (_, original) = _isRunning.compareExchange(expected: false, desired: true, ordering: .acquiringAndReleasing) + let (_, original) = _isLambdaRuntimeRunning.compareExchange( + expected: false, + desired: true, + ordering: .acquiringAndReleasing + ) // if the original value was already true, run() is already running if original { @@ -71,7 +77,7 @@ public final class LambdaRuntime: Sendable where Handler: StreamingLamb } defer { - _isRunning.store(false, ordering: .releasing) + _isLambdaRuntimeRunning.store(false, ordering: .releasing) } // The handler can be non-sendable, we want to ensure we only ever have one copy of it @@ -85,71 +91,101 @@ public final class LambdaRuntime: Sendable where Handler: StreamingLamb // https://docs.aws.amazon.com/lambda/latest/dg/runtimes-api.html if let runtimeEndpoint = Lambda.env("AWS_LAMBDA_RUNTIME_API") { - let ipAndPort = runtimeEndpoint.split(separator: ":", maxSplits: 1) - let ip = String(ipAndPort[0]) - guard let port = Int(ipAndPort[1]) else { throw LambdaRuntimeError(code: .invalidPort) } - - do { - try await LambdaRuntimeClient.withRuntimeClient( - configuration: .init(ip: ip, port: port), - eventLoop: self.eventLoop, - logger: self.logger - ) { runtimeClient in - try await Lambda.runLoop( - runtimeClient: runtimeClient, - handler: handler, - logger: self.logger - ) - } - } catch { - // catch top level errors that have not been handled until now - // this avoids the runtime to crash and generate a backtrace - if let error = error as? LambdaRuntimeError, - error.code != .connectionToControlPlaneLost - { - // if the error is a LambdaRuntimeError but not a connection error, - // we rethrow it to preserve existing behaviour - self.logger.error("LambdaRuntime.run() failed with error", metadata: ["error": "\(error)"]) - throw error - } else { - self.logger.trace("LambdaRuntime.run() connection lost") - } - } + self.logger.trace("Starting the Runtime Interface Client") + try await LambdaRuntime.startRuntimeInterfaceClient( + endpoint: runtimeEndpoint, + handler: handler, + eventLoop: self.eventLoop, + logger: self.logger + ) } else { - #if LocalServerSupport - - // we're not running on Lambda and we're compiled in DEBUG mode, - // let's start a local server for testing + self.logger.trace("Starting the local test HTTP server") + try await LambdaRuntime.startLocalServer( + handler: handler, + eventLoop: self.eventLoop, + logger: self.logger + ) + } + } - let host = Lambda.env("LOCAL_LAMBDA_HOST") ?? "127.0.0.1" - let port = Lambda.env("LOCAL_LAMBDA_PORT").flatMap(Int.init) ?? 7000 - let endpoint = Lambda.env("LOCAL_LAMBDA_INVOCATION_ENDPOINT") + internal static func startRuntimeInterfaceClient( + endpoint: String, + handler: Handler, + eventLoop: EventLoop, + logger: Logger + ) async throws { + + let ipAndPort = endpoint.split(separator: ":", maxSplits: 1) + let ip = String(ipAndPort[0]) + guard let port = Int(ipAndPort[1]) else { throw LambdaRuntimeError(code: .invalidPort) } + + do { + try await LambdaRuntimeClient.withRuntimeClient( + configuration: .init(ip: ip, port: port), + eventLoop: eventLoop, + logger: logger + ) { runtimeClient in + try await Lambda.runLoop( + runtimeClient: runtimeClient, + handler: handler, + logger: logger + ) + } + } catch { + // catch top level errors that have not been handled until now + // this avoids the runtime to crash and generate a backtrace + if let error = error as? LambdaRuntimeError, + error.code != .connectionToControlPlaneLost + { + // if the error is a LambdaRuntimeError but not a connection error, + // we rethrow it to preserve existing behaviour + logger.error("LambdaRuntime.run() failed with error", metadata: ["error": "\(error)"]) + throw error + } else { + logger.trace("LambdaRuntime.run() connection lost") + } + } + } - try await Lambda.withLocalServer( - host: host, - port: port, - invocationEndpoint: endpoint, - logger: self.logger - ) { - - try await LambdaRuntimeClient.withRuntimeClient( - configuration: .init(ip: host, port: port), - eventLoop: self.eventLoop, - logger: self.logger - ) { runtimeClient in - try await Lambda.runLoop( - runtimeClient: runtimeClient, - handler: handler, - logger: self.logger - ) - } + internal static func startLocalServer( + handler: sending Handler, + eventLoop: EventLoop, + logger: Logger + ) async throws { + #if LocalServerSupport + + // we're not running on Lambda and we're compiled in DEBUG mode, + // let's start a local server for testing + + let host = Lambda.env("LOCAL_LAMBDA_HOST") ?? "127.0.0.1" + let port = Lambda.env("LOCAL_LAMBDA_PORT").flatMap(Int.init) ?? 7000 + let endpoint = Lambda.env("LOCAL_LAMBDA_INVOCATION_ENDPOINT") + + try await Lambda.withLocalServer( + host: host, + port: port, + invocationEndpoint: endpoint, + logger: logger + ) { + + try await LambdaRuntimeClient.withRuntimeClient( + configuration: .init(ip: host, port: port), + eventLoop: eventLoop, + logger: logger + ) { runtimeClient in + try await Lambda.runLoop( + runtimeClient: runtimeClient, + handler: handler, + logger: logger + ) } - #else - // When the LocalServerSupport trait is disabled, we can't start a local server because the local server code is not compiled. - throw LambdaRuntimeError(code: .missingLambdaRuntimeAPIEnvironmentVariable) - #endif } + #else + // When the LocalServerSupport trait is disabled, we can't start a local server because the local server code is not compiled. + throw LambdaRuntimeError(code: .missingLambdaRuntimeAPIEnvironmentVariable) + #endif } + } diff --git a/Sources/AWSLambdaRuntime/LambdaHandlers.swift b/Sources/AWSLambdaRuntime/LambdaRuntimeHandlers.swift similarity index 100% rename from Sources/AWSLambdaRuntime/LambdaHandlers.swift rename to Sources/AWSLambdaRuntime/LambdaRuntimeHandlers.swift From 2766f3f5e8071ef04bfb4b34fb519b4a27948be6 Mon Sep 17 00:00:00 2001 From: Sebastien Stormacq Date: Sun, 4 Jan 2026 09:40:50 -0500 Subject: [PATCH 03/21] add support for LLambda Managed Instances --- .../LambdaManagedRuntime+JSON.swift | 232 ++++++++++++++++++ .../LambdaManagedRuntime+Codable.swift | 112 +++++++++ ...ambdaManagedRuntime+ServiceLifecycle.swift | 31 +++ .../ManagedRuntime/LambdaManagedRuntime.swift | 146 +++++++++++ .../LambdaManagedRuntimeHandlers.swift | 182 ++++++++++++++ 5 files changed, 703 insertions(+) create mode 100644 Sources/AWSLambdaRuntime/FoundationSupport/LambdaManagedRuntime+JSON.swift create mode 100644 Sources/AWSLambdaRuntime/ManagedRuntime/LambdaManagedRuntime+Codable.swift create mode 100644 Sources/AWSLambdaRuntime/ManagedRuntime/LambdaManagedRuntime+ServiceLifecycle.swift create mode 100644 Sources/AWSLambdaRuntime/ManagedRuntime/LambdaManagedRuntime.swift create mode 100644 Sources/AWSLambdaRuntime/ManagedRuntime/LambdaManagedRuntimeHandlers.swift diff --git a/Sources/AWSLambdaRuntime/FoundationSupport/LambdaManagedRuntime+JSON.swift b/Sources/AWSLambdaRuntime/FoundationSupport/LambdaManagedRuntime+JSON.swift new file mode 100644 index 000000000..8d7f47e4f --- /dev/null +++ b/Sources/AWSLambdaRuntime/FoundationSupport/LambdaManagedRuntime+JSON.swift @@ -0,0 +1,232 @@ +//===----------------------------------------------------------------------===// +// +// 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 + +public struct LambdaJSONEventDecoderSendable: LambdaEventDecoder & Sendable { + @usableFromInline let jsonDecoder: JSONDecoder + + @inlinable + public init(_ jsonDecoder: JSONDecoder) { + self.jsonDecoder = jsonDecoder + } + + @inlinable + public func decode(_ type: Event.Type, from buffer: NIOCore.ByteBuffer) throws -> Event + where Event: Decodable { + try buffer.getJSONDecodable( + Event.self, + decoder: self.jsonDecoder, + at: buffer.readerIndex, + length: buffer.readableBytes + )! // must work, enough readable bytes + } +} + +public struct LambdaJSONOutputEncoderSendable: LambdaOutputEncoder & Sendable { + @usableFromInline let jsonEncoder: JSONEncoder + + @inlinable + public init(_ jsonEncoder: JSONEncoder) { + self.jsonEncoder = jsonEncoder + } + + @inlinable + public func encode(_ value: Output, into buffer: inout ByteBuffer) throws { + try buffer.writeJSONEncodable(value, encoder: self.jsonEncoder) + } +} + +@available(LambdaSwift 2.0, *) +extension LambdaCodableAdapterSendable { + /// Initializes an instance given an encoder, decoder, and a handler with a non-`Void` output. + /// - Parameters: + /// - encoder: The encoder object that will be used to encode the generic `Output` obtained from the `handler`'s `outputWriter` into a `ByteBuffer`. By default, a JSONEncoder is used. + /// - decoder: The decoder object that will be used to decode the received `ByteBuffer` event into the generic `Event` type served to the `handler`. By default, a JSONDecoder is used. + /// - handler: The handler object. + public init( + encoder: JSONEncoder = JSONEncoder(), + decoder: JSONDecoder = JSONDecoder(), + handler: Handler + ) + where + Output: Encodable, + Output == Handler.Output, + Encoder == LambdaJSONOutputEncoderSendable, + Decoder == LambdaJSONEventDecoderSendable + { + self.init( + encoder: LambdaJSONOutputEncoderSendable(encoder), + decoder: LambdaJSONEventDecoderSendable(decoder), + handler: handler + ) + } + + /// Initializes an instance given a decoder, and a handler with a `Void` output. + /// - Parameters: + /// - decoder: The decoder object that will be used to decode the received `ByteBuffer` event into the generic `Event` type served to the `handler`. By default, a JSONDecoder is used. + /// - handler: The handler object. + public init( + decoder: JSONDecoder = JSONDecoder(), + handler: sending Handler + ) + where + Output == Void, + Handler.Output == Void, + Decoder == LambdaJSONEventDecoderSendable, + Encoder == VoidEncoder + { + self.init( + decoder: LambdaJSONEventDecoderSendable(decoder), + handler: handler + ) + } +} + +@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( + decoder: JSONDecoder = JSONDecoder(), + encoder: JSONEncoder = JSONEncoder(), + logger: Logger = Logger(label: "LambdaManagedRuntime"), + body: @Sendable @escaping (Event, LambdaContext) async throws -> Output + ) + where + Handler == LambdaCodableAdapterSendable< + LambdaHandlerAdapterSendable>, + Event, + Output, + LambdaJSONEventDecoderSendable, + LambdaJSONOutputEncoderSendable + > + { + let handler = LambdaCodableAdapterSendable( + encoder: encoder, + decoder: decoder, + handler: LambdaHandlerAdapterSendable(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( + decoder: JSONDecoder = JSONDecoder(), + logger: Logger = Logger(label: "LambdaRuntime"), + body: @Sendable @escaping (Event, LambdaContext) async throws -> Void + ) + where + Handler == LambdaCodableAdapterSendable< + LambdaHandlerAdapterSendable>, + Event, + Void, + LambdaJSONEventDecoderSendable, + VoidEncoder + > + { + let handler = LambdaCodableAdapterSendable( + decoder: LambdaJSONEventDecoderSendable(decoder), + handler: LambdaHandlerAdapterSendable(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( + decoder: JSONDecoder = JSONDecoder(), + logger: Logger = Logger(label: "LambdaRuntime"), + lambdaHandler: LHandler + ) + where + Handler == LambdaCodableAdapterSendable< + LambdaHandlerAdapterSendable, + Event, + Void, + LambdaJSONEventDecoderSendable, + VoidEncoder + >, + LHandler.Event == Event, + LHandler.Output == Void + { + let handler = LambdaCodableAdapterSendable( + decoder: LambdaJSONEventDecoderSendable(decoder), + handler: LambdaHandlerAdapterSendable(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( + decoder: JSONDecoder = JSONDecoder(), + encoder: JSONEncoder = JSONEncoder(), + logger: Logger = Logger(label: "LambdaRuntime"), + lambdaHandler: LHandler + ) + where + Handler == LambdaCodableAdapterSendable< + LambdaHandlerAdapterSendable, + Event, + Output, + LambdaJSONEventDecoderSendable, + LambdaJSONOutputEncoderSendable + >, + LHandler.Event == Event, + LHandler.Output == Output + { + let handler = LambdaCodableAdapterSendable( + encoder: encoder, + decoder: decoder, + handler: LambdaHandlerAdapterSendable(handler: lambdaHandler) + ) + + self.init(handler: handler, logger: logger) + } +} +#endif // trait: FoundationJSONSupport + +#endif // trait: ManagedRuntimeSupport \ No newline at end of file diff --git a/Sources/AWSLambdaRuntime/ManagedRuntime/LambdaManagedRuntime+Codable.swift b/Sources/AWSLambdaRuntime/ManagedRuntime/LambdaManagedRuntime+Codable.swift new file mode 100644 index 000000000..393732614 --- /dev/null +++ b/Sources/AWSLambdaRuntime/ManagedRuntime/LambdaManagedRuntime+Codable.swift @@ -0,0 +1,112 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the SwiftAWSLambdaRuntime open source project +// +// Copyright SwiftAWSLambdaRuntime project authors +// Copyright (c) Amazon.com, Inc. or its affiliates. +// Licensed under Apache License v2.0 +// +// See LICENSE.txt for license information +// See CONTRIBUTORS.txt for the list of SwiftAWSLambdaRuntime project authors +// +// SPDX-License-Identifier: Apache-2.0 +// +//===----------------------------------------------------------------------===// + +#if ManagedRuntimeSupport + +import NIOCore + +/// Adapts a ``LambdaHandler`` conforming handler to conform to ``LambdaWithBackgroundProcessingHandler``. +@available(LambdaSwift 2.0, *) +public struct LambdaHandlerAdapterSendable< + Event: Decodable, + Output, + Handler: LambdaHandler & Sendable +>: Sendable, LambdaWithBackgroundProcessingHandler where Handler.Event == Event, Handler.Output == Output { + @usableFromInline let handler: Handler + + /// Initializes an instance given a concrete handler. + /// - Parameter handler: The ``LambdaHandler`` conforming handler that is to be adapted to ``LambdaWithBackgroundProcessingHandler``. + @inlinable + public init(handler: sending Handler) { + self.handler = handler + } + + /// Passes the generic `Event` object to the ``LambdaHandler/handle(_:context:)`` function, and + /// the resulting output is then written to ``LambdaWithBackgroundProcessingHandler``'s `outputWriter`. + /// - Parameters: + /// - event: The received event. + /// - outputWriter: The writer to write the computed response to. + /// - context: The ``LambdaContext`` containing the invocation's metadata. + @inlinable + public func handle( + _ event: Event, + outputWriter: some LambdaResponseWriter, + context: LambdaContext + ) async throws { + let output = try await self.handler.handle(event, context: context) + try await outputWriter.write(output) + } +} + +/// Adapts a ``LambdaWithBackgroundProcessingHandler`` conforming handler to conform to ``StreamingLambdaHandler``. +@available(LambdaSwift 2.0, *) +public struct LambdaCodableAdapterSendable< + Handler: LambdaWithBackgroundProcessingHandler & Sendable, + Event: Decodable, + Output, + Decoder: LambdaEventDecoder & Sendable, + Encoder: LambdaOutputEncoder & Sendable +>: Sendable, StreamingLambdaHandler where Handler.Event == Event, Handler.Output == Output, Encoder.Output == Output { + @usableFromInline let handler: Handler + @usableFromInline let encoder: Encoder + @usableFromInline let decoder: Decoder + @usableFromInline var byteBuffer: ByteBuffer = .init() + + /// Initializes an instance given an encoder, decoder, and a handler with a non-`Void` output. + /// - Parameters: + /// - encoder: The encoder object that will be used to encode the generic `Output` obtained from the `handler`'s `outputWriter` into a `ByteBuffer`. + /// - decoder: The decoder object that will be used to decode the received `ByteBuffer` event into the generic `Event` type served to the `handler`. + /// - handler: The handler object. + @inlinable + public init(encoder: Encoder, decoder: Decoder, handler: Handler) where Output: Encodable { + self.encoder = encoder + self.decoder = decoder + self.handler = handler + } + + /// Initializes an instance given a decoder, and a handler with a `Void` output. + /// - Parameters: + /// - decoder: The decoder object that will be used to decode the received `ByteBuffer` event into the generic `Event` type served to the `handler`. + /// - handler: The handler object. + @inlinable + public init(decoder: Decoder, handler: Handler) where Output == Void, Encoder == VoidEncoder { + self.encoder = VoidEncoder() + self.decoder = decoder + self.handler = handler + } + + /// A ``StreamingLambdaHandler/handle(_:responseWriter:context:)`` wrapper. + /// - Parameters: + /// - request: The received event. + /// - responseWriter: The writer to write the computed response to. + /// - context: The ``LambdaContext`` containing the invocation's metadata. + @inlinable + public mutating func handle( + _ request: ByteBuffer, + responseWriter: Writer, + context: LambdaContext + ) async throws { + let event = try self.decoder.decode(Event.self, from: request) + + let writer = LambdaCodableResponseWriter( + encoder: self.encoder, + streamWriter: responseWriter + ) + try await self.handler.handle(event, outputWriter: writer, context: context) + } +} + + +#endif \ No newline at end of file diff --git a/Sources/AWSLambdaRuntime/ManagedRuntime/LambdaManagedRuntime+ServiceLifecycle.swift b/Sources/AWSLambdaRuntime/ManagedRuntime/LambdaManagedRuntime+ServiceLifecycle.swift new file mode 100644 index 000000000..a47bcd3a2 --- /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 000000000..003d6af49 --- /dev/null +++ b/Sources/AWSLambdaRuntime/ManagedRuntime/LambdaManagedRuntime.swift @@ -0,0 +1,146 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the SwiftAWSLambdaRuntime open source project +// +// Copyright SwiftAWSLambdaRuntime project authors +// Copyright (c) Amazon.com, Inc. or its affiliates. +// Licensed under Apache License v2.0 +// +// See LICENSE.txt for license information +// See CONTRIBUTORS.txt for the list of SwiftAWSLambdaRuntime project authors +// +// SPDX-License-Identifier: Apache-2.0 +// +//===----------------------------------------------------------------------===// + +#if ManagedRuntimeSupport + +import Logging +import NIOCore +import Synchronization + +// This is our guardian to ensure only one LambdaManagedRuntime is running at the time +// We use an Atomic here to ensure thread safety +@available(LambdaSwift 2.0, *) +private let _isLambdaManagedRuntimeRunning = Atomic(false) + +@available(LambdaSwift 2.0, *) +public final class LambdaManagedRuntime: Sendable where Handler: StreamingLambdaHandler & Sendable { + + @usableFromInline + let logger: Logger + + @usableFromInline + let eventLoop: EventLoop + + @usableFromInline + let handler: Handler + + public init( + handler: Handler, + eventLoop: EventLoop = Lambda.defaultEventLoop, + logger: Logger = Logger(label: "LambdaManagedRuntime") + ) { + self.handler = handler + self.eventLoop = eventLoop + + // by setting the log level here, we understand it can not be changed dynamically at runtime + // developers have to wait for AWS Lambda to dispose and recreate a runtime environment to pickup a change + // this approach is less flexible but more performant than reading the value of the environment variable at each invocation + var log = logger + + // use the LOG_LEVEL environment variable to set the log level. + // if the environment variable is not set, use the default log level from the logger provided + log.logLevel = Lambda.env("LOG_LEVEL").flatMap { .init(rawValue: $0) } ?? logger.logLevel + + self.logger = log + self.logger.debug("LambdaManagedRuntime initialized") + } + + #if !ServiceLifecycleSupport + public func run() async throws { + try await self._run() + } + #endif + + /// Starts the Runtime Interface Client (RIC), i.e. the loop that will poll events, + /// dispatch them to the Handler and push back results or errors. + /// Thus function make sure only one run() is called at a time + internal func _run() async throws { + + // we use an atomic global variable to ensure only one LambdaRuntime is running at the time + let (_, original) = _isLambdaManagedRuntimeRunning.compareExchange( + expected: false, + desired: true, + ordering: .acquiringAndReleasing + ) + + // if the original value was already true, run() is already running + if original { + throw LambdaRuntimeError(code: .runtimeCanOnlyBeStartedOnce) + } + + defer { + _isLambdaManagedRuntimeRunning.store(false, ordering: .releasing) + } + + // are we running inside an AWS Lambda runtime environment ? + // AWS_LAMBDA_RUNTIME_API is set when running on Lambda + // https://docs.aws.amazon.com/lambda/latest/dg/runtimes-api.html + if let runtimeEndpoint = Lambda.env("AWS_LAMBDA_RUNTIME_API") { + + // Get the max concurrency authorized by user when running on + // Lambda Managed Instances + // See: + // - https://docs.aws.amazon.com/lambda/latest/dg/lambda-managed-instances.html#lambda-managed-instances-concurrency-model + // - https://docs.aws.amazon.com/lambda/latest/dg/configuration-envvars.html + // + // and the NodeJS implementation + // https://github.com/aws/aws-lambda-nodejs-runtime-interface-client/blob/a4560c87426fa0a34756296a30d7add1388e575c/src/utils/env.ts#L34 + // https://github.com/aws/aws-lambda-nodejs-runtime-interface-client/blob/a4560c87426fa0a34756296a30d7add1388e575c/src/worker/ignition.ts#L12 + let maxConcurrency = Int(Lambda.env("AWS_LAMBDA_MAX_CONCURRENCY") ?? "1") ?? 1 + + // when max concurrency is 1, do not pay the overhead of launching a Task + if maxConcurrency <= 1 { + self.logger.trace("Starting the Runtime Interface Client") + try await LambdaRuntime.startRuntimeInterfaceClient( + endpoint: runtimeEndpoint, + handler: self.handler, + eventLoop: self.eventLoop, + logger: self.logger + ) + } else { + + try await withThrowingTaskGroup(of: Void.self) { group in + + self.logger.trace("Starting \(maxConcurrency) Runtime Interface Clients") + for i in 0.. Output +// } + +/// This protocol is exactly like ``LambdaHandler``, with the only difference being the added support for executing background +/// work after the result has been sent to the AWS Lambda control plane. +/// This is achieved by not having a return type in the `handle` function. The output is instead written into a +/// ``LambdaResponseWriter``that is passed in as an argument, meaning that the +/// ``LambdaWithBackgroundProcessingHandler/handle(_:outputWriter:context:)`` function is then +/// free to implement any background work after the result has been sent to the AWS Lambda control plane. +// @available(LambdaSwift 2.0, *) +// public protocol LambdaWithBackgroundProcessingHandler { +// /// Generic input type. +// /// The body of the request sent to Lambda will be decoded into this type for the handler to consume. +// associatedtype Event +// /// Generic output type. +// /// This is the type that the `handle` function will send through the ``LambdaResponseWriter``. +// associatedtype Output + +// /// Implement the business logic of the Lambda function here. +// /// - Parameters: +// /// - event: The generic ``LambdaWithBackgroundProcessingHandler/Event`` object representing the invocation's input data. +// /// - outputWriter: The writer to send the computed response to. A call to `outputWriter.write(_:)` will return the response to the AWS Lambda response endpoint. +// /// Any background work can then be executed before returning. +// /// - context: The ``LambdaContext`` containing the invocation's metadata. +// func handle( +// _ event: Event, +// outputWriter: some LambdaResponseWriter, +// context: LambdaContext +// ) async throws +// } + +/// Used with ``LambdaWithBackgroundProcessingHandler``. +/// A mechanism to "return" an output from ``LambdaWithBackgroundProcessingHandler/handle(_:outputWriter:context:)`` without the function needing to +/// have a return type and exit at that point. This allows for background work to be executed _after_ a response has been sent to the AWS Lambda response endpoint. +// public protocol LambdaResponseWriter { +// associatedtype Output +// /// Sends the generic ``LambdaResponseWriter/Output`` object (representing the computed result of the handler) +// /// to the AWS Lambda response endpoint. +// /// This function simply serves as a mechanism to return the computed result from a handler function +// /// without an explicit `return`. +// func write(_ output: Output) async throws +// } + +/// A ``StreamingLambdaHandler`` 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 StreamingClosureHandler: StreamingLambdaHandler { +// let body: (ByteBuffer, LambdaResponseStreamWriter, LambdaContext) async throws -> Void + +// /// Initialize an instance from a handler function in the form of a closure. +// /// - Parameter body: The handler function written as a closure. +// public init( +// body: @escaping (ByteBuffer, LambdaResponseStreamWriter, LambdaContext) async throws -> Void +// ) { +// self.body = body +// } + +// /// Calls the provided `self.body` closure with the `ByteBuffer` invocation event, the ``LambdaResponseStreamWriter``, and the ``LambdaContext`` +// /// - Parameters: +// /// - event: The invocation's input data. +// /// - responseWriter: A ``LambdaResponseStreamWriter`` to write the invocation's response to. +// /// If no response or error is written to `responseWriter` an error will be reported to the invoker. +// /// - context: The ``LambdaContext`` containing the invocation's metadata. +// public func handle( +// _ event: ByteBuffer, +// responseWriter: some LambdaResponseStreamWriter, +// context: LambdaContext +// ) async throws { +// try await self.body(event, responseWriter, context) +// } +// } + +/// 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: 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) + } +} From a9bc954142876bbd8244c1f68767e44ce0ba0ce8 Mon Sep 17 00:00:00 2001 From: Sebastien Stormacq Date: Sun, 4 Jan 2026 09:40:59 -0500 Subject: [PATCH 04/21] add examples --- Examples/ManagedInstances/.gitignore | 3 + Examples/ManagedInstances/Package.swift | 46 +++++++ Examples/ManagedInstances/README.md | 126 ++++++++++++++++++ .../Sources/BackgroundTasks/main.swift | 60 +++++++++ .../Sources/HelloJSON/main.swift | 41 ++++++ .../Sources/Streaming/main.swift | 69 ++++++++++ Examples/ManagedInstances/deploy.sh | 37 +++++ Examples/ManagedInstances/template.yaml | 81 +++++++++++ Package.swift | 10 +- Package@swift-6.0.swift | 9 +- 10 files changed, 474 insertions(+), 8 deletions(-) create mode 100644 Examples/ManagedInstances/.gitignore create mode 100644 Examples/ManagedInstances/Package.swift create mode 100644 Examples/ManagedInstances/README.md create mode 100644 Examples/ManagedInstances/Sources/BackgroundTasks/main.swift create mode 100644 Examples/ManagedInstances/Sources/HelloJSON/main.swift create mode 100644 Examples/ManagedInstances/Sources/Streaming/main.swift create mode 100755 Examples/ManagedInstances/deploy.sh create mode 100644 Examples/ManagedInstances/template.yaml diff --git a/Examples/ManagedInstances/.gitignore b/Examples/ManagedInstances/.gitignore new file mode 100644 index 000000000..a03a102dd --- /dev/null +++ b/Examples/ManagedInstances/.gitignore @@ -0,0 +1,3 @@ +response.json +samconfig.toml +Makefile diff --git a/Examples/ManagedInstances/Package.swift b/Examples/ManagedInstances/Package.swift new file mode 100644 index 000000000..2868d5bbf --- /dev/null +++ b/Examples/ManagedInstances/Package.swift @@ -0,0 +1,46 @@ +// swift-tools-version:6.2 + +import PackageDescription + +let package = Package( + name: "swift-aws-lambda-runtime-example", + platforms: [.macOS(.v15)], + products: [ + .executable(name: "HelloJSON", targets: ["HelloJSON"]), + .executable(name: "Streaming", targets: ["Streaming"]), + .executable(name: "BackgroundTasks", targets: ["BackgroundTasks"]), + ], + dependencies: [ + // For local development (default) + .package(name: "swift-aws-lambda-runtime", path: "../.."), + + // For standalone usage, comment the line above and uncomment below: + // .package(url: "https://github.com/awslabs/swift-aws-lambda-runtime.git", from: "2.0.0"), + + .package(url: "https://github.com/awslabs/swift-aws-lambda-events.git", from: "1.0.0"), + ], + targets: [ + .executableTarget( + name: "HelloJSON", + dependencies: [ + .product(name: "AWSLambdaRuntime", package: "swift-aws-lambda-runtime") + ], + path: "Sources/HelloJSON" + ), + .executableTarget( + name: "Streaming", + dependencies: [ + .product(name: "AWSLambdaRuntime", package: "swift-aws-lambda-runtime"), + .product(name: "AWSLambdaEvents", package: "swift-aws-lambda-events"), + ], + path: "Sources/Streaming" + ), + .executableTarget( + name: "BackgroundTasks", + dependencies: [ + .product(name: "AWSLambdaRuntime", package: "swift-aws-lambda-runtime") + ], + path: "Sources/BackgroundTasks" + ), + ] +) diff --git a/Examples/ManagedInstances/README.md b/Examples/ManagedInstances/README.md new file mode 100644 index 000000000..e22bc73a9 --- /dev/null +++ b/Examples/ManagedInstances/README.md @@ -0,0 +1,126 @@ +# Lambda Managed Instances Example + +This example demonstrates deploying Swift Lambda functions to Lambda Managed Instances using AWS SAM. Lambda Managed Instances provide serverless simplicity with EC2 flexibility and cost optimization by running your functions on customer-owned EC2 instances. + +## Functions Included + +1. **HelloJSON** - JSON input/output with structured data types +2. **Streaming** - Demonstrates response streaming capabilities +3. **BackgroundTasks** - Handles long-running background processing + +## Prerequisites + +- AWS CLI configured with appropriate permissions +- SAM CLI installed +- Swift 6.0+ installed +- An existing [Lambda Managed Instances capacity provider](https://docs.aws.amazon.com/lambda/latest/dg/lambda-managed-instances-capacity-providers.html) + +## Capacity Provider Configuration + +[Create your own capcity provider](https://docs.aws.amazon.com/lambda/latest/dg/lambda-managed-instances-capacity-providers.html#lambda-managed-instances-creating-capacity-provider) before deploying this example. + +This example uses a pre-configured capacity provider with the ARN: +``` +arn:aws:lambda:us-west-2:486652066693:capacity-provider:TestEC2 +``` + +## Deployment + +```bash +# Build the Swift packages +swift package archive --allow-network-access docker + +# Change the values below to match your setup +REGION=us-west-2 +CAPACITY_PROVIDER=arn:aws:lambda:us-west-2:486652066693:capacity-provider:TestEC2 + +# Deploy using SAM +sam deploy \ + --resolve-s3 \ + --template-file template.yaml \ + --stack-name swift-lambda-managed-instances \ + --capabilities CAPABILITY_IAM \ + --region ${REGION} \ + --parameter-overrides \ + CapacityProviderArn=${CAPACITY_PROVIDER} +``` + +## Function Details + +### HelloJSON Function +- **Timeout**: 15 seconds (default) +- **Concurrency**: 8 per execution environment (default) +- **Input**: JSON `{"name": "string", "age": number}` +- **Output**: JSON `{"greetings": "string"}` + +### Streaming Function +- **Timeout**: 60 seconds +- **Concurrency**: 8 per execution environment (default) +- **Features**: Response streaming enabled +- **Output**: Streams numbers with pauses + +### BackgroundTasks Function +- **Timeout**: 300 seconds (5 minutes) +- **Concurrency**: 8 per execution environment (default) +- **Input**: JSON `{"message": "string"}` +- **Features**: Long-running background processing after response + +## Testing with AWS CLI + +After deployment, invoke each function with the AWS CLI: + +### Test HelloJSON Function +```bash +REGION=us-west-2 +aws lambda invoke \ +--region ${REGION} \ +--function-name swift-lambda-managed-instances-HelloJSON \ +--payload $(echo '{ "name" : "Swift Developer", "age" : 50 }' | base64) \ +out.txt && cat out.txt && rm out.txt + +# Expected output: {"greetings": "Hello Swift Developer. You look older than your age."} +``` + +### Test Streaming Function +```bash +# Get the Streaming URL +REGION=us-west-2 +STREAMING_URL=$(aws cloudformation describe-stacks \ + --stack-name swift-lambda-managed-instances \ + --region ${REGION} \ + --query 'Stacks[0].Outputs[?OutputKey==`StreamingFunctionUrl`].OutputValue' \ + --output text) + +# Set the AWS_ACCESS_KEY_ID, AWS_SECRET_ACCESS_KEY, and AWS_SESSION_TOKEN environment variables +eval $(aws configure export-credentials --format env) + +# Test with curl (streaming response) +curl "$STREAMING_URL" \ + --user "${AWS_ACCESS_KEY_ID}":"${AWS_SECRET_ACCESS_KEY}" \ + --aws-sigv4 "aws:amz:${REGION}:lambda" \ + -H "x-amz-security-token: ${AWS_SESSION_TOKEN}" \ + --no-buffer + +# Expected output: Numbers streaming with pauses +``` + +### Test BackgroundTasks Function +```bash +# Test with AWS CLI +REGION=us-west-2 +aws lambda invoke \ +--region ${REGION} \ +--function-name swift-lambda-managed-instances-BackgroundTasks \ +--payload $(echo '{ "message" : "Additional processing in the background" }' | base64) \ +out.txt && cat out.txt && rm out.txt + +# Expected output: {"echoedMessage": "Additional processing in the background"} +# Note: Background processing continues after response is sent +``` + +## Cleanup + +To remove all resources: +```bash +sam delete --stack-name swift-lambda-managed-instances --region ${REGION}$ +``` \ No newline at end of file diff --git a/Examples/ManagedInstances/Sources/BackgroundTasks/main.swift b/Examples/ManagedInstances/Sources/BackgroundTasks/main.swift new file mode 100644 index 000000000..f9fc81edc --- /dev/null +++ b/Examples/ManagedInstances/Sources/BackgroundTasks/main.swift @@ -0,0 +1,60 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the SwiftAWSLambdaRuntime open source project +// +// Copyright SwiftAWSLambdaRuntime project authors +// Copyright (c) Amazon.com, Inc. or its affiliates. +// Licensed under Apache License v2.0 +// +// See LICENSE.txt for license information +// See CONTRIBUTORS.txt for the list of SwiftAWSLambdaRuntime project authors +// +// SPDX-License-Identifier: Apache-2.0 +// +//===----------------------------------------------------------------------===// + +import AWSLambdaRuntime + +#if canImport(FoundationEssentials) +import FoundationEssentials +#else +import Foundation +#endif + +// for a simple struct as this one, the compiler automatically infers Sendable +// With Lambda Managed Instances, your handler struct MUST be Sendable +struct BackgroundProcessingHandler: LambdaWithBackgroundProcessingHandler, Sendable { + struct Input: Decodable { + let message: String + } + + struct Greeting: Encodable { + let echoedMessage: String + } + + typealias Event = Input + typealias Output = Greeting + + func handle( + _ event: Event, + outputWriter: some LambdaResponseWriter, + context: LambdaContext + ) async throws { + // Return result to the Lambda control plane + context.logger.debug("BackgroundProcessingHandler - message received") + try await outputWriter.write(Greeting(echoedMessage: event.message)) + + // Perform some background work, e.g: + context.logger.debug("BackgroundProcessingHandler - response sent. Performing background tasks.") + try await Task.sleep(for: .seconds(10)) + + // Exit the function. All asynchronous work has been executed before exiting the scope of this function. + // Follows structured concurrency principles. + context.logger.debug("BackgroundProcessingHandler - Background tasks completed. Returning") + return + } +} + +let adapter = LambdaCodableAdapterSendable(handler: BackgroundProcessingHandler()) +let runtime = LambdaManagedRuntime(handler: adapter) +try await runtime.run() diff --git a/Examples/ManagedInstances/Sources/HelloJSON/main.swift b/Examples/ManagedInstances/Sources/HelloJSON/main.swift new file mode 100644 index 000000000..f166c02f5 --- /dev/null +++ b/Examples/ManagedInstances/Sources/HelloJSON/main.swift @@ -0,0 +1,41 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the SwiftAWSLambdaRuntime open source project +// +// Copyright SwiftAWSLambdaRuntime project authors +// Copyright (c) Amazon.com, Inc. or its affiliates. +// Licensed under Apache License v2.0 +// +// See LICENSE.txt for license information +// See CONTRIBUTORS.txt for the list of SwiftAWSLambdaRuntime project authors +// +// SPDX-License-Identifier: Apache-2.0 +// +//===----------------------------------------------------------------------===// + +import AWSLambdaRuntime + +// in this example we are receiving and responding with JSON structures + +// the data structure to represent the input parameter +struct HelloRequest: Decodable { + let name: String + let age: Int +} + +// the data structure to represent the output response +struct HelloResponse: Encodable { + let greetings: String +} + +// the Lambda runtime +let runtime = LambdaManagedRuntime { + (event: HelloRequest, context: LambdaContext) in + + HelloResponse( + greetings: "Hello \(event.name). You look \(event.age > 30 ? "younger" : "older") than your age." + ) +} + +// start the loop +try await runtime.run() diff --git a/Examples/ManagedInstances/Sources/Streaming/main.swift b/Examples/ManagedInstances/Sources/Streaming/main.swift new file mode 100644 index 000000000..e7183cec3 --- /dev/null +++ b/Examples/ManagedInstances/Sources/Streaming/main.swift @@ -0,0 +1,69 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the SwiftAWSLambdaRuntime open source project +// +// Copyright SwiftAWSLambdaRuntime project authors +// Copyright (c) Amazon.com, Inc. or its affiliates. +// Licensed under Apache License v2.0 +// +// See LICENSE.txt for license information +// See CONTRIBUTORS.txt for the list of SwiftAWSLambdaRuntime project authors +// +// SPDX-License-Identifier: Apache-2.0 +// +//===----------------------------------------------------------------------===// + +import AWSLambdaEvents +import AWSLambdaRuntime +import NIOCore + +#if canImport(FoundationEssentials) +import FoundationEssentials +#else +import Foundation +#endif + +// for a simple struct as this one, the compiler automatically infers Sendable +// With Lambda Managed Instances, your handler struct MUST be Sendable +struct SendNumbersWithPause: StreamingLambdaHandler, Sendable { + func handle( + _ event: ByteBuffer, + responseWriter: some LambdaResponseStreamWriter, + context: LambdaContext + ) async throws { + + // the payload here is an API Gateway V1 request + // Check the body of the request to extract the business event + let payload = try JSONDecoder().decode(FunctionURLRequest.self, from: Data(event.readableBytesView)) + let _ = payload.body + + // Send HTTP status code and headers before streaming the response body + try await responseWriter.writeStatusAndHeaders( + StreamingLambdaStatusAndHeadersResponse( + statusCode: 418, // I'm a tea pot + headers: [ + "Content-Type": "text/plain", + "x-my-custom-header": "streaming-example", + ] + ) + ) + + // Stream numbers with pauses to demonstrate streaming functionality + for i in 1...3 { + // Send partial data + try await responseWriter.write(ByteBuffer(string: "Number: \(i)\n")) + + // Perform some long asynchronous work to simulate processing + try await Task.sleep(for: .milliseconds(1000)) + } + + // Send final message + try await responseWriter.write(ByteBuffer(string: "Streaming complete!\n")) + + // All data has been sent. Close off the response stream. + try await responseWriter.finish() + } +} + +let runtime = LambdaManagedRuntime(handler: SendNumbersWithPause()) +try await runtime.run() diff --git a/Examples/ManagedInstances/deploy.sh b/Examples/ManagedInstances/deploy.sh new file mode 100755 index 000000000..7cab53063 --- /dev/null +++ b/Examples/ManagedInstances/deploy.sh @@ -0,0 +1,37 @@ +#!/bin/bash + +# Deploy script for Lambda Managed Instances example +# This script builds the Swift Lambda functions and deploys them using SAM + +set -e + +echo "🚀 Building Swift Lambda functions for Managed Instances..." + +# Build all targets +LAMBDA_USE_LOCAL_DEPS=../.. swift package archive --allow-network-connections docker --disable-docker-image-update + +echo "📦 Packaging complete. Deploying to AWS..." + +# Change the values below to match your setup +REGION=us-west-2 +CAPACITY_PROVIDER=arn:aws:lambda:us-west-2:486652066693:capacity-provider:TestEC2 + +# Deploy using SAM +sam deploy \ + --region ${REGION} \ + --resolve-s3 \ + --template-file template.yaml \ + --stack-name swift-lambda-managed-instances \ + --capabilities CAPABILITY_IAM \ + --region us-west-2 \ + --parameter-overrides \ + CapacityProviderArn=${CAPACITY_PROVIDER} + +echo "✅ Deployment complete!" +echo "" +echo "📋 Stack outputs:" +aws cloudformation describe-stacks \ + --stack-name swift-lambda-managed-instances \ + --region ${REGION} \ + --query 'Stacks[0].Outputs[*].[OutputKey,OutputValue]' \ + --output table diff --git a/Examples/ManagedInstances/template.yaml b/Examples/ManagedInstances/template.yaml new file mode 100644 index 000000000..a807d640a --- /dev/null +++ b/Examples/ManagedInstances/template.yaml @@ -0,0 +1,81 @@ +AWSTemplateFormatVersion: '2010-09-09' +Transform: AWS::Serverless-2016-10-31 +Description: SAM Template for Lambda Managed Instances Example + +# This template deploys three Lambda functions to Lambda Managed Instances +# using a pre-existing capacity provider. + +Parameters: + CapacityProviderArn: + Type: String + Default: arn:aws:lambda:us-west-2:486652066693:capacity-provider:TestEC2 # TODO : CHANGE ME! + 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" \ No newline at end of file diff --git a/Package.swift b/Package.swift index 99ea9667b..36a0e4672 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 5f4021d4e..be01537f7 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( From 79408e79d9a851bddad9a5c6f7790ef987d723a3 Mon Sep 17 00:00:00 2001 From: Sebastien Stormacq Date: Sun, 4 Jan 2026 10:03:52 -0500 Subject: [PATCH 05/21] swift format --- .../FoundationSupport/LambdaManagedRuntime+JSON.swift | 4 ++-- .../ManagedRuntime/LambdaManagedRuntime+Codable.swift | 3 +-- .../ManagedRuntime/LambdaManagedRuntime.swift | 2 +- .../ManagedRuntime/LambdaManagedRuntimeHandlers.swift | 2 +- 4 files changed, 5 insertions(+), 6 deletions(-) diff --git a/Sources/AWSLambdaRuntime/FoundationSupport/LambdaManagedRuntime+JSON.swift b/Sources/AWSLambdaRuntime/FoundationSupport/LambdaManagedRuntime+JSON.swift index 8d7f47e4f..261594670 100644 --- a/Sources/AWSLambdaRuntime/FoundationSupport/LambdaManagedRuntime+JSON.swift +++ b/Sources/AWSLambdaRuntime/FoundationSupport/LambdaManagedRuntime+JSON.swift @@ -129,7 +129,7 @@ extension LambdaManagedRuntime { Output, LambdaJSONEventDecoderSendable, LambdaJSONOutputEncoderSendable - > + > { let handler = LambdaCodableAdapterSendable( encoder: encoder, @@ -229,4 +229,4 @@ extension LambdaManagedRuntime { } #endif // trait: FoundationJSONSupport -#endif // trait: ManagedRuntimeSupport \ No newline at end of file +#endif // trait: ManagedRuntimeSupport diff --git a/Sources/AWSLambdaRuntime/ManagedRuntime/LambdaManagedRuntime+Codable.swift b/Sources/AWSLambdaRuntime/ManagedRuntime/LambdaManagedRuntime+Codable.swift index 393732614..28d3115b6 100644 --- a/Sources/AWSLambdaRuntime/ManagedRuntime/LambdaManagedRuntime+Codable.swift +++ b/Sources/AWSLambdaRuntime/ManagedRuntime/LambdaManagedRuntime+Codable.swift @@ -108,5 +108,4 @@ public struct LambdaCodableAdapterSendable< } } - -#endif \ No newline at end of file +#endif diff --git a/Sources/AWSLambdaRuntime/ManagedRuntime/LambdaManagedRuntime.swift b/Sources/AWSLambdaRuntime/ManagedRuntime/LambdaManagedRuntime.swift index 003d6af49..59135122a 100644 --- a/Sources/AWSLambdaRuntime/ManagedRuntime/LambdaManagedRuntime.swift +++ b/Sources/AWSLambdaRuntime/ManagedRuntime/LambdaManagedRuntime.swift @@ -32,7 +32,7 @@ public final class LambdaManagedRuntime: Sendable where Handler: Stream @usableFromInline let eventLoop: EventLoop - + @usableFromInline let handler: Handler diff --git a/Sources/AWSLambdaRuntime/ManagedRuntime/LambdaManagedRuntimeHandlers.swift b/Sources/AWSLambdaRuntime/ManagedRuntime/LambdaManagedRuntimeHandlers.swift index b6167ab9e..286eb882b 100644 --- a/Sources/AWSLambdaRuntime/ManagedRuntime/LambdaManagedRuntimeHandlers.swift +++ b/Sources/AWSLambdaRuntime/ManagedRuntime/LambdaManagedRuntimeHandlers.swift @@ -157,7 +157,7 @@ import NIOCore /// 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: LambdaHandler, Sendable{ +public struct ClosureHandlerSendable: LambdaHandler, Sendable { let body: @Sendable (Event, LambdaContext) async throws -> Output /// Initialize with a closure handler over generic `Input` and `Output` types. From 0f932125252c99f36829e5b752aff372317b96c4 Mon Sep 17 00:00:00 2001 From: Sebastien Stormacq Date: Sun, 4 Jan 2026 10:07:53 -0500 Subject: [PATCH 06/21] cleanup --- .../LambdaRuntimeHandlers.swift | 182 ------------------ .../LambdaManagedRuntimeHandlers.swift | 139 +------------ 2 files changed, 2 insertions(+), 319 deletions(-) delete mode 100644 Sources/AWSLambdaRuntime/LambdaRuntimeHandlers.swift diff --git a/Sources/AWSLambdaRuntime/LambdaRuntimeHandlers.swift b/Sources/AWSLambdaRuntime/LambdaRuntimeHandlers.swift deleted file mode 100644 index 94e923079..000000000 --- a/Sources/AWSLambdaRuntime/LambdaRuntimeHandlers.swift +++ /dev/null @@ -1,182 +0,0 @@ -//===----------------------------------------------------------------------===// -// -// 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 - -/// The base handler protocol that receives a `ByteBuffer` representing the incoming event and returns the response as a `ByteBuffer` too. -/// This handler protocol supports response streaming. Bytes can be streamed outwards through the ``LambdaResponseStreamWriter`` -/// passed as an argument in the ``handle(_:responseWriter:context:)`` function. -/// Background work can also be executed after returning the response. After closing the response stream by calling -/// ``LambdaResponseStreamWriter/finish()`` or ``LambdaResponseStreamWriter/writeAndFinish(_:)``, -/// the ``handle(_:responseWriter:context:)`` function is free to execute any background work. -@available(LambdaSwift 2.0, *) -public protocol StreamingLambdaHandler: _Lambda_SendableMetatype { - /// The handler function -- implement the business logic of the Lambda function here. - /// - Parameters: - /// - event: The invocation's input data. - /// - responseWriter: A ``LambdaResponseStreamWriter`` to write the invocation's response to. - /// If no response or error is written to `responseWriter` an error will be reported to the invoker. - /// - context: The ``LambdaContext`` containing the invocation's metadata. - /// - Throws: - /// How the thrown error will be handled by the runtime: - /// - An invocation error will be reported if the error is thrown before the first call to - /// ``LambdaResponseStreamWriter/write(_:)``. - /// - If the error is thrown after call(s) to ``LambdaResponseStreamWriter/write(_:)`` but before - /// a call to ``LambdaResponseStreamWriter/finish()``, the response stream will be closed and trailing - /// headers will be sent. - /// - If ``LambdaResponseStreamWriter/finish()`` has already been called before the error is thrown, the - /// error will be logged. - mutating func handle( - _ event: ByteBuffer, - responseWriter: some LambdaResponseStreamWriter, - context: LambdaContext - ) async throws -} - -/// A writer object to write the Lambda response stream into. The HTTP response is started lazily. -/// before the first call to ``write(_:)`` or ``writeAndFinish(_:)``. -public protocol LambdaResponseStreamWriter { - /// Write a response part into the stream. Bytes written are streamed continually. - /// - Parameter buffer: The buffer to write. - /// - Parameter hasCustomHeaders: If `true`, the response will be sent with custom HTTP status code and headers. - func write(_ buffer: ByteBuffer, hasCustomHeaders: Bool) async throws - - /// End the response stream and the underlying HTTP response. - func finish() async throws - - /// Write a response part into the stream and then end the stream as well as the underlying HTTP response. - /// - Parameter buffer: The buffer to write. - func writeAndFinish(_ buffer: ByteBuffer) async throws -} - -/// This handler protocol is intended to serve the most common use-cases. -/// This protocol is completely agnostic to any encoding/decoding -- decoding the received event invocation into an ``Event`` object and encoding the returned ``Output`` object is handled by the library. -/// The``handle(_:context:)`` function simply receives the generic ``Event`` object as input and returns the generic ``Output`` object. -/// -/// - note: This handler protocol does not support response streaming because the output has to be encoded prior to it being sent, e.g. it is not possible to encode a partial/incomplete JSON string. -/// This protocol also does not support the execution of background work after the response has been returned -- the ``LambdaWithBackgroundProcessingHandler`` protocol caters for such use-cases. -@available(LambdaSwift 2.0, *) -public protocol LambdaHandler { - /// Generic input type. - /// The body of the request sent to Lambda will be decoded into this type for the handler to consume. - associatedtype Event - /// Generic output type. - /// This is the return type of the ``LambdaHandler/handle(_:context:)`` function. - associatedtype Output - - /// Implement the business logic of the Lambda function here. - /// - Parameters: - /// - event: The generic ``LambdaHandler/Event`` object representing the invocation's input data. - /// - context: The ``LambdaContext`` containing the invocation's metadata. - /// - Returns: A generic ``Output`` object representing the computed result. - func handle(_ event: Event, context: LambdaContext) async throws -> Output -} - -/// This protocol is exactly like ``LambdaHandler``, with the only difference being the added support for executing background -/// work after the result has been sent to the AWS Lambda control plane. -/// This is achieved by not having a return type in the `handle` function. The output is instead written into a -/// ``LambdaResponseWriter``that is passed in as an argument, meaning that the -/// ``LambdaWithBackgroundProcessingHandler/handle(_:outputWriter:context:)`` function is then -/// free to implement any background work after the result has been sent to the AWS Lambda control plane. -@available(LambdaSwift 2.0, *) -public protocol LambdaWithBackgroundProcessingHandler { - /// Generic input type. - /// The body of the request sent to Lambda will be decoded into this type for the handler to consume. - associatedtype Event - /// Generic output type. - /// This is the type that the `handle` function will send through the ``LambdaResponseWriter``. - associatedtype Output - - /// Implement the business logic of the Lambda function here. - /// - Parameters: - /// - event: The generic ``LambdaWithBackgroundProcessingHandler/Event`` object representing the invocation's input data. - /// - outputWriter: The writer to send the computed response to. A call to `outputWriter.write(_:)` will return the response to the AWS Lambda response endpoint. - /// Any background work can then be executed before returning. - /// - context: The ``LambdaContext`` containing the invocation's metadata. - func handle( - _ event: Event, - outputWriter: some LambdaResponseWriter, - context: LambdaContext - ) async throws -} - -/// Used with ``LambdaWithBackgroundProcessingHandler``. -/// A mechanism to "return" an output from ``LambdaWithBackgroundProcessingHandler/handle(_:outputWriter:context:)`` without the function needing to -/// have a return type and exit at that point. This allows for background work to be executed _after_ a response has been sent to the AWS Lambda response endpoint. -public protocol LambdaResponseWriter { - associatedtype Output - /// Sends the generic ``LambdaResponseWriter/Output`` object (representing the computed result of the handler) - /// to the AWS Lambda response endpoint. - /// This function simply serves as a mechanism to return the computed result from a handler function - /// without an explicit `return`. - func write(_ output: Output) async throws -} - -/// A ``StreamingLambdaHandler`` 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 StreamingClosureHandler: StreamingLambdaHandler { - let body: (ByteBuffer, LambdaResponseStreamWriter, LambdaContext) async throws -> Void - - /// Initialize an instance from a handler function in the form of a closure. - /// - Parameter body: The handler function written as a closure. - public init( - body: @escaping (ByteBuffer, LambdaResponseStreamWriter, LambdaContext) async throws -> Void - ) { - self.body = body - } - - /// Calls the provided `self.body` closure with the `ByteBuffer` invocation event, the ``LambdaResponseStreamWriter``, and the ``LambdaContext`` - /// - Parameters: - /// - event: The invocation's input data. - /// - responseWriter: A ``LambdaResponseStreamWriter`` to write the invocation's response to. - /// If no response or error is written to `responseWriter` an error will be reported to the invoker. - /// - context: The ``LambdaContext`` containing the invocation's metadata. - public func handle( - _ event: ByteBuffer, - responseWriter: some LambdaResponseStreamWriter, - context: LambdaContext - ) async throws { - try await self.body(event, responseWriter, context) - } -} - -/// 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 ClosureHandler: LambdaHandler { - let body: (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: sending @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: @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) - } -} diff --git a/Sources/AWSLambdaRuntime/ManagedRuntime/LambdaManagedRuntimeHandlers.swift b/Sources/AWSLambdaRuntime/ManagedRuntime/LambdaManagedRuntimeHandlers.swift index 286eb882b..ec4b493c8 100644 --- a/Sources/AWSLambdaRuntime/ManagedRuntime/LambdaManagedRuntimeHandlers.swift +++ b/Sources/AWSLambdaRuntime/ManagedRuntime/LambdaManagedRuntimeHandlers.swift @@ -16,143 +16,7 @@ import Logging import NIOCore -/// The base handler protocol that receives a `ByteBuffer` representing the incoming event and returns the response as a `ByteBuffer` too. -/// This handler protocol supports response streaming. Bytes can be streamed outwards through the ``LambdaResponseStreamWriter`` -/// passed as an argument in the ``handle(_:responseWriter:context:)`` function. -/// Background work can also be executed after returning the response. After closing the response stream by calling -/// ``LambdaResponseStreamWriter/finish()`` or ``LambdaResponseStreamWriter/writeAndFinish(_:)``, -/// the ``handle(_:responseWriter:context:)`` function is free to execute any background work. -// @available(LambdaSwift 2.0, *) -// public protocol StreamingLambdaHandler: _Lambda_SendableMetatype { -// /// The handler function -- implement the business logic of the Lambda function here. -// /// - Parameters: -// /// - event: The invocation's input data. -// /// - responseWriter: A ``LambdaResponseStreamWriter`` to write the invocation's response to. -// /// If no response or error is written to `responseWriter` an error will be reported to the invoker. -// /// - context: The ``LambdaContext`` containing the invocation's metadata. -// /// - Throws: -// /// How the thrown error will be handled by the runtime: -// /// - An invocation error will be reported if the error is thrown before the first call to -// /// ``LambdaResponseStreamWriter/write(_:)``. -// /// - If the error is thrown after call(s) to ``LambdaResponseStreamWriter/write(_:)`` but before -// /// a call to ``LambdaResponseStreamWriter/finish()``, the response stream will be closed and trailing -// /// headers will be sent. -// /// - If ``LambdaResponseStreamWriter/finish()`` has already been called before the error is thrown, the -// /// error will be logged. -// mutating func handle( -// _ event: ByteBuffer, -// responseWriter: some LambdaResponseStreamWriter, -// context: LambdaContext -// ) async throws -// } - -/// A writer object to write the Lambda response stream into. The HTTP response is started lazily. -/// before the first call to ``write(_:)`` or ``writeAndFinish(_:)``. -// public protocol LambdaResponseStreamWriter { -// /// Write a response part into the stream. Bytes written are streamed continually. -// /// - Parameter buffer: The buffer to write. -// /// - Parameter hasCustomHeaders: If `true`, the response will be sent with custom HTTP status code and headers. -// func write(_ buffer: ByteBuffer, hasCustomHeaders: Bool) async throws - -// /// End the response stream and the underlying HTTP response. -// func finish() async throws - -// /// Write a response part into the stream and then end the stream as well as the underlying HTTP response. -// /// - Parameter buffer: The buffer to write. -// func writeAndFinish(_ buffer: ByteBuffer) async throws -// } - -/// This handler protocol is intended to serve the most common use-cases. -/// This protocol is completely agnostic to any encoding/decoding -- decoding the received event invocation into an ``Event`` object and encoding the returned ``Output`` object is handled by the library. -/// The``handle(_:context:)`` function simply receives the generic ``Event`` object as input and returns the generic ``Output`` object. -/// -/// - note: This handler protocol does not support response streaming because the output has to be encoded prior to it being sent, e.g. it is not possible to encode a partial/incomplete JSON string. -/// This protocol also does not support the execution of background work after the response has been returned -- the ``LambdaWithBackgroundProcessingHandler`` protocol caters for such use-cases. -// @available(LambdaSwift 2.0, *) -// public protocol LambdaHandler { -// /// Generic input type. -// /// The body of the request sent to Lambda will be decoded into this type for the handler to consume. -// associatedtype Event -// /// Generic output type. -// /// This is the return type of the ``LambdaHandler/handle(_:context:)`` function. -// associatedtype Output - -// /// Implement the business logic of the Lambda function here. -// /// - Parameters: -// /// - event: The generic ``LambdaHandler/Event`` object representing the invocation's input data. -// /// - context: The ``LambdaContext`` containing the invocation's metadata. -// /// - Returns: A generic ``Output`` object representing the computed result. -// func handle(_ event: Event, context: LambdaContext) async throws -> Output -// } - -/// This protocol is exactly like ``LambdaHandler``, with the only difference being the added support for executing background -/// work after the result has been sent to the AWS Lambda control plane. -/// This is achieved by not having a return type in the `handle` function. The output is instead written into a -/// ``LambdaResponseWriter``that is passed in as an argument, meaning that the -/// ``LambdaWithBackgroundProcessingHandler/handle(_:outputWriter:context:)`` function is then -/// free to implement any background work after the result has been sent to the AWS Lambda control plane. -// @available(LambdaSwift 2.0, *) -// public protocol LambdaWithBackgroundProcessingHandler { -// /// Generic input type. -// /// The body of the request sent to Lambda will be decoded into this type for the handler to consume. -// associatedtype Event -// /// Generic output type. -// /// This is the type that the `handle` function will send through the ``LambdaResponseWriter``. -// associatedtype Output - -// /// Implement the business logic of the Lambda function here. -// /// - Parameters: -// /// - event: The generic ``LambdaWithBackgroundProcessingHandler/Event`` object representing the invocation's input data. -// /// - outputWriter: The writer to send the computed response to. A call to `outputWriter.write(_:)` will return the response to the AWS Lambda response endpoint. -// /// Any background work can then be executed before returning. -// /// - context: The ``LambdaContext`` containing the invocation's metadata. -// func handle( -// _ event: Event, -// outputWriter: some LambdaResponseWriter, -// context: LambdaContext -// ) async throws -// } - -/// Used with ``LambdaWithBackgroundProcessingHandler``. -/// A mechanism to "return" an output from ``LambdaWithBackgroundProcessingHandler/handle(_:outputWriter:context:)`` without the function needing to -/// have a return type and exit at that point. This allows for background work to be executed _after_ a response has been sent to the AWS Lambda response endpoint. -// public protocol LambdaResponseWriter { -// associatedtype Output -// /// Sends the generic ``LambdaResponseWriter/Output`` object (representing the computed result of the handler) -// /// to the AWS Lambda response endpoint. -// /// This function simply serves as a mechanism to return the computed result from a handler function -// /// without an explicit `return`. -// func write(_ output: Output) async throws -// } - -/// A ``StreamingLambdaHandler`` 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 StreamingClosureHandler: StreamingLambdaHandler { -// let body: (ByteBuffer, LambdaResponseStreamWriter, LambdaContext) async throws -> Void - -// /// Initialize an instance from a handler function in the form of a closure. -// /// - Parameter body: The handler function written as a closure. -// public init( -// body: @escaping (ByteBuffer, LambdaResponseStreamWriter, LambdaContext) async throws -> Void -// ) { -// self.body = body -// } - -// /// Calls the provided `self.body` closure with the `ByteBuffer` invocation event, the ``LambdaResponseStreamWriter``, and the ``LambdaContext`` -// /// - Parameters: -// /// - event: The invocation's input data. -// /// - responseWriter: A ``LambdaResponseStreamWriter`` to write the invocation's response to. -// /// If no response or error is written to `responseWriter` an error will be reported to the invoker. -// /// - context: The ``LambdaContext`` containing the invocation's metadata. -// public func handle( -// _ event: ByteBuffer, -// responseWriter: some LambdaResponseStreamWriter, -// context: LambdaContext -// ) async throws { -// try await self.body(event, responseWriter, context) -// } -// } +#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. @@ -180,3 +44,4 @@ public struct ClosureHandlerSendable: LambdaHandler, S try await self.body(event, context) } } +#endif From ec514de25fffbdcbd2269fa5353d251ac392afb4 Mon Sep 17 00:00:00 2001 From: Sebastien Stormacq Date: Sun, 4 Jan 2026 10:12:54 -0500 Subject: [PATCH 07/21] fix yaml lint --- Examples/ManagedInstances/template.yaml | 28 ++++++++++++------------- 1 file changed, 14 insertions(+), 14 deletions(-) diff --git a/Examples/ManagedInstances/template.yaml b/Examples/ManagedInstances/template.yaml index a807d640a..2aa50dd01 100644 --- a/Examples/ManagedInstances/template.yaml +++ b/Examples/ManagedInstances/template.yaml @@ -8,22 +8,22 @@ Description: SAM Template for Lambda Managed Instances Example Parameters: CapacityProviderArn: Type: String - Default: arn:aws:lambda:us-west-2:486652066693:capacity-provider:TestEC2 # TODO : CHANGE ME! + Default: arn:aws:lambda:us-west-2:486652066693:capacity-provider:TestEC2 # TODO : CHANGE ME! 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 + 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 @@ -59,7 +59,7 @@ Outputs: # Function URL for reference StreamingFunctionUrl: Description: Streaming Function URL - Value: !GetAtt StreamingFunctionUrl.FunctionUrl + Value: !GetAtt StreamingFunctionUrl.FunctionUrl # Function ARNs for reference HelloJSONFunctionArn: @@ -78,4 +78,4 @@ Outputs: Description: "BackgroundTasks Function ARN" Value: !GetAtt BackgroundTasksFunction.Arn Export: - Name: !Sub "${AWS::StackName}-BackgroundTasksArn" \ No newline at end of file + Name: !Sub "${AWS::StackName}-BackgroundTasksArn" From e6ceca8237fd62aba934fe829c6a089d67037b23 Mon Sep 17 00:00:00 2001 From: Sebastien Stormacq Date: Sun, 4 Jan 2026 10:13:02 -0500 Subject: [PATCH 08/21] fix missing license header --- Examples/ManagedInstances/deploy.sh | 37 ----------------------------- 1 file changed, 37 deletions(-) delete mode 100755 Examples/ManagedInstances/deploy.sh diff --git a/Examples/ManagedInstances/deploy.sh b/Examples/ManagedInstances/deploy.sh deleted file mode 100755 index 7cab53063..000000000 --- a/Examples/ManagedInstances/deploy.sh +++ /dev/null @@ -1,37 +0,0 @@ -#!/bin/bash - -# Deploy script for Lambda Managed Instances example -# This script builds the Swift Lambda functions and deploys them using SAM - -set -e - -echo "🚀 Building Swift Lambda functions for Managed Instances..." - -# Build all targets -LAMBDA_USE_LOCAL_DEPS=../.. swift package archive --allow-network-connections docker --disable-docker-image-update - -echo "📦 Packaging complete. Deploying to AWS..." - -# Change the values below to match your setup -REGION=us-west-2 -CAPACITY_PROVIDER=arn:aws:lambda:us-west-2:486652066693:capacity-provider:TestEC2 - -# Deploy using SAM -sam deploy \ - --region ${REGION} \ - --resolve-s3 \ - --template-file template.yaml \ - --stack-name swift-lambda-managed-instances \ - --capabilities CAPABILITY_IAM \ - --region us-west-2 \ - --parameter-overrides \ - CapacityProviderArn=${CAPACITY_PROVIDER} - -echo "✅ Deployment complete!" -echo "" -echo "📋 Stack outputs:" -aws cloudformation describe-stacks \ - --stack-name swift-lambda-managed-instances \ - --region ${REGION} \ - --query 'Stacks[0].Outputs[*].[OutputKey,OutputValue]' \ - --output table From e3d37e830aadabbf644beda23e202c355e6ac0e4 Mon Sep 17 00:00:00 2001 From: Sebastien Stormacq Date: Sun, 4 Jan 2026 10:15:07 -0500 Subject: [PATCH 09/21] add Managed Instances to the integration tests --- .github/workflows/pull_request.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/pull_request.yml b/.github/workflows/pull_request.yml index 7964fef3b..5c17f54e2 100644 --- a/.github/workflows/pull_request.yml +++ b/.github/workflows/pull_request.yml @@ -40,7 +40,7 @@ jobs: # We pass the list of examples here, but we can't pass an array as argument # Instead, we pass a String with a valid JSON array. # The workaround is mentioned here https://github.com/orgs/community/discussions/11692 - examples: "[ 'APIGatewayV1', 'APIGatewayV2', 'APIGatewayV2+LambdaAuthorizer', 'BackgroundTasks', 'HelloJSON', 'HelloWorld', 'HelloWorldNoTraits', 'HummingbirdLambda', 'MultiSourceAPI', 'MultiTenant', 'ResourcesPackaging', 'S3EventNotifier', 'S3_AWSSDK', 'S3_Soto', 'Streaming+APIGateway', 'Streaming+FunctionUrl', 'Streaming+Codable', 'ServiceLifecycle+Postgres', 'Testing', 'Tutorial' ]" + examples: "[ 'APIGatewayV1', 'APIGatewayV2', 'APIGatewayV2+LambdaAuthorizer', 'BackgroundTasks', 'HelloJSON', 'HelloWorld', 'HelloWorldNoTraits', 'HummingbirdLambda', 'ManagedInstances', 'MultiSourceAPI', 'MultiTenant', 'ResourcesPackaging', 'S3EventNotifier', 'S3_AWSSDK', 'S3_Soto', 'Streaming+APIGateway', 'Streaming+FunctionUrl', 'Streaming+Codable', 'ServiceLifecycle+Postgres', 'Testing', 'Tutorial' ]" archive_plugin_examples: "[ 'HelloWorld', 'ResourcesPackaging' ]" archive_plugin_enabled: true From c435933fc4dd6bbee7495c36a38fcbc1ab2d405d Mon Sep 17 00:00:00 2001 From: Sebastien Stormacq Date: Sun, 4 Jan 2026 11:29:15 -0500 Subject: [PATCH 10/21] add some tests for LambdaManagedRuntime --- .../Tests/LambdaStreamingCodableTests.swift | 2 +- .../LambdaManagedRuntimeTests.swift | 200 ++++++++++++++++++ Tests/AWSLambdaRuntimeTests/Utils.swift | 19 ++ 3 files changed, 220 insertions(+), 1 deletion(-) create mode 100644 Tests/AWSLambdaRuntimeTests/LambdaManagedRuntimeTests.swift diff --git a/Examples/Streaming+Codable/Tests/LambdaStreamingCodableTests.swift b/Examples/Streaming+Codable/Tests/LambdaStreamingCodableTests.swift index ca25ed34c..3754c2e44 100644 --- a/Examples/Streaming+Codable/Tests/LambdaStreamingCodableTests.swift +++ b/Examples/Streaming+Codable/Tests/LambdaStreamingCodableTests.swift @@ -336,7 +336,7 @@ extension LambdaContext { traceID: "test-trace-id", invokedFunctionARN: "arn:aws:lambda:us-east-1:123456789012:function:test", timeout: .seconds(30), - logger: Logger(label: "test") + logger: Logger(label: "MockedLambdaContext") ) } } diff --git a/Tests/AWSLambdaRuntimeTests/LambdaManagedRuntimeTests.swift b/Tests/AWSLambdaRuntimeTests/LambdaManagedRuntimeTests.swift new file mode 100644 index 000000000..a948d4af0 --- /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") + ) + } +} From 8ea2da54c5fffebe09bb7f693bc41a53ebd391c5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A9bastien=20Stormacq?= Date: Sun, 4 Jan 2026 11:47:46 -0500 Subject: [PATCH 11/21] Update Examples/ManagedInstances/Sources/Streaming/main.swift Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- Examples/ManagedInstances/Sources/Streaming/main.swift | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Examples/ManagedInstances/Sources/Streaming/main.swift b/Examples/ManagedInstances/Sources/Streaming/main.swift index e7183cec3..3109d55fd 100644 --- a/Examples/ManagedInstances/Sources/Streaming/main.swift +++ b/Examples/ManagedInstances/Sources/Streaming/main.swift @@ -32,8 +32,8 @@ struct SendNumbersWithPause: StreamingLambdaHandler, Sendable { context: LambdaContext ) async throws { - // the payload here is an API Gateway V1 request - // Check the body of the request to extract the business event + // 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 From 32d62a1f13e400fbe72ae835e52af413e4b1b2ef Mon Sep 17 00:00:00 2001 From: Sebastien Stormacq Date: Sun, 4 Jan 2026 11:52:51 -0500 Subject: [PATCH 12/21] fix typos --- Examples/ManagedInstances/README.md | 6 +++--- Examples/ManagedInstances/template.yaml | 2 +- .../ManagedRuntime/LambdaManagedRuntime.swift | 2 +- Sources/AWSLambdaRuntime/Runtime/LambdaRuntime.swift | 2 +- 4 files changed, 6 insertions(+), 6 deletions(-) diff --git a/Examples/ManagedInstances/README.md b/Examples/ManagedInstances/README.md index e22bc73a9..7e8b0fdbe 100644 --- a/Examples/ManagedInstances/README.md +++ b/Examples/ManagedInstances/README.md @@ -17,7 +17,7 @@ This example demonstrates deploying Swift Lambda functions to Lambda Managed Ins ## Capacity Provider Configuration -[Create your own capcity 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. +[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: ``` @@ -32,7 +32,7 @@ swift package archive --allow-network-access docker # Change the values below to match your setup REGION=us-west-2 -CAPACITY_PROVIDER=arn:aws:lambda:us-west-2:486652066693:capacity-provider:TestEC2 +CAPACITY_PROVIDER=arn:aws:lambda:us-west-2::capacity-provider: # Deploy using SAM sam deploy \ @@ -122,5 +122,5 @@ out.txt && cat out.txt && rm out.txt To remove all resources: ```bash -sam delete --stack-name swift-lambda-managed-instances --region ${REGION}$ +sam delete --stack-name swift-lambda-managed-instances --region ${REGION} ``` \ No newline at end of file diff --git a/Examples/ManagedInstances/template.yaml b/Examples/ManagedInstances/template.yaml index 2aa50dd01..ba1307c5a 100644 --- a/Examples/ManagedInstances/template.yaml +++ b/Examples/ManagedInstances/template.yaml @@ -8,7 +8,7 @@ Description: SAM Template for Lambda Managed Instances Example Parameters: CapacityProviderArn: Type: String - Default: arn:aws:lambda:us-west-2:486652066693:capacity-provider:TestEC2 # TODO : CHANGE ME! + 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: diff --git a/Sources/AWSLambdaRuntime/ManagedRuntime/LambdaManagedRuntime.swift b/Sources/AWSLambdaRuntime/ManagedRuntime/LambdaManagedRuntime.swift index 59135122a..257e117ab 100644 --- a/Sources/AWSLambdaRuntime/ManagedRuntime/LambdaManagedRuntime.swift +++ b/Sources/AWSLambdaRuntime/ManagedRuntime/LambdaManagedRuntime.swift @@ -65,7 +65,7 @@ public final class LambdaManagedRuntime: Sendable where Handler: Stream /// 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. - /// Thus function make sure only one run() is called at a time + /// 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 diff --git a/Sources/AWSLambdaRuntime/Runtime/LambdaRuntime.swift b/Sources/AWSLambdaRuntime/Runtime/LambdaRuntime.swift index 67d7f0177..63bb1c95f 100644 --- a/Sources/AWSLambdaRuntime/Runtime/LambdaRuntime.swift +++ b/Sources/AWSLambdaRuntime/Runtime/LambdaRuntime.swift @@ -61,7 +61,7 @@ public final class LambdaRuntime: Sendable where Handler: StreamingLamb /// 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. - /// Thus function make sure only one run() is called at a time + /// 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 From 08032dee21890f45fde780307b7743f6a26daebf Mon Sep 17 00:00:00 2001 From: Sebastien Stormacq Date: Sun, 4 Jan 2026 17:35:36 -0500 Subject: [PATCH 13/21] add lambda managed instances section in the readme --- readme.md | 74 +++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 74 insertions(+) diff --git a/readme.md b/readme.md index d543bc733..d24f60550 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/). From d13cac04490b50dbe565f7957156e9fcfec30dab Mon Sep 17 00:00:00 2001 From: Sebastien Stormacq Date: Tue, 13 Jan 2026 11:18:55 +0100 Subject: [PATCH 14/21] rever github action --- .github/workflows/pull_request.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/pull_request.yml b/.github/workflows/pull_request.yml index 5c17f54e2..7964fef3b 100644 --- a/.github/workflows/pull_request.yml +++ b/.github/workflows/pull_request.yml @@ -40,7 +40,7 @@ jobs: # We pass the list of examples here, but we can't pass an array as argument # Instead, we pass a String with a valid JSON array. # The workaround is mentioned here https://github.com/orgs/community/discussions/11692 - examples: "[ 'APIGatewayV1', 'APIGatewayV2', 'APIGatewayV2+LambdaAuthorizer', 'BackgroundTasks', 'HelloJSON', 'HelloWorld', 'HelloWorldNoTraits', 'HummingbirdLambda', 'ManagedInstances', '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', '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 From a6c217e63c0ac916058090b588fa66bf349c8231 Mon Sep 17 00:00:00 2001 From: Sebastien Stormacq Date: Tue, 13 Jan 2026 11:50:51 +0100 Subject: [PATCH 15/21] revert line breaks on LambdaRuntime+JSON --- .../FoundationSupport/LambdaRuntime+JSON.swift | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/Sources/AWSLambdaRuntime/FoundationSupport/LambdaRuntime+JSON.swift b/Sources/AWSLambdaRuntime/FoundationSupport/LambdaRuntime+JSON.swift index 1c05f029e..646e77f93 100644 --- a/Sources/AWSLambdaRuntime/FoundationSupport/LambdaRuntime+JSON.swift +++ b/Sources/AWSLambdaRuntime/FoundationSupport/LambdaRuntime+JSON.swift @@ -125,7 +125,6 @@ extension LambdaResponseStreamWriter { try await self.writeStatusAndHeaders(response, encoder: LambdaJSONOutputEncoder(encoder)) } } - @available(LambdaSwift 2.0, *) extension LambdaRuntime { /// Initialize an instance with a `LambdaHandler` defined in the form of a closure **with a non-`Void` return type**. @@ -245,4 +244,4 @@ extension LambdaRuntime { self.init(handler: handler, logger: logger) } } -#endif // trait: FoundationJSONSupport \ No newline at end of file +#endif // trait: FoundationJSONSupport From 2459313cd202a43d486b29bd2d96a5dc6d82960a Mon Sep 17 00:00:00 2001 From: Sebastien Stormacq Date: Tue, 13 Jan 2026 12:08:23 +0100 Subject: [PATCH 16/21] remove convenience code --- .../LambdaManagedRuntime+JSON.swift | 232 ------------------ .../LambdaManagedRuntime+Codable.swift | 111 --------- .../LambdaManagedRuntimeHandlers.swift | 47 ---- .../LambdaManagedRuntimeTests.swift | 68 ++--- 4 files changed, 34 insertions(+), 424 deletions(-) delete mode 100644 Sources/AWSLambdaRuntime/FoundationSupport/LambdaManagedRuntime+JSON.swift delete mode 100644 Sources/AWSLambdaRuntime/ManagedRuntime/LambdaManagedRuntime+Codable.swift delete mode 100644 Sources/AWSLambdaRuntime/ManagedRuntime/LambdaManagedRuntimeHandlers.swift diff --git a/Sources/AWSLambdaRuntime/FoundationSupport/LambdaManagedRuntime+JSON.swift b/Sources/AWSLambdaRuntime/FoundationSupport/LambdaManagedRuntime+JSON.swift deleted file mode 100644 index 261594670..000000000 --- a/Sources/AWSLambdaRuntime/FoundationSupport/LambdaManagedRuntime+JSON.swift +++ /dev/null @@ -1,232 +0,0 @@ -//===----------------------------------------------------------------------===// -// -// 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 - -public struct LambdaJSONEventDecoderSendable: LambdaEventDecoder & Sendable { - @usableFromInline let jsonDecoder: JSONDecoder - - @inlinable - public init(_ jsonDecoder: JSONDecoder) { - self.jsonDecoder = jsonDecoder - } - - @inlinable - public func decode(_ type: Event.Type, from buffer: NIOCore.ByteBuffer) throws -> Event - where Event: Decodable { - try buffer.getJSONDecodable( - Event.self, - decoder: self.jsonDecoder, - at: buffer.readerIndex, - length: buffer.readableBytes - )! // must work, enough readable bytes - } -} - -public struct LambdaJSONOutputEncoderSendable: LambdaOutputEncoder & Sendable { - @usableFromInline let jsonEncoder: JSONEncoder - - @inlinable - public init(_ jsonEncoder: JSONEncoder) { - self.jsonEncoder = jsonEncoder - } - - @inlinable - public func encode(_ value: Output, into buffer: inout ByteBuffer) throws { - try buffer.writeJSONEncodable(value, encoder: self.jsonEncoder) - } -} - -@available(LambdaSwift 2.0, *) -extension LambdaCodableAdapterSendable { - /// Initializes an instance given an encoder, decoder, and a handler with a non-`Void` output. - /// - Parameters: - /// - encoder: The encoder object that will be used to encode the generic `Output` obtained from the `handler`'s `outputWriter` into a `ByteBuffer`. By default, a JSONEncoder is used. - /// - decoder: The decoder object that will be used to decode the received `ByteBuffer` event into the generic `Event` type served to the `handler`. By default, a JSONDecoder is used. - /// - handler: The handler object. - public init( - encoder: JSONEncoder = JSONEncoder(), - decoder: JSONDecoder = JSONDecoder(), - handler: Handler - ) - where - Output: Encodable, - Output == Handler.Output, - Encoder == LambdaJSONOutputEncoderSendable, - Decoder == LambdaJSONEventDecoderSendable - { - self.init( - encoder: LambdaJSONOutputEncoderSendable(encoder), - decoder: LambdaJSONEventDecoderSendable(decoder), - handler: handler - ) - } - - /// Initializes an instance given a decoder, and a handler with a `Void` output. - /// - Parameters: - /// - decoder: The decoder object that will be used to decode the received `ByteBuffer` event into the generic `Event` type served to the `handler`. By default, a JSONDecoder is used. - /// - handler: The handler object. - public init( - decoder: JSONDecoder = JSONDecoder(), - handler: sending Handler - ) - where - Output == Void, - Handler.Output == Void, - Decoder == LambdaJSONEventDecoderSendable, - Encoder == VoidEncoder - { - self.init( - decoder: LambdaJSONEventDecoderSendable(decoder), - handler: handler - ) - } -} - -@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( - decoder: JSONDecoder = JSONDecoder(), - encoder: JSONEncoder = JSONEncoder(), - logger: Logger = Logger(label: "LambdaManagedRuntime"), - body: @Sendable @escaping (Event, LambdaContext) async throws -> Output - ) - where - Handler == LambdaCodableAdapterSendable< - LambdaHandlerAdapterSendable>, - Event, - Output, - LambdaJSONEventDecoderSendable, - LambdaJSONOutputEncoderSendable - > - { - let handler = LambdaCodableAdapterSendable( - encoder: encoder, - decoder: decoder, - handler: LambdaHandlerAdapterSendable(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( - decoder: JSONDecoder = JSONDecoder(), - logger: Logger = Logger(label: "LambdaRuntime"), - body: @Sendable @escaping (Event, LambdaContext) async throws -> Void - ) - where - Handler == LambdaCodableAdapterSendable< - LambdaHandlerAdapterSendable>, - Event, - Void, - LambdaJSONEventDecoderSendable, - VoidEncoder - > - { - let handler = LambdaCodableAdapterSendable( - decoder: LambdaJSONEventDecoderSendable(decoder), - handler: LambdaHandlerAdapterSendable(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( - decoder: JSONDecoder = JSONDecoder(), - logger: Logger = Logger(label: "LambdaRuntime"), - lambdaHandler: LHandler - ) - where - Handler == LambdaCodableAdapterSendable< - LambdaHandlerAdapterSendable, - Event, - Void, - LambdaJSONEventDecoderSendable, - VoidEncoder - >, - LHandler.Event == Event, - LHandler.Output == Void - { - let handler = LambdaCodableAdapterSendable( - decoder: LambdaJSONEventDecoderSendable(decoder), - handler: LambdaHandlerAdapterSendable(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( - decoder: JSONDecoder = JSONDecoder(), - encoder: JSONEncoder = JSONEncoder(), - logger: Logger = Logger(label: "LambdaRuntime"), - lambdaHandler: LHandler - ) - where - Handler == LambdaCodableAdapterSendable< - LambdaHandlerAdapterSendable, - Event, - Output, - LambdaJSONEventDecoderSendable, - LambdaJSONOutputEncoderSendable - >, - LHandler.Event == Event, - LHandler.Output == Output - { - let handler = LambdaCodableAdapterSendable( - encoder: encoder, - decoder: decoder, - handler: LambdaHandlerAdapterSendable(handler: lambdaHandler) - ) - - self.init(handler: handler, logger: logger) - } -} -#endif // trait: FoundationJSONSupport - -#endif // trait: ManagedRuntimeSupport diff --git a/Sources/AWSLambdaRuntime/ManagedRuntime/LambdaManagedRuntime+Codable.swift b/Sources/AWSLambdaRuntime/ManagedRuntime/LambdaManagedRuntime+Codable.swift deleted file mode 100644 index 28d3115b6..000000000 --- a/Sources/AWSLambdaRuntime/ManagedRuntime/LambdaManagedRuntime+Codable.swift +++ /dev/null @@ -1,111 +0,0 @@ -//===----------------------------------------------------------------------===// -// -// This source file is part of the SwiftAWSLambdaRuntime open source project -// -// Copyright SwiftAWSLambdaRuntime project authors -// Copyright (c) Amazon.com, Inc. or its affiliates. -// Licensed under Apache License v2.0 -// -// See LICENSE.txt for license information -// See CONTRIBUTORS.txt for the list of SwiftAWSLambdaRuntime project authors -// -// SPDX-License-Identifier: Apache-2.0 -// -//===----------------------------------------------------------------------===// - -#if ManagedRuntimeSupport - -import NIOCore - -/// Adapts a ``LambdaHandler`` conforming handler to conform to ``LambdaWithBackgroundProcessingHandler``. -@available(LambdaSwift 2.0, *) -public struct LambdaHandlerAdapterSendable< - Event: Decodable, - Output, - Handler: LambdaHandler & Sendable ->: Sendable, LambdaWithBackgroundProcessingHandler where Handler.Event == Event, Handler.Output == Output { - @usableFromInline let handler: Handler - - /// Initializes an instance given a concrete handler. - /// - Parameter handler: The ``LambdaHandler`` conforming handler that is to be adapted to ``LambdaWithBackgroundProcessingHandler``. - @inlinable - public init(handler: sending Handler) { - self.handler = handler - } - - /// Passes the generic `Event` object to the ``LambdaHandler/handle(_:context:)`` function, and - /// the resulting output is then written to ``LambdaWithBackgroundProcessingHandler``'s `outputWriter`. - /// - Parameters: - /// - event: The received event. - /// - outputWriter: The writer to write the computed response to. - /// - context: The ``LambdaContext`` containing the invocation's metadata. - @inlinable - public func handle( - _ event: Event, - outputWriter: some LambdaResponseWriter, - context: LambdaContext - ) async throws { - let output = try await self.handler.handle(event, context: context) - try await outputWriter.write(output) - } -} - -/// Adapts a ``LambdaWithBackgroundProcessingHandler`` conforming handler to conform to ``StreamingLambdaHandler``. -@available(LambdaSwift 2.0, *) -public struct LambdaCodableAdapterSendable< - Handler: LambdaWithBackgroundProcessingHandler & Sendable, - Event: Decodable, - Output, - Decoder: LambdaEventDecoder & Sendable, - Encoder: LambdaOutputEncoder & Sendable ->: Sendable, StreamingLambdaHandler where Handler.Event == Event, Handler.Output == Output, Encoder.Output == Output { - @usableFromInline let handler: Handler - @usableFromInline let encoder: Encoder - @usableFromInline let decoder: Decoder - @usableFromInline var byteBuffer: ByteBuffer = .init() - - /// Initializes an instance given an encoder, decoder, and a handler with a non-`Void` output. - /// - Parameters: - /// - encoder: The encoder object that will be used to encode the generic `Output` obtained from the `handler`'s `outputWriter` into a `ByteBuffer`. - /// - decoder: The decoder object that will be used to decode the received `ByteBuffer` event into the generic `Event` type served to the `handler`. - /// - handler: The handler object. - @inlinable - public init(encoder: Encoder, decoder: Decoder, handler: Handler) where Output: Encodable { - self.encoder = encoder - self.decoder = decoder - self.handler = handler - } - - /// Initializes an instance given a decoder, and a handler with a `Void` output. - /// - Parameters: - /// - decoder: The decoder object that will be used to decode the received `ByteBuffer` event into the generic `Event` type served to the `handler`. - /// - handler: The handler object. - @inlinable - public init(decoder: Decoder, handler: Handler) where Output == Void, Encoder == VoidEncoder { - self.encoder = VoidEncoder() - self.decoder = decoder - self.handler = handler - } - - /// A ``StreamingLambdaHandler/handle(_:responseWriter:context:)`` wrapper. - /// - Parameters: - /// - request: The received event. - /// - responseWriter: The writer to write the computed response to. - /// - context: The ``LambdaContext`` containing the invocation's metadata. - @inlinable - public mutating func handle( - _ request: ByteBuffer, - responseWriter: Writer, - context: LambdaContext - ) async throws { - let event = try self.decoder.decode(Event.self, from: request) - - let writer = LambdaCodableResponseWriter( - encoder: self.encoder, - streamWriter: responseWriter - ) - try await self.handler.handle(event, outputWriter: writer, context: context) - } -} - -#endif diff --git a/Sources/AWSLambdaRuntime/ManagedRuntime/LambdaManagedRuntimeHandlers.swift b/Sources/AWSLambdaRuntime/ManagedRuntime/LambdaManagedRuntimeHandlers.swift deleted file mode 100644 index ec4b493c8..000000000 --- a/Sources/AWSLambdaRuntime/ManagedRuntime/LambdaManagedRuntimeHandlers.swift +++ /dev/null @@ -1,47 +0,0 @@ -//===----------------------------------------------------------------------===// -// -// 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: LambdaHandler, Sendable { - let body: @Sendable (Event, LambdaContext) async throws -> Output - - /// Initialize with a closure handler over generic `Input` and `Output` types. - /// - Parameter body: The handler function written as a closure. - public init(body: @Sendable @escaping (Event, LambdaContext) async throws -> Output) where Output: Encodable { - self.body = body - } - - /// Initialize with a closure handler over a generic `Input` type, and a `Void` `Output`. - /// - Parameter body: The handler function written as a closure. - public init(body: @Sendable @escaping (Event, LambdaContext) async throws -> Void) where Output == Void { - self.body = body - } - - /// Calls the provided `self.body` closure with the generic `Event` object representing the incoming event, and the ``LambdaContext`` - /// - Parameters: - /// - event: The generic `Event` object representing the invocation's input data. - /// - context: The ``LambdaContext`` containing the invocation's metadata. - public func handle(_ event: Event, context: LambdaContext) async throws -> Output { - try await self.body(event, context) - } -} -#endif diff --git a/Tests/AWSLambdaRuntimeTests/LambdaManagedRuntimeTests.swift b/Tests/AWSLambdaRuntimeTests/LambdaManagedRuntimeTests.swift index a948d4af0..effeefbc9 100644 --- a/Tests/AWSLambdaRuntimeTests/LambdaManagedRuntimeTests.swift +++ b/Tests/AWSLambdaRuntimeTests/LambdaManagedRuntimeTests.swift @@ -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(JSONEncoder()) - - let concurrentTasks = 10 - - let results = try await withThrowingTaskGroup(of: String.self) { group in - for i in 0..(JSONEncoder()) + + // let concurrentTasks = 10 + + // let results = try await withThrowingTaskGroup(of: String.self) { group in + // for i in 0.. Date: Tue, 13 Jan 2026 12:09:05 +0100 Subject: [PATCH 17/21] Remove Sendable on VoidEncoder --- Sources/AWSLambdaRuntime/Runtime/LambdaRuntime+Codable.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Sources/AWSLambdaRuntime/Runtime/LambdaRuntime+Codable.swift b/Sources/AWSLambdaRuntime/Runtime/LambdaRuntime+Codable.swift index 27a7c3ace..f20f5a476 100644 --- a/Sources/AWSLambdaRuntime/Runtime/LambdaRuntime+Codable.swift +++ b/Sources/AWSLambdaRuntime/Runtime/LambdaRuntime+Codable.swift @@ -39,7 +39,7 @@ public protocol LambdaOutputEncoder { func encode(_ value: Output, into buffer: inout ByteBuffer) throws } -public struct VoidEncoder: LambdaOutputEncoder, Sendable { +public struct VoidEncoder: LambdaOutputEncoder { public typealias Output = Void public init() {} From c69c103da868fdedaf24480bff13265e1ea29d9e Mon Sep 17 00:00:00 2001 From: Sebastien Stormacq Date: Tue, 13 Jan 2026 12:09:59 +0100 Subject: [PATCH 18/21] make static lambda context for testing, public --- Tests/AWSLambdaRuntimeTests/Utils.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Tests/AWSLambdaRuntimeTests/Utils.swift b/Tests/AWSLambdaRuntimeTests/Utils.swift index a0002247c..16078f6c7 100644 --- a/Tests/AWSLambdaRuntimeTests/Utils.swift +++ b/Tests/AWSLambdaRuntimeTests/Utils.swift @@ -32,7 +32,7 @@ extension Date { @available(LambdaSwift 2.0, *) extension LambdaContext { - static func makeTest() -> LambdaContext { + public static func makeTest() -> LambdaContext { LambdaContext.__forTestsOnly( requestID: "test-request-id", traceID: "test-trace-id", From 51caf806dab90950f49efc159d85df93a44308bb Mon Sep 17 00:00:00 2001 From: Sebastien Stormacq Date: Tue, 13 Jan 2026 13:28:56 +0100 Subject: [PATCH 19/21] add convenience methods --- .../LambdaManagedRuntime+JSON.swift | 198 ++++++++++++++++++ .../LambdaRuntime+JSON.swift | 4 +- .../LambdaManagedRuntime+Codable.swift | 111 ++++++++++ .../LambdaManagedRuntimeHandlers.swift | 47 +++++ .../Runtime/LambdaRuntime+Codable.swift | 2 +- 5 files changed, 359 insertions(+), 3 deletions(-) create mode 100644 Sources/AWSLambdaRuntime/FoundationSupport/LambdaManagedRuntime+JSON.swift create mode 100644 Sources/AWSLambdaRuntime/ManagedRuntime/LambdaManagedRuntime+Codable.swift create mode 100644 Sources/AWSLambdaRuntime/ManagedRuntime/LambdaManagedRuntimeHandlers.swift diff --git a/Sources/AWSLambdaRuntime/FoundationSupport/LambdaManagedRuntime+JSON.swift b/Sources/AWSLambdaRuntime/FoundationSupport/LambdaManagedRuntime+JSON.swift new file mode 100644 index 000000000..83d78a0f2 --- /dev/null +++ b/Sources/AWSLambdaRuntime/FoundationSupport/LambdaManagedRuntime+JSON.swift @@ -0,0 +1,198 @@ +//===----------------------------------------------------------------------===// +// +// 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 LambdaCodableAdapterSendable { + /// Initializes an instance given an encoder, decoder, and a handler with a non-`Void` output. + /// - Parameters: + /// - encoder: The encoder object that will be used to encode the generic `Output` obtained from the `handler`'s `outputWriter` into a `ByteBuffer`. By default, a JSONEncoder is used. + /// - decoder: The decoder object that will be used to decode the received `ByteBuffer` event into the generic `Event` type served to the `handler`. By default, a JSONDecoder is used. + /// - handler: The handler object. + public init( + encoder: JSONEncoder = JSONEncoder(), + decoder: JSONDecoder = JSONDecoder(), + handler: Handler + ) + where + Output: Encodable, + Output == Handler.Output, + Encoder == LambdaJSONOutputEncoder, + Decoder == LambdaJSONEventDecoder + { + self.init( + encoder: LambdaJSONOutputEncoder(encoder), + decoder: LambdaJSONEventDecoder(decoder), + handler: handler + ) + } + + /// Initializes an instance given a decoder, and a handler with a `Void` output. + /// - Parameters: + /// - decoder: The decoder object that will be used to decode the received `ByteBuffer` event into the generic `Event` type served to the `handler`. By default, a JSONDecoder is used. + /// - handler: The handler object. + public init( + decoder: JSONDecoder = JSONDecoder(), + handler: sending Handler + ) + where + Output == Void, + Handler.Output == Void, + Decoder == LambdaJSONEventDecoder, + Encoder == VoidEncoder + { + self.init( + decoder: LambdaJSONEventDecoder(decoder), + handler: handler + ) + } +} + +@available(LambdaSwift 2.0, *) +extension 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( + decoder: JSONDecoder = JSONDecoder(), + encoder: JSONEncoder = JSONEncoder(), + logger: Logger = Logger(label: "LambdaManagedRuntime"), + body: @Sendable @escaping (Event, LambdaContext) async throws -> Output + ) + where + Handler == LambdaCodableAdapterSendable< + LambdaHandlerAdapterSendable>, + Event, + Output, + LambdaJSONEventDecoder, + LambdaJSONOutputEncoder + > + { + let handler = LambdaCodableAdapterSendable( + encoder: encoder, + decoder: decoder, + handler: LambdaHandlerAdapterSendable(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( + decoder: JSONDecoder = JSONDecoder(), + logger: Logger = Logger(label: "LambdaRuntime"), + body: @Sendable @escaping (Event, LambdaContext) async throws -> Void + ) + where + Handler == LambdaCodableAdapterSendable< + LambdaHandlerAdapterSendable>, + Event, + Void, + LambdaJSONEventDecoder, + VoidEncoder + > + { + let handler = LambdaCodableAdapterSendable( + decoder: LambdaJSONEventDecoder(decoder), + handler: LambdaHandlerAdapterSendable(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( + decoder: JSONDecoder = JSONDecoder(), + logger: Logger = Logger(label: "LambdaRuntime"), + lambdaHandler: LHandler + ) + where + Handler == LambdaCodableAdapterSendable< + LambdaHandlerAdapterSendable, + Event, + Void, + LambdaJSONEventDecoder, + VoidEncoder + >, + LHandler.Event == Event, + LHandler.Output == Void + { + let handler = LambdaCodableAdapterSendable( + decoder: LambdaJSONEventDecoder(decoder), + handler: LambdaHandlerAdapterSendable(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( + decoder: JSONDecoder = JSONDecoder(), + encoder: JSONEncoder = JSONEncoder(), + logger: Logger = Logger(label: "LambdaRuntime"), + lambdaHandler: LHandler + ) + where + Handler == LambdaCodableAdapterSendable< + LambdaHandlerAdapterSendable, + Event, + Output, + LambdaJSONEventDecoder, + LambdaJSONOutputEncoder + >, + LHandler.Event == Event, + LHandler.Output == Output + { + let handler = LambdaCodableAdapterSendable( + encoder: encoder, + decoder: decoder, + handler: LambdaHandlerAdapterSendable(handler: lambdaHandler) + ) + + self.init(handler: handler, logger: logger) + } +} +#endif // trait: FoundationJSONSupport + +#endif // trait: ManagedRuntimeSupport diff --git a/Sources/AWSLambdaRuntime/FoundationSupport/LambdaRuntime+JSON.swift b/Sources/AWSLambdaRuntime/FoundationSupport/LambdaRuntime+JSON.swift index 646e77f93..4a02bc51c 100644 --- a/Sources/AWSLambdaRuntime/FoundationSupport/LambdaRuntime+JSON.swift +++ b/Sources/AWSLambdaRuntime/FoundationSupport/LambdaRuntime+JSON.swift @@ -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: LambdaOutputEncoder { +public struct LambdaJSONOutputEncoder: LambdaOutputEncoder, Sendable { @usableFromInline let jsonEncoder: JSONEncoder @inlinable diff --git a/Sources/AWSLambdaRuntime/ManagedRuntime/LambdaManagedRuntime+Codable.swift b/Sources/AWSLambdaRuntime/ManagedRuntime/LambdaManagedRuntime+Codable.swift new file mode 100644 index 000000000..28d3115b6 --- /dev/null +++ b/Sources/AWSLambdaRuntime/ManagedRuntime/LambdaManagedRuntime+Codable.swift @@ -0,0 +1,111 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the SwiftAWSLambdaRuntime open source project +// +// Copyright SwiftAWSLambdaRuntime project authors +// Copyright (c) Amazon.com, Inc. or its affiliates. +// Licensed under Apache License v2.0 +// +// See LICENSE.txt for license information +// See CONTRIBUTORS.txt for the list of SwiftAWSLambdaRuntime project authors +// +// SPDX-License-Identifier: Apache-2.0 +// +//===----------------------------------------------------------------------===// + +#if ManagedRuntimeSupport + +import NIOCore + +/// Adapts a ``LambdaHandler`` conforming handler to conform to ``LambdaWithBackgroundProcessingHandler``. +@available(LambdaSwift 2.0, *) +public struct LambdaHandlerAdapterSendable< + Event: Decodable, + Output, + Handler: LambdaHandler & Sendable +>: Sendable, LambdaWithBackgroundProcessingHandler where Handler.Event == Event, Handler.Output == Output { + @usableFromInline let handler: Handler + + /// Initializes an instance given a concrete handler. + /// - Parameter handler: The ``LambdaHandler`` conforming handler that is to be adapted to ``LambdaWithBackgroundProcessingHandler``. + @inlinable + public init(handler: sending Handler) { + self.handler = handler + } + + /// Passes the generic `Event` object to the ``LambdaHandler/handle(_:context:)`` function, and + /// the resulting output is then written to ``LambdaWithBackgroundProcessingHandler``'s `outputWriter`. + /// - Parameters: + /// - event: The received event. + /// - outputWriter: The writer to write the computed response to. + /// - context: The ``LambdaContext`` containing the invocation's metadata. + @inlinable + public func handle( + _ event: Event, + outputWriter: some LambdaResponseWriter, + context: LambdaContext + ) async throws { + let output = try await self.handler.handle(event, context: context) + try await outputWriter.write(output) + } +} + +/// Adapts a ``LambdaWithBackgroundProcessingHandler`` conforming handler to conform to ``StreamingLambdaHandler``. +@available(LambdaSwift 2.0, *) +public struct LambdaCodableAdapterSendable< + Handler: LambdaWithBackgroundProcessingHandler & Sendable, + Event: Decodable, + Output, + Decoder: LambdaEventDecoder & Sendable, + Encoder: LambdaOutputEncoder & Sendable +>: Sendable, StreamingLambdaHandler where Handler.Event == Event, Handler.Output == Output, Encoder.Output == Output { + @usableFromInline let handler: Handler + @usableFromInline let encoder: Encoder + @usableFromInline let decoder: Decoder + @usableFromInline var byteBuffer: ByteBuffer = .init() + + /// Initializes an instance given an encoder, decoder, and a handler with a non-`Void` output. + /// - Parameters: + /// - encoder: The encoder object that will be used to encode the generic `Output` obtained from the `handler`'s `outputWriter` into a `ByteBuffer`. + /// - decoder: The decoder object that will be used to decode the received `ByteBuffer` event into the generic `Event` type served to the `handler`. + /// - handler: The handler object. + @inlinable + public init(encoder: Encoder, decoder: Decoder, handler: Handler) where Output: Encodable { + self.encoder = encoder + self.decoder = decoder + self.handler = handler + } + + /// Initializes an instance given a decoder, and a handler with a `Void` output. + /// - Parameters: + /// - decoder: The decoder object that will be used to decode the received `ByteBuffer` event into the generic `Event` type served to the `handler`. + /// - handler: The handler object. + @inlinable + public init(decoder: Decoder, handler: Handler) where Output == Void, Encoder == VoidEncoder { + self.encoder = VoidEncoder() + self.decoder = decoder + self.handler = handler + } + + /// A ``StreamingLambdaHandler/handle(_:responseWriter:context:)`` wrapper. + /// - Parameters: + /// - request: The received event. + /// - responseWriter: The writer to write the computed response to. + /// - context: The ``LambdaContext`` containing the invocation's metadata. + @inlinable + public mutating func handle( + _ request: ByteBuffer, + responseWriter: Writer, + context: LambdaContext + ) async throws { + let event = try self.decoder.decode(Event.self, from: request) + + let writer = LambdaCodableResponseWriter( + encoder: self.encoder, + streamWriter: responseWriter + ) + try await self.handler.handle(event, outputWriter: writer, context: context) + } +} + +#endif diff --git a/Sources/AWSLambdaRuntime/ManagedRuntime/LambdaManagedRuntimeHandlers.swift b/Sources/AWSLambdaRuntime/ManagedRuntime/LambdaManagedRuntimeHandlers.swift new file mode 100644 index 000000000..ec4b493c8 --- /dev/null +++ b/Sources/AWSLambdaRuntime/ManagedRuntime/LambdaManagedRuntimeHandlers.swift @@ -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: LambdaHandler, Sendable { + let body: @Sendable (Event, LambdaContext) async throws -> Output + + /// Initialize with a closure handler over generic `Input` and `Output` types. + /// - Parameter body: The handler function written as a closure. + public init(body: @Sendable @escaping (Event, LambdaContext) async throws -> Output) where Output: Encodable { + self.body = body + } + + /// Initialize with a closure handler over a generic `Input` type, and a `Void` `Output`. + /// - Parameter body: The handler function written as a closure. + public init(body: @Sendable @escaping (Event, LambdaContext) async throws -> Void) where Output == Void { + self.body = body + } + + /// Calls the provided `self.body` closure with the generic `Event` object representing the incoming event, and the ``LambdaContext`` + /// - Parameters: + /// - event: The generic `Event` object representing the invocation's input data. + /// - context: The ``LambdaContext`` containing the invocation's metadata. + public func handle(_ event: Event, context: LambdaContext) async throws -> Output { + try await self.body(event, context) + } +} +#endif diff --git a/Sources/AWSLambdaRuntime/Runtime/LambdaRuntime+Codable.swift b/Sources/AWSLambdaRuntime/Runtime/LambdaRuntime+Codable.swift index f20f5a476..27a7c3ace 100644 --- a/Sources/AWSLambdaRuntime/Runtime/LambdaRuntime+Codable.swift +++ b/Sources/AWSLambdaRuntime/Runtime/LambdaRuntime+Codable.swift @@ -39,7 +39,7 @@ public protocol LambdaOutputEncoder { func encode(_ value: Output, into buffer: inout ByteBuffer) throws } -public struct VoidEncoder: LambdaOutputEncoder { +public struct VoidEncoder: LambdaOutputEncoder, Sendable { public typealias Output = Void public init() {} From d6404fc789ee69ccc5bca1bb82d1b16e48f86de9 Mon Sep 17 00:00:00 2001 From: Sebastien Stormacq Date: Tue, 13 Jan 2026 13:29:05 +0100 Subject: [PATCH 20/21] add example --- Examples/ManagedInstances/.gitignore | 3 + .../BackgroundTasks/main.swift | 60 +++++++++ .../ManagedInstances/HelloJSON/main.swift | 41 ++++++ Examples/ManagedInstances/Package.swift | 46 +++++++ Examples/ManagedInstances/README.md | 126 ++++++++++++++++++ .../Sources/BackgroundTasks/main.swift | 60 +++++++++ .../Sources/HelloJSON/main.swift | 41 ++++++ .../Sources/Streaming/main.swift | 69 ++++++++++ .../ManagedInstances/Streaming/main.swift | 69 ++++++++++ Examples/ManagedInstances/template.yaml | 81 +++++++++++ 10 files changed, 596 insertions(+) create mode 100644 Examples/ManagedInstances/.gitignore create mode 100644 Examples/ManagedInstances/BackgroundTasks/main.swift create mode 100644 Examples/ManagedInstances/HelloJSON/main.swift create mode 100644 Examples/ManagedInstances/Package.swift create mode 100644 Examples/ManagedInstances/README.md create mode 100644 Examples/ManagedInstances/Sources/BackgroundTasks/main.swift create mode 100644 Examples/ManagedInstances/Sources/HelloJSON/main.swift create mode 100644 Examples/ManagedInstances/Sources/Streaming/main.swift create mode 100644 Examples/ManagedInstances/Streaming/main.swift create mode 100644 Examples/ManagedInstances/template.yaml diff --git a/Examples/ManagedInstances/.gitignore b/Examples/ManagedInstances/.gitignore new file mode 100644 index 000000000..a03a102dd --- /dev/null +++ b/Examples/ManagedInstances/.gitignore @@ -0,0 +1,3 @@ +response.json +samconfig.toml +Makefile diff --git a/Examples/ManagedInstances/BackgroundTasks/main.swift b/Examples/ManagedInstances/BackgroundTasks/main.swift new file mode 100644 index 000000000..f9fc81edc --- /dev/null +++ b/Examples/ManagedInstances/BackgroundTasks/main.swift @@ -0,0 +1,60 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the SwiftAWSLambdaRuntime open source project +// +// Copyright SwiftAWSLambdaRuntime project authors +// Copyright (c) Amazon.com, Inc. or its affiliates. +// Licensed under Apache License v2.0 +// +// See LICENSE.txt for license information +// See CONTRIBUTORS.txt for the list of SwiftAWSLambdaRuntime project authors +// +// SPDX-License-Identifier: Apache-2.0 +// +//===----------------------------------------------------------------------===// + +import AWSLambdaRuntime + +#if canImport(FoundationEssentials) +import FoundationEssentials +#else +import Foundation +#endif + +// for a simple struct as this one, the compiler automatically infers Sendable +// With Lambda Managed Instances, your handler struct MUST be Sendable +struct BackgroundProcessingHandler: LambdaWithBackgroundProcessingHandler, Sendable { + struct Input: Decodable { + let message: String + } + + struct Greeting: Encodable { + let echoedMessage: String + } + + typealias Event = Input + typealias Output = Greeting + + func handle( + _ event: Event, + outputWriter: some LambdaResponseWriter, + context: LambdaContext + ) async throws { + // Return result to the Lambda control plane + context.logger.debug("BackgroundProcessingHandler - message received") + try await outputWriter.write(Greeting(echoedMessage: event.message)) + + // Perform some background work, e.g: + context.logger.debug("BackgroundProcessingHandler - response sent. Performing background tasks.") + try await Task.sleep(for: .seconds(10)) + + // Exit the function. All asynchronous work has been executed before exiting the scope of this function. + // Follows structured concurrency principles. + context.logger.debug("BackgroundProcessingHandler - Background tasks completed. Returning") + return + } +} + +let adapter = LambdaCodableAdapterSendable(handler: BackgroundProcessingHandler()) +let runtime = LambdaManagedRuntime(handler: adapter) +try await runtime.run() diff --git a/Examples/ManagedInstances/HelloJSON/main.swift b/Examples/ManagedInstances/HelloJSON/main.swift new file mode 100644 index 000000000..f166c02f5 --- /dev/null +++ b/Examples/ManagedInstances/HelloJSON/main.swift @@ -0,0 +1,41 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the SwiftAWSLambdaRuntime open source project +// +// Copyright SwiftAWSLambdaRuntime project authors +// Copyright (c) Amazon.com, Inc. or its affiliates. +// Licensed under Apache License v2.0 +// +// See LICENSE.txt for license information +// See CONTRIBUTORS.txt for the list of SwiftAWSLambdaRuntime project authors +// +// SPDX-License-Identifier: Apache-2.0 +// +//===----------------------------------------------------------------------===// + +import AWSLambdaRuntime + +// in this example we are receiving and responding with JSON structures + +// the data structure to represent the input parameter +struct HelloRequest: Decodable { + let name: String + let age: Int +} + +// the data structure to represent the output response +struct HelloResponse: Encodable { + let greetings: String +} + +// the Lambda runtime +let runtime = LambdaManagedRuntime { + (event: HelloRequest, context: LambdaContext) in + + HelloResponse( + greetings: "Hello \(event.name). You look \(event.age > 30 ? "younger" : "older") than your age." + ) +} + +// start the loop +try await runtime.run() diff --git a/Examples/ManagedInstances/Package.swift b/Examples/ManagedInstances/Package.swift new file mode 100644 index 000000000..2868d5bbf --- /dev/null +++ b/Examples/ManagedInstances/Package.swift @@ -0,0 +1,46 @@ +// swift-tools-version:6.2 + +import PackageDescription + +let package = Package( + name: "swift-aws-lambda-runtime-example", + platforms: [.macOS(.v15)], + products: [ + .executable(name: "HelloJSON", targets: ["HelloJSON"]), + .executable(name: "Streaming", targets: ["Streaming"]), + .executable(name: "BackgroundTasks", targets: ["BackgroundTasks"]), + ], + dependencies: [ + // For local development (default) + .package(name: "swift-aws-lambda-runtime", path: "../.."), + + // For standalone usage, comment the line above and uncomment below: + // .package(url: "https://github.com/awslabs/swift-aws-lambda-runtime.git", from: "2.0.0"), + + .package(url: "https://github.com/awslabs/swift-aws-lambda-events.git", from: "1.0.0"), + ], + targets: [ + .executableTarget( + name: "HelloJSON", + dependencies: [ + .product(name: "AWSLambdaRuntime", package: "swift-aws-lambda-runtime") + ], + path: "Sources/HelloJSON" + ), + .executableTarget( + name: "Streaming", + dependencies: [ + .product(name: "AWSLambdaRuntime", package: "swift-aws-lambda-runtime"), + .product(name: "AWSLambdaEvents", package: "swift-aws-lambda-events"), + ], + path: "Sources/Streaming" + ), + .executableTarget( + name: "BackgroundTasks", + dependencies: [ + .product(name: "AWSLambdaRuntime", package: "swift-aws-lambda-runtime") + ], + path: "Sources/BackgroundTasks" + ), + ] +) diff --git a/Examples/ManagedInstances/README.md b/Examples/ManagedInstances/README.md new file mode 100644 index 000000000..7e8b0fdbe --- /dev/null +++ b/Examples/ManagedInstances/README.md @@ -0,0 +1,126 @@ +# Lambda Managed Instances Example + +This example demonstrates deploying Swift Lambda functions to Lambda Managed Instances using AWS SAM. Lambda Managed Instances provide serverless simplicity with EC2 flexibility and cost optimization by running your functions on customer-owned EC2 instances. + +## Functions Included + +1. **HelloJSON** - JSON input/output with structured data types +2. **Streaming** - Demonstrates response streaming capabilities +3. **BackgroundTasks** - Handles long-running background processing + +## Prerequisites + +- AWS CLI configured with appropriate permissions +- SAM CLI installed +- Swift 6.0+ installed +- An existing [Lambda Managed Instances capacity provider](https://docs.aws.amazon.com/lambda/latest/dg/lambda-managed-instances-capacity-providers.html) + +## Capacity Provider Configuration + +[Create your own capacity provider](https://docs.aws.amazon.com/lambda/latest/dg/lambda-managed-instances-capacity-providers.html#lambda-managed-instances-creating-capacity-provider) before deploying this example. + +This example uses a pre-configured capacity provider with the ARN: +``` +arn:aws:lambda:us-west-2:486652066693:capacity-provider:TestEC2 +``` + +## Deployment + +```bash +# Build the Swift packages +swift package archive --allow-network-access docker + +# Change the values below to match your setup +REGION=us-west-2 +CAPACITY_PROVIDER=arn:aws:lambda:us-west-2::capacity-provider: + +# Deploy using SAM +sam deploy \ + --resolve-s3 \ + --template-file template.yaml \ + --stack-name swift-lambda-managed-instances \ + --capabilities CAPABILITY_IAM \ + --region ${REGION} \ + --parameter-overrides \ + CapacityProviderArn=${CAPACITY_PROVIDER} +``` + +## Function Details + +### HelloJSON Function +- **Timeout**: 15 seconds (default) +- **Concurrency**: 8 per execution environment (default) +- **Input**: JSON `{"name": "string", "age": number}` +- **Output**: JSON `{"greetings": "string"}` + +### Streaming Function +- **Timeout**: 60 seconds +- **Concurrency**: 8 per execution environment (default) +- **Features**: Response streaming enabled +- **Output**: Streams numbers with pauses + +### BackgroundTasks Function +- **Timeout**: 300 seconds (5 minutes) +- **Concurrency**: 8 per execution environment (default) +- **Input**: JSON `{"message": "string"}` +- **Features**: Long-running background processing after response + +## Testing with AWS CLI + +After deployment, invoke each function with the AWS CLI: + +### Test HelloJSON Function +```bash +REGION=us-west-2 +aws lambda invoke \ +--region ${REGION} \ +--function-name swift-lambda-managed-instances-HelloJSON \ +--payload $(echo '{ "name" : "Swift Developer", "age" : 50 }' | base64) \ +out.txt && cat out.txt && rm out.txt + +# Expected output: {"greetings": "Hello Swift Developer. You look older than your age."} +``` + +### Test Streaming Function +```bash +# Get the Streaming URL +REGION=us-west-2 +STREAMING_URL=$(aws cloudformation describe-stacks \ + --stack-name swift-lambda-managed-instances \ + --region ${REGION} \ + --query 'Stacks[0].Outputs[?OutputKey==`StreamingFunctionUrl`].OutputValue' \ + --output text) + +# Set the AWS_ACCESS_KEY_ID, AWS_SECRET_ACCESS_KEY, and AWS_SESSION_TOKEN environment variables +eval $(aws configure export-credentials --format env) + +# Test with curl (streaming response) +curl "$STREAMING_URL" \ + --user "${AWS_ACCESS_KEY_ID}":"${AWS_SECRET_ACCESS_KEY}" \ + --aws-sigv4 "aws:amz:${REGION}:lambda" \ + -H "x-amz-security-token: ${AWS_SESSION_TOKEN}" \ + --no-buffer + +# Expected output: Numbers streaming with pauses +``` + +### Test BackgroundTasks Function +```bash +# Test with AWS CLI +REGION=us-west-2 +aws lambda invoke \ +--region ${REGION} \ +--function-name swift-lambda-managed-instances-BackgroundTasks \ +--payload $(echo '{ "message" : "Additional processing in the background" }' | base64) \ +out.txt && cat out.txt && rm out.txt + +# Expected output: {"echoedMessage": "Additional processing in the background"} +# Note: Background processing continues after response is sent +``` + +## Cleanup + +To remove all resources: +```bash +sam delete --stack-name swift-lambda-managed-instances --region ${REGION} +``` \ No newline at end of file diff --git a/Examples/ManagedInstances/Sources/BackgroundTasks/main.swift b/Examples/ManagedInstances/Sources/BackgroundTasks/main.swift new file mode 100644 index 000000000..f9fc81edc --- /dev/null +++ b/Examples/ManagedInstances/Sources/BackgroundTasks/main.swift @@ -0,0 +1,60 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the SwiftAWSLambdaRuntime open source project +// +// Copyright SwiftAWSLambdaRuntime project authors +// Copyright (c) Amazon.com, Inc. or its affiliates. +// Licensed under Apache License v2.0 +// +// See LICENSE.txt for license information +// See CONTRIBUTORS.txt for the list of SwiftAWSLambdaRuntime project authors +// +// SPDX-License-Identifier: Apache-2.0 +// +//===----------------------------------------------------------------------===// + +import AWSLambdaRuntime + +#if canImport(FoundationEssentials) +import FoundationEssentials +#else +import Foundation +#endif + +// for a simple struct as this one, the compiler automatically infers Sendable +// With Lambda Managed Instances, your handler struct MUST be Sendable +struct BackgroundProcessingHandler: LambdaWithBackgroundProcessingHandler, Sendable { + struct Input: Decodable { + let message: String + } + + struct Greeting: Encodable { + let echoedMessage: String + } + + typealias Event = Input + typealias Output = Greeting + + func handle( + _ event: Event, + outputWriter: some LambdaResponseWriter, + context: LambdaContext + ) async throws { + // Return result to the Lambda control plane + context.logger.debug("BackgroundProcessingHandler - message received") + try await outputWriter.write(Greeting(echoedMessage: event.message)) + + // Perform some background work, e.g: + context.logger.debug("BackgroundProcessingHandler - response sent. Performing background tasks.") + try await Task.sleep(for: .seconds(10)) + + // Exit the function. All asynchronous work has been executed before exiting the scope of this function. + // Follows structured concurrency principles. + context.logger.debug("BackgroundProcessingHandler - Background tasks completed. Returning") + return + } +} + +let adapter = LambdaCodableAdapterSendable(handler: BackgroundProcessingHandler()) +let runtime = LambdaManagedRuntime(handler: adapter) +try await runtime.run() diff --git a/Examples/ManagedInstances/Sources/HelloJSON/main.swift b/Examples/ManagedInstances/Sources/HelloJSON/main.swift new file mode 100644 index 000000000..f166c02f5 --- /dev/null +++ b/Examples/ManagedInstances/Sources/HelloJSON/main.swift @@ -0,0 +1,41 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the SwiftAWSLambdaRuntime open source project +// +// Copyright SwiftAWSLambdaRuntime project authors +// Copyright (c) Amazon.com, Inc. or its affiliates. +// Licensed under Apache License v2.0 +// +// See LICENSE.txt for license information +// See CONTRIBUTORS.txt for the list of SwiftAWSLambdaRuntime project authors +// +// SPDX-License-Identifier: Apache-2.0 +// +//===----------------------------------------------------------------------===// + +import AWSLambdaRuntime + +// in this example we are receiving and responding with JSON structures + +// the data structure to represent the input parameter +struct HelloRequest: Decodable { + let name: String + let age: Int +} + +// the data structure to represent the output response +struct HelloResponse: Encodable { + let greetings: String +} + +// the Lambda runtime +let runtime = LambdaManagedRuntime { + (event: HelloRequest, context: LambdaContext) in + + HelloResponse( + greetings: "Hello \(event.name). You look \(event.age > 30 ? "younger" : "older") than your age." + ) +} + +// start the loop +try await runtime.run() diff --git a/Examples/ManagedInstances/Sources/Streaming/main.swift b/Examples/ManagedInstances/Sources/Streaming/main.swift new file mode 100644 index 000000000..3109d55fd --- /dev/null +++ b/Examples/ManagedInstances/Sources/Streaming/main.swift @@ -0,0 +1,69 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the SwiftAWSLambdaRuntime open source project +// +// Copyright SwiftAWSLambdaRuntime project authors +// Copyright (c) Amazon.com, Inc. or its affiliates. +// Licensed under Apache License v2.0 +// +// See LICENSE.txt for license information +// See CONTRIBUTORS.txt for the list of SwiftAWSLambdaRuntime project authors +// +// SPDX-License-Identifier: Apache-2.0 +// +//===----------------------------------------------------------------------===// + +import AWSLambdaEvents +import AWSLambdaRuntime +import NIOCore + +#if canImport(FoundationEssentials) +import FoundationEssentials +#else +import Foundation +#endif + +// for a simple struct as this one, the compiler automatically infers Sendable +// With Lambda Managed Instances, your handler struct MUST be Sendable +struct SendNumbersWithPause: StreamingLambdaHandler, Sendable { + func handle( + _ event: ByteBuffer, + responseWriter: some LambdaResponseStreamWriter, + context: LambdaContext + ) async throws { + + // The payload here is a Lambda Function URL request + // Check the body of the Function URL request to extract the business event + let payload = try JSONDecoder().decode(FunctionURLRequest.self, from: Data(event.readableBytesView)) + let _ = payload.body + + // Send HTTP status code and headers before streaming the response body + try await responseWriter.writeStatusAndHeaders( + StreamingLambdaStatusAndHeadersResponse( + statusCode: 418, // I'm a tea pot + headers: [ + "Content-Type": "text/plain", + "x-my-custom-header": "streaming-example", + ] + ) + ) + + // Stream numbers with pauses to demonstrate streaming functionality + for i in 1...3 { + // Send partial data + try await responseWriter.write(ByteBuffer(string: "Number: \(i)\n")) + + // Perform some long asynchronous work to simulate processing + try await Task.sleep(for: .milliseconds(1000)) + } + + // Send final message + try await responseWriter.write(ByteBuffer(string: "Streaming complete!\n")) + + // All data has been sent. Close off the response stream. + try await responseWriter.finish() + } +} + +let runtime = LambdaManagedRuntime(handler: SendNumbersWithPause()) +try await runtime.run() diff --git a/Examples/ManagedInstances/Streaming/main.swift b/Examples/ManagedInstances/Streaming/main.swift new file mode 100644 index 000000000..3109d55fd --- /dev/null +++ b/Examples/ManagedInstances/Streaming/main.swift @@ -0,0 +1,69 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the SwiftAWSLambdaRuntime open source project +// +// Copyright SwiftAWSLambdaRuntime project authors +// Copyright (c) Amazon.com, Inc. or its affiliates. +// Licensed under Apache License v2.0 +// +// See LICENSE.txt for license information +// See CONTRIBUTORS.txt for the list of SwiftAWSLambdaRuntime project authors +// +// SPDX-License-Identifier: Apache-2.0 +// +//===----------------------------------------------------------------------===// + +import AWSLambdaEvents +import AWSLambdaRuntime +import NIOCore + +#if canImport(FoundationEssentials) +import FoundationEssentials +#else +import Foundation +#endif + +// for a simple struct as this one, the compiler automatically infers Sendable +// With Lambda Managed Instances, your handler struct MUST be Sendable +struct SendNumbersWithPause: StreamingLambdaHandler, Sendable { + func handle( + _ event: ByteBuffer, + responseWriter: some LambdaResponseStreamWriter, + context: LambdaContext + ) async throws { + + // The payload here is a Lambda Function URL request + // Check the body of the Function URL request to extract the business event + let payload = try JSONDecoder().decode(FunctionURLRequest.self, from: Data(event.readableBytesView)) + let _ = payload.body + + // Send HTTP status code and headers before streaming the response body + try await responseWriter.writeStatusAndHeaders( + StreamingLambdaStatusAndHeadersResponse( + statusCode: 418, // I'm a tea pot + headers: [ + "Content-Type": "text/plain", + "x-my-custom-header": "streaming-example", + ] + ) + ) + + // Stream numbers with pauses to demonstrate streaming functionality + for i in 1...3 { + // Send partial data + try await responseWriter.write(ByteBuffer(string: "Number: \(i)\n")) + + // Perform some long asynchronous work to simulate processing + try await Task.sleep(for: .milliseconds(1000)) + } + + // Send final message + try await responseWriter.write(ByteBuffer(string: "Streaming complete!\n")) + + // All data has been sent. Close off the response stream. + try await responseWriter.finish() + } +} + +let runtime = LambdaManagedRuntime(handler: SendNumbersWithPause()) +try await runtime.run() diff --git a/Examples/ManagedInstances/template.yaml b/Examples/ManagedInstances/template.yaml new file mode 100644 index 000000000..ba1307c5a --- /dev/null +++ b/Examples/ManagedInstances/template.yaml @@ -0,0 +1,81 @@ +AWSTemplateFormatVersion: '2010-09-09' +Transform: AWS::Serverless-2016-10-31 +Description: SAM Template for Lambda Managed Instances Example + +# This template deploys three Lambda functions to Lambda Managed Instances +# using a pre-existing capacity provider. + +Parameters: + CapacityProviderArn: + Type: String + Default: arn:aws:lambda:us-west-2:${AWS::AccountId}:capacity-provider:MyCapacityProvider # TODO : CHANGE The Name! + Description: ARN of the existing capacity provider for Lambda Managed Instances + +Globals: + Function: + Handler: swift.bootstrap # ignored by the Swift runtime + Runtime: provided.al2023 + Architectures: + - arm64 + Timeout: 15 + CapacityProviderConfig: + Arn: !Ref CapacityProviderArn + PerExecutionEnvironmentMaxConcurrency: 8 + Environment: + Variables: + LOG_LEVEL: trace + +Resources: + # HelloJSON Function - JSON input/output with structured data + HelloJSONFunction: + Type: AWS::Serverless::Function + Properties: + CodeUri: .build/plugins/AWSLambdaPackager/outputs/AWSLambdaPackager/HelloJSON/HelloJSON.zip + FunctionName: !Sub "${AWS::StackName}-HelloJSON" + + # Streaming Function - Demonstrates response streaming capabilities + StreamingFunction: + Type: AWS::Serverless::Function + Properties: + CodeUri: .build/plugins/AWSLambdaPackager/outputs/AWSLambdaPackager/Streaming/Streaming.zip + FunctionName: !Sub "${AWS::StackName}-Streaming" + Timeout: 60 # Longer timeout for streaming operations + FunctionUrlConfig: + AuthType: AWS_IAM + InvokeMode: RESPONSE_STREAM + + # BackgroundTasks Function - Handles long-running background processing + BackgroundTasksFunction: + Type: AWS::Serverless::Function + Properties: + CodeUri: .build/plugins/AWSLambdaPackager/outputs/AWSLambdaPackager/BackgroundTasks/BackgroundTasks.zip + FunctionName: !Sub "${AWS::StackName}-BackgroundTasks" + Timeout: 300 # 5 minutes for background processing + Environment: + Variables: + LOG_LEVEL: debug + +Outputs: + # Function URL for reference + StreamingFunctionUrl: + Description: Streaming Function URL + Value: !GetAtt StreamingFunctionUrl.FunctionUrl + + # Function ARNs for reference + HelloJSONFunctionArn: + Description: "HelloJSON Function ARN" + Value: !GetAtt HelloJSONFunction.Arn + Export: + Name: !Sub "${AWS::StackName}-HelloJSONArn" + + StreamingFunctionArn: + Description: "Streaming Function ARN" + Value: !GetAtt StreamingFunction.Arn + Export: + Name: !Sub "${AWS::StackName}-StreamingArn" + + BackgroundTasksFunctionArn: + Description: "BackgroundTasks Function ARN" + Value: !GetAtt BackgroundTasksFunction.Arn + Export: + Name: !Sub "${AWS::StackName}-BackgroundTasksArn" From e2e1c4966e391df48d7abb7a7f6c5da29083fe5a Mon Sep 17 00:00:00 2001 From: Sebastien Stormacq Date: Tue, 13 Jan 2026 13:33:50 +0100 Subject: [PATCH 21/21] re-add unit tests and integration test --- .github/workflows/pull_request.yml | 2 +- .../LambdaManagedRuntimeTests.swift | 68 +++++++++---------- 2 files changed, 35 insertions(+), 35 deletions(-) diff --git a/.github/workflows/pull_request.yml b/.github/workflows/pull_request.yml index 7964fef3b..5c17f54e2 100644 --- a/.github/workflows/pull_request.yml +++ b/.github/workflows/pull_request.yml @@ -40,7 +40,7 @@ jobs: # We pass the list of examples here, but we can't pass an array as argument # Instead, we pass a String with a valid JSON array. # The workaround is mentioned here https://github.com/orgs/community/discussions/11692 - examples: "[ 'APIGatewayV1', 'APIGatewayV2', 'APIGatewayV2+LambdaAuthorizer', 'BackgroundTasks', 'HelloJSON', 'HelloWorld', 'HelloWorldNoTraits', 'HummingbirdLambda', 'MultiSourceAPI', 'MultiTenant', 'ResourcesPackaging', 'S3EventNotifier', 'S3_AWSSDK', 'S3_Soto', 'Streaming+APIGateway', 'Streaming+FunctionUrl', 'Streaming+Codable', 'ServiceLifecycle+Postgres', 'Testing', 'Tutorial' ]" + examples: "[ 'APIGatewayV1', 'APIGatewayV2', 'APIGatewayV2+LambdaAuthorizer', 'BackgroundTasks', 'HelloJSON', 'HelloWorld', 'HelloWorldNoTraits', 'HummingbirdLambda', 'ManagedInstances', 'MultiSourceAPI', 'MultiTenant', 'ResourcesPackaging', 'S3EventNotifier', 'S3_AWSSDK', 'S3_Soto', 'Streaming+APIGateway', 'Streaming+FunctionUrl', 'Streaming+Codable', 'ServiceLifecycle+Postgres', 'Testing', 'Tutorial' ]" archive_plugin_examples: "[ 'HelloWorld', 'ResourcesPackaging' ]" archive_plugin_enabled: true diff --git a/Tests/AWSLambdaRuntimeTests/LambdaManagedRuntimeTests.swift b/Tests/AWSLambdaRuntimeTests/LambdaManagedRuntimeTests.swift index effeefbc9..91219e067 100644 --- a/Tests/AWSLambdaRuntimeTests/LambdaManagedRuntimeTests.swift +++ b/Tests/AWSLambdaRuntimeTests/LambdaManagedRuntimeTests.swift @@ -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(JSONEncoder()) - - // let concurrentTasks = 10 - - // let results = try await withThrowingTaskGroup(of: String.self) { group in - // for i in 0..(JSONEncoder()) + + let concurrentTasks = 10 + + let results = try await withThrowingTaskGroup(of: String.self) { group in + for i in 0..