From fb1622808cb284e8e86c2239c94ee3cae129b563 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A9bastien=20Stormacq?= Date: Wed, 2 Jul 2025 14:35:09 +0200 Subject: [PATCH 1/2] remove @unchecked sendable on LambdaRuntime --- .../Sources/AuthorizerLambda/main.swift | 73 +++++++++---------- .../FoundationSupport/Lambda+JSON.swift | 4 +- Sources/AWSLambdaRuntime/Lambda+Codable.swift | 16 +++- Sources/AWSLambdaRuntime/LambdaHandlers.swift | 16 ++-- Sources/AWSLambdaRuntime/LambdaRuntime.swift | 22 +----- 5 files changed, 62 insertions(+), 69 deletions(-) diff --git a/Examples/APIGateway+LambdaAuthorizer/Sources/AuthorizerLambda/main.swift b/Examples/APIGateway+LambdaAuthorizer/Sources/AuthorizerLambda/main.swift index 60ea2b7b..afe0ee21 100644 --- a/Examples/APIGateway+LambdaAuthorizer/Sources/AuthorizerLambda/main.swift +++ b/Examples/APIGateway+LambdaAuthorizer/Sources/AuthorizerLambda/main.swift @@ -22,58 +22,57 @@ import AWSLambdaRuntime // This code is shown for the example only and is not used in this demo. // This code doesn't perform any type of token validation. It should be used as a reference only. let policyAuthorizerHandler: - (APIGatewayLambdaAuthorizerRequest, LambdaContext) async throws -> APIGatewayLambdaAuthorizerPolicyResponse = { - (request: APIGatewayLambdaAuthorizerRequest, context: LambdaContext) in + @Sendable (APIGatewayLambdaAuthorizerRequest, LambdaContext) async throws -> + APIGatewayLambdaAuthorizerPolicyResponse = { + (request: APIGatewayLambdaAuthorizerRequest, context: LambdaContext) in - context.logger.debug("+++ Policy Authorizer called +++") + context.logger.debug("+++ Policy Authorizer called +++") - // typically, this function will check the validity of the incoming token received in the request + // typically, this function will check the validity of the incoming token received in the request - // then it creates and returns a response - return APIGatewayLambdaAuthorizerPolicyResponse( - principalId: "John Appleseed", + // then it creates and returns a response + return APIGatewayLambdaAuthorizerPolicyResponse( + principalId: "John Appleseed", - // this policy allows the caller to invoke any API Gateway endpoint - policyDocument: .init(statement: [ - .init( - action: "execute-api:Invoke", - effect: .allow, - resource: "*" - ) + // this policy allows the caller to invoke any API Gateway endpoint + policyDocument: .init(statement: [ + .init( + action: "execute-api:Invoke", + effect: .allow, + resource: "*" + ) - ]), + ]), - // this is additional context we want to return to the caller - context: [ - "abc1": "xyz1", - "abc2": "xyz2", - ] - ) - } + // this is additional context we want to return to the caller + context: [ + "abc1": "xyz1", + "abc2": "xyz2", + ] + ) + } +// let runtime = LambdaRuntime(body: policyAuthorizerHandler) // // This is an example of a simple authorizer that always authorizes the request. // A simple authorizer returns a yes/no decision and optional context key-value pairs // // This code doesn't perform any type of token validation. It should be used as a reference only. -let simpleAuthorizerHandler: - (APIGatewayLambdaAuthorizerRequest, LambdaContext) async throws -> APIGatewayLambdaAuthorizerSimpleResponse = { - (_: APIGatewayLambdaAuthorizerRequest, context: LambdaContext) in +let runtime = LambdaRuntime { + (_: APIGatewayLambdaAuthorizerRequest, context: LambdaContext) -> APIGatewayLambdaAuthorizerSimpleResponse in - context.logger.debug("+++ Simple Authorizer called +++") + context.logger.debug("+++ Simple Authorizer called +++") - // typically, this function will check the validity of the incoming token received in the request + // typically, this function will check the validity of the incoming token received in the request - return APIGatewayLambdaAuthorizerSimpleResponse( - // this is the authorization decision: yes or no - isAuthorized: true, + return APIGatewayLambdaAuthorizerSimpleResponse( + // this is the authorization decision: yes or no + isAuthorized: true, - // this is additional context we want to return to the caller - context: ["abc1": "xyz1"] - ) - } + // this is additional context we want to return to the caller + context: ["abc1": "xyz1"] + ) +} -// create the runtime and start polling for new events. -// in this demo we use the simple authorizer handler -let runtime = LambdaRuntime(body: simpleAuthorizerHandler) +// start polling for new events. try await runtime.run() diff --git a/Sources/AWSLambdaRuntime/FoundationSupport/Lambda+JSON.swift b/Sources/AWSLambdaRuntime/FoundationSupport/Lambda+JSON.swift index 9b47bd7d..5152af71 100644 --- a/Sources/AWSLambdaRuntime/FoundationSupport/Lambda+JSON.swift +++ b/Sources/AWSLambdaRuntime/FoundationSupport/Lambda+JSON.swift @@ -95,7 +95,7 @@ extension LambdaRuntime { decoder: JSONDecoder = JSONDecoder(), encoder: JSONEncoder = JSONEncoder(), logger: Logger = Logger(label: "LambdaRuntime"), - body: sending @escaping (Event, LambdaContext) async throws -> Output + body: @Sendable @escaping (Event, LambdaContext) async throws -> Output ) where Handler == LambdaCodableAdapter< @@ -122,7 +122,7 @@ extension LambdaRuntime { public convenience init( decoder: JSONDecoder = JSONDecoder(), logger: Logger = Logger(label: "LambdaRuntime"), - body: sending @escaping (Event, LambdaContext) async throws -> Void + body: @Sendable @escaping (Event, LambdaContext) async throws -> Void ) where Handler == LambdaCodableAdapter< diff --git a/Sources/AWSLambdaRuntime/Lambda+Codable.swift b/Sources/AWSLambdaRuntime/Lambda+Codable.swift index abc8728b..6aafa71f 100644 --- a/Sources/AWSLambdaRuntime/Lambda+Codable.swift +++ b/Sources/AWSLambdaRuntime/Lambda+Codable.swift @@ -16,7 +16,7 @@ import NIOCore /// The protocol a decoder must conform to so that it can be used with ``LambdaCodableAdapter`` to decode incoming /// `ByteBuffer` events. -public protocol LambdaEventDecoder { +public protocol LambdaEventDecoder: Sendable { /// Decode the `ByteBuffer` representing the received event into the generic `Event` type /// the handler will receive. /// - Parameters: @@ -28,7 +28,7 @@ public protocol LambdaEventDecoder { /// The protocol an encoder must conform to so that it can be used with ``LambdaCodableAdapter`` to encode the generic /// ``LambdaOutputEncoder/Output`` object into a `ByteBuffer`. -public protocol LambdaOutputEncoder { +public protocol LambdaOutputEncoder: Sendable { associatedtype Output /// Encode the generic type `Output` the handler has returned into a `ByteBuffer`. @@ -52,7 +52,7 @@ public struct LambdaHandlerAdapter< Event: Decodable, Output, Handler: LambdaHandler ->: LambdaWithBackgroundProcessingHandler where Handler.Event == Event, Handler.Output == Output { +>: LambdaWithBackgroundProcessingHandler where Handler.Event == Event, Handler.Output == Output, Handler: Sendable { @usableFromInline let handler: Handler /// Initializes an instance given a concrete handler. @@ -86,7 +86,15 @@ public struct LambdaCodableAdapter< Output, Decoder: LambdaEventDecoder, Encoder: LambdaOutputEncoder ->: StreamingLambdaHandler where Handler.Event == Event, Handler.Output == Output, Encoder.Output == Output { +>: StreamingLambdaHandler +where + Handler.Event == Event, + Handler.Output == Output, + Encoder.Output == Output, + Handler: Sendable, + Decoder: Sendable, + Encoder: Sendable +{ @usableFromInline let handler: Handler @usableFromInline let encoder: Encoder @usableFromInline let decoder: Decoder diff --git a/Sources/AWSLambdaRuntime/LambdaHandlers.swift b/Sources/AWSLambdaRuntime/LambdaHandlers.swift index cc23fa4a..382acc39 100644 --- a/Sources/AWSLambdaRuntime/LambdaHandlers.swift +++ b/Sources/AWSLambdaRuntime/LambdaHandlers.swift @@ -21,7 +21,7 @@ import NIOCore /// 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. -public protocol StreamingLambdaHandler: _Lambda_SendableMetatype { +public protocol StreamingLambdaHandler: Sendable, _Lambda_SendableMetatype { /// The handler function -- implement the business logic of the Lambda function here. /// - Parameters: /// - event: The invocation's input data. @@ -65,7 +65,7 @@ public protocol LambdaResponseStreamWriter { /// /// - 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. -public protocol LambdaHandler { +public protocol LambdaHandler: Sendable { /// Generic input type. /// The body of the request sent to Lambda will be decoded into this type for the handler to consume. associatedtype Event @@ -87,7 +87,7 @@ public protocol LambdaHandler { /// ``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. -public protocol LambdaWithBackgroundProcessingHandler { +public protocol LambdaWithBackgroundProcessingHandler: Sendable { /// Generic input type. /// The body of the request sent to Lambda will be decoded into this type for the handler to consume. associatedtype Event @@ -151,17 +151,17 @@ public struct StreamingClosureHandler: StreamingLambdaHandler { /// 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. public struct ClosureHandler: LambdaHandler { - let body: (Event, LambdaContext) async throws -> Output + 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: sending @escaping (Event, LambdaContext) async throws -> Output) where Output: Encodable { + 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: @escaping (Event, LambdaContext) async throws -> Void) where Output == Void { + public init(body: @Sendable @escaping (Event, LambdaContext) async throws -> Void) where Output == Void { self.body = body } @@ -202,7 +202,7 @@ extension LambdaRuntime { encoder: sending Encoder, decoder: sending Decoder, logger: Logger = Logger(label: "LambdaRuntime"), - body: sending @escaping (Event, LambdaContext) async throws -> Output + body: @Sendable @escaping (Event, LambdaContext) async throws -> Output ) where Handler == LambdaCodableAdapter< @@ -232,7 +232,7 @@ extension LambdaRuntime { public convenience init( decoder: sending Decoder, logger: Logger = Logger(label: "LambdaRuntime"), - body: sending @escaping (Event, LambdaContext) async throws -> Void + body: @Sendable @escaping (Event, LambdaContext) async throws -> Void ) where Handler == LambdaCodableAdapter< diff --git a/Sources/AWSLambdaRuntime/LambdaRuntime.swift b/Sources/AWSLambdaRuntime/LambdaRuntime.swift index d1d1674c..69da3b5b 100644 --- a/Sources/AWSLambdaRuntime/LambdaRuntime.swift +++ b/Sources/AWSLambdaRuntime/LambdaRuntime.swift @@ -22,13 +22,9 @@ import FoundationEssentials import Foundation #endif -// We need `@unchecked` Sendable here, as `NIOLockedValueBox` does not understand `sending` today. -// We don't want to use `NIOLockedValueBox` here anyway. We would love to use Mutex here, but this -// sadly crashes the compiler today. -public final class LambdaRuntime: @unchecked Sendable where Handler: StreamingLambdaHandler { - // TODO: We want to change this to Mutex as soon as this doesn't crash the Swift compiler on Linux anymore +public final class LambdaRuntime: Sendable where Handler: StreamingLambdaHandler { @usableFromInline - let handlerMutex: NIOLockedValueBox + let handler: Handler @usableFromInline let logger: Logger @usableFromInline @@ -39,7 +35,7 @@ public final class LambdaRuntime: @unchecked Sendable where Handler: St eventLoop: EventLoop = Lambda.defaultEventLoop, logger: Logger = Logger(label: "LambdaRuntime") ) { - self.handlerMutex = NIOLockedValueBox(handler) + self.handler = handler self.eventLoop = eventLoop // by setting the log level here, we understand it can not be changed dynamically at runtime @@ -64,16 +60,6 @@ public final class LambdaRuntime: @unchecked Sendable where Handler: St @inlinable internal func _run() async throws { - let handler = self.handlerMutex.withLockedValue { handler in - let result = handler - handler = nil - return result - } - - guard let handler else { - throw LambdaRuntimeError(code: .runtimeCanOnlyBeStartedOnce) - } - // 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 @@ -110,7 +96,7 @@ public final class LambdaRuntime: @unchecked Sendable where Handler: St ) { runtimeClient in try await Lambda.runLoop( runtimeClient: runtimeClient, - handler: handler, + handler: self.handler, logger: self.logger ) } From fd6ede9fcca29c7fd3f1f70b04ba80423e8f1e06 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A9bastien=20Stormacq?= Date: Wed, 2 Jul 2025 14:45:23 +0200 Subject: [PATCH 2/2] remove explicit @Sendable --- .../Sources/AuthorizerLambda/main.swift | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/Examples/APIGateway+LambdaAuthorizer/Sources/AuthorizerLambda/main.swift b/Examples/APIGateway+LambdaAuthorizer/Sources/AuthorizerLambda/main.swift index afe0ee21..32a6feaa 100644 --- a/Examples/APIGateway+LambdaAuthorizer/Sources/AuthorizerLambda/main.swift +++ b/Examples/APIGateway+LambdaAuthorizer/Sources/AuthorizerLambda/main.swift @@ -22,7 +22,7 @@ import AWSLambdaRuntime // This code is shown for the example only and is not used in this demo. // This code doesn't perform any type of token validation. It should be used as a reference only. let policyAuthorizerHandler: - @Sendable (APIGatewayLambdaAuthorizerRequest, LambdaContext) async throws -> + (APIGatewayLambdaAuthorizerRequest, LambdaContext) async throws -> APIGatewayLambdaAuthorizerPolicyResponse = { (request: APIGatewayLambdaAuthorizerRequest, context: LambdaContext) in @@ -51,7 +51,9 @@ let policyAuthorizerHandler: ] ) } -// let runtime = LambdaRuntime(body: policyAuthorizerHandler) +// let runtime = LambdaRuntime() { try await policyAuthorizerHandler($0, $1) } +// start polling for new events. +// try await runtime.run() // // This is an example of a simple authorizer that always authorizes the request.