From 456569a51c82f9c5ef960dffe47141cd8077d27d Mon Sep 17 00:00:00 2001 From: Sebastien Stormacq Date: Fri, 14 Mar 2025 13:49:07 +0100 Subject: [PATCH 01/23] make LambdaRuntime a singleton without breaking the API --- .../ControlPlaneRequest.swift | 8 ++-- .../FoundationSupport/Lambda+JSON.swift | 12 +++++- Sources/AWSLambdaRuntime/LambdaHandlers.swift | 20 +++++++--- Sources/AWSLambdaRuntime/LambdaRuntime.swift | 29 ++++++++++++-- .../LambdaRuntimeClient.swift | 32 ++++++++-------- ...r.swift => LambdaRuntimeClientError.swift} | 2 +- .../LambdaRuntimeTests.swift | 38 +++++++++++++++++++ 7 files changed, 110 insertions(+), 31 deletions(-) rename Sources/AWSLambdaRuntime/{LambdaRuntimeError.swift => LambdaRuntimeClientError.swift} (96%) create mode 100644 Tests/AWSLambdaRuntimeTests/LambdaRuntimeTests.swift diff --git a/Sources/AWSLambdaRuntime/ControlPlaneRequest.swift b/Sources/AWSLambdaRuntime/ControlPlaneRequest.swift index 29016b0e..b15e1dbd 100644 --- a/Sources/AWSLambdaRuntime/ControlPlaneRequest.swift +++ b/Sources/AWSLambdaRuntime/ControlPlaneRequest.swift @@ -36,19 +36,19 @@ package struct InvocationMetadata: Hashable { package let clientContext: String? package let cognitoIdentity: String? - package init(headers: HTTPHeaders) throws(LambdaRuntimeError) { + package init(headers: HTTPHeaders) throws(LambdaRuntimeClientError) { guard let requestID = headers.first(name: AmazonHeaders.requestID), !requestID.isEmpty else { - throw LambdaRuntimeError(code: .nextInvocationMissingHeaderRequestID) + throw LambdaRuntimeClientError(code: .nextInvocationMissingHeaderRequestID) } guard let deadline = headers.first(name: AmazonHeaders.deadline), let unixTimeInMilliseconds = Int64(deadline) else { - throw LambdaRuntimeError(code: .nextInvocationMissingHeaderDeadline) + throw LambdaRuntimeClientError(code: .nextInvocationMissingHeaderDeadline) } guard let invokedFunctionARN = headers.first(name: AmazonHeaders.invokedFunctionARN) else { - throw LambdaRuntimeError(code: .nextInvocationMissingHeaderInvokeFuctionARN) + throw LambdaRuntimeClientError(code: .nextInvocationMissingHeaderInvokeFuctionARN) } self.requestID = requestID diff --git a/Sources/AWSLambdaRuntime/FoundationSupport/Lambda+JSON.swift b/Sources/AWSLambdaRuntime/FoundationSupport/Lambda+JSON.swift index 9bd4d30f..aedf7782 100644 --- a/Sources/AWSLambdaRuntime/FoundationSupport/Lambda+JSON.swift +++ b/Sources/AWSLambdaRuntime/FoundationSupport/Lambda+JSON.swift @@ -108,7 +108,11 @@ extension LambdaRuntime { handler: LambdaHandlerAdapter(handler: ClosureHandler(body: body)) ) - self.init(handler: handler) + do { + try self.init(handler: handler) + } catch { + fatalError("Failed to initialize LambdaRuntime: \(error)") + } } /// Initialize an instance with a `LambdaHandler` defined in the form of a closure **with a `Void` return type**. @@ -132,7 +136,11 @@ extension LambdaRuntime { handler: LambdaHandlerAdapter(handler: ClosureHandler(body: body)) ) - self.init(handler: handler) + do { + try self.init(handler: handler) + } catch { + fatalError("Failed to initialize LambdaRuntime: \(error)") + } } } #endif // trait: FoundationJSONSupport diff --git a/Sources/AWSLambdaRuntime/LambdaHandlers.swift b/Sources/AWSLambdaRuntime/LambdaHandlers.swift index d6e0b373..e8f9dac1 100644 --- a/Sources/AWSLambdaRuntime/LambdaHandlers.swift +++ b/Sources/AWSLambdaRuntime/LambdaHandlers.swift @@ -179,7 +179,11 @@ extension LambdaRuntime { public convenience init( body: @Sendable @escaping (ByteBuffer, LambdaResponseStreamWriter, LambdaContext) async throws -> Void ) where Handler == StreamingClosureHandler { - self.init(handler: StreamingClosureHandler(body: body)) + do { + try self.init(handler: StreamingClosureHandler(body: body)) + } catch { + fatalError("Failed to initialize LambdaRuntime: \(error)") + } } /// Initialize an instance with a ``LambdaHandler`` defined in the form of a closure **with a non-`Void` return type**, an encoder, and a decoder. @@ -213,8 +217,11 @@ extension LambdaRuntime { decoder: decoder, handler: streamingAdapter ) - - self.init(handler: codableWrapper) + do { + try self.init(handler: codableWrapper) + } catch { + fatalError("Failed to initialize LambdaRuntime: \(error)") + } } /// Initialize an instance with a ``LambdaHandler`` defined in the form of a closure **with a `Void` return type**, an encoder, and a decoder. @@ -238,7 +245,10 @@ extension LambdaRuntime { decoder: decoder, handler: LambdaHandlerAdapter(handler: ClosureHandler(body: body)) ) - - self.init(handler: handler) + do { + try self.init(handler: handler) + } catch { + fatalError("Failed to initialize LambdaRuntime: \(error)") + } } } diff --git a/Sources/AWSLambdaRuntime/LambdaRuntime.swift b/Sources/AWSLambdaRuntime/LambdaRuntime.swift index 6bc2403c..c1aed07d 100644 --- a/Sources/AWSLambdaRuntime/LambdaRuntime.swift +++ b/Sources/AWSLambdaRuntime/LambdaRuntime.swift @@ -15,6 +15,7 @@ import Logging import NIOConcurrencyHelpers import NIOCore +import Synchronization #if canImport(FoundationEssentials) import FoundationEssentials @@ -22,6 +23,13 @@ import FoundationEssentials import Foundation #endif +// This is our gardian to ensure only one LambdaRuntime is initialized +// We use a Mutex here to ensure thread safety +// We don't use LambdaRuntime<> as the type here, as we don't know the concrete type that will be used +private let _singleton = Mutex(false) +public enum LambdaRuntimeError: Error { + case moreThanOneLambdaRuntimeInstance +} // 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. @@ -35,7 +43,22 @@ public final class LambdaRuntime: @unchecked Sendable where Handler: St handler: sending Handler, eventLoop: EventLoop = Lambda.defaultEventLoop, logger: Logger = Logger(label: "LambdaRuntime") - ) { + ) throws(LambdaRuntimeError) { + + do { + try _singleton.withLock { + let alreadyCreated = $0 + guard alreadyCreated == false else { + throw LambdaRuntimeError.moreThanOneLambdaRuntimeInstance + } + $0 = true + } + } catch _ as LambdaRuntimeError { + throw LambdaRuntimeError.moreThanOneLambdaRuntimeInstance + } catch { + fatalError("An unknown error occurred: \(error)") + } + self.handlerMutex = NIOLockedValueBox(handler) self.eventLoop = eventLoop @@ -56,7 +79,7 @@ public final class LambdaRuntime: @unchecked Sendable where Handler: St } guard let handler else { - throw LambdaRuntimeError(code: .runtimeCanOnlyBeStartedOnce) + throw LambdaRuntimeClientError(code: .runtimeCanOnlyBeStartedOnce) } // are we running inside an AWS Lambda runtime environment ? @@ -66,7 +89,7 @@ public final class LambdaRuntime: @unchecked Sendable where Handler: St let ipAndPort = runtimeEndpoint.split(separator: ":", maxSplits: 1) let ip = String(ipAndPort[0]) - guard let port = Int(ipAndPort[1]) else { throw LambdaRuntimeError(code: .invalidPort) } + guard let port = Int(ipAndPort[1]) else { throw LambdaRuntimeClientError(code: .invalidPort) } try await LambdaRuntimeClient.withRuntimeClient( configuration: .init(ip: ip, port: port), diff --git a/Sources/AWSLambdaRuntime/LambdaRuntimeClient.swift b/Sources/AWSLambdaRuntime/LambdaRuntimeClient.swift index 196cbeb1..98fe0c25 100644 --- a/Sources/AWSLambdaRuntime/LambdaRuntimeClient.swift +++ b/Sources/AWSLambdaRuntime/LambdaRuntimeClient.swift @@ -134,7 +134,7 @@ final actor LambdaRuntimeClient: LambdaRuntimeClientProtocol { case .connecting(let continuations): for continuation in continuations { - continuation.resume(throwing: LambdaRuntimeError(code: .closingRuntimeClient)) + continuation.resume(throwing: LambdaRuntimeClientError(code: .closingRuntimeClient)) } self.connectionState = .connecting([]) @@ -173,7 +173,7 @@ final actor LambdaRuntimeClient: LambdaRuntimeClientProtocol { private func write(_ buffer: NIOCore.ByteBuffer) async throws { switch self.lambdaState { case .idle, .sentResponse: - throw LambdaRuntimeError(code: .writeAfterFinishHasBeenSent) + throw LambdaRuntimeClientError(code: .writeAfterFinishHasBeenSent) case .waitingForNextInvocation: fatalError("Invalid state: \(self.lambdaState)") @@ -194,7 +194,7 @@ final actor LambdaRuntimeClient: LambdaRuntimeClientProtocol { private func writeAndFinish(_ buffer: NIOCore.ByteBuffer?) async throws { switch self.lambdaState { case .idle, .sentResponse: - throw LambdaRuntimeError(code: .finishAfterFinishHasBeenSent) + throw LambdaRuntimeClientError(code: .finishAfterFinishHasBeenSent) case .waitingForNextInvocation: fatalError("Invalid state: \(self.lambdaState)") @@ -261,7 +261,7 @@ final actor LambdaRuntimeClient: LambdaRuntimeClientProtocol { case (.connecting(let array), .notClosing): self.connectionState = .disconnected for continuation in array { - continuation.resume(throwing: LambdaRuntimeError(code: .lostConnectionToControlPlane)) + continuation.resume(throwing: LambdaRuntimeClientError(code: .lostConnectionToControlPlane)) } case (.connecting(let array), .closing(let continuation)): @@ -394,7 +394,7 @@ extension LambdaRuntimeClient: LambdaChannelHandlerDelegate { } for continuation in continuations { - continuation.resume(throwing: LambdaRuntimeError(code: .connectionToControlPlaneLost)) + continuation.resume(throwing: LambdaRuntimeClientError(code: .connectionToControlPlaneLost)) } case .connected(let stateChannel, _): @@ -489,7 +489,7 @@ private final class LambdaChannelHandler fatalError("Invalid state: \(self.state)") case .disconnected: - throw LambdaRuntimeError(code: .connectionToControlPlaneLost) + throw LambdaRuntimeClientError(code: .connectionToControlPlaneLost) } } @@ -528,10 +528,10 @@ private final class LambdaChannelHandler ) case .disconnected: - throw LambdaRuntimeError(code: .connectionToControlPlaneLost) + throw LambdaRuntimeClientError(code: .connectionToControlPlaneLost) case .closing: - throw LambdaRuntimeError(code: .connectionToControlPlaneGoingAway) + throw LambdaRuntimeClientError(code: .connectionToControlPlaneGoingAway) } } @@ -553,13 +553,13 @@ private final class LambdaChannelHandler case .connected(_, .idle), .connected(_, .sentResponse): - throw LambdaRuntimeError(code: .writeAfterFinishHasBeenSent) + throw LambdaRuntimeClientError(code: .writeAfterFinishHasBeenSent) case .disconnected: - throw LambdaRuntimeError(code: .connectionToControlPlaneLost) + throw LambdaRuntimeClientError(code: .connectionToControlPlaneLost) case .closing: - throw LambdaRuntimeError(code: .connectionToControlPlaneGoingAway) + throw LambdaRuntimeClientError(code: .connectionToControlPlaneGoingAway) } } @@ -586,13 +586,13 @@ private final class LambdaChannelHandler } case .connected(_, .sentResponse): - throw LambdaRuntimeError(code: .finishAfterFinishHasBeenSent) + throw LambdaRuntimeClientError(code: .finishAfterFinishHasBeenSent) case .disconnected: - throw LambdaRuntimeError(code: .connectionToControlPlaneLost) + throw LambdaRuntimeClientError(code: .connectionToControlPlaneLost) case .closing: - throw LambdaRuntimeError(code: .connectionToControlPlaneGoingAway) + throw LambdaRuntimeClientError(code: .connectionToControlPlaneGoingAway) } } @@ -759,7 +759,7 @@ extension LambdaChannelHandler: ChannelInboundHandler { self.delegate.connectionWillClose(channel: context.channel) context.close(promise: nil) continuation.resume( - throwing: LambdaRuntimeError(code: .invocationMissingMetadata, underlying: error) + throwing: LambdaRuntimeClientError(code: .invocationMissingMetadata, underlying: error) ) } @@ -769,7 +769,7 @@ extension LambdaChannelHandler: ChannelInboundHandler { continuation.resume() } else { self.state = .connected(context, .idle) - continuation.resume(throwing: LambdaRuntimeError(code: .unexpectedStatusCodeForRequest)) + continuation.resume(throwing: LambdaRuntimeClientError(code: .unexpectedStatusCodeForRequest)) } case .disconnected, .closing, .connected(_, _): diff --git a/Sources/AWSLambdaRuntime/LambdaRuntimeError.swift b/Sources/AWSLambdaRuntime/LambdaRuntimeClientError.swift similarity index 96% rename from Sources/AWSLambdaRuntime/LambdaRuntimeError.swift rename to Sources/AWSLambdaRuntime/LambdaRuntimeClientError.swift index a6b4ac66..3b7ebff2 100644 --- a/Sources/AWSLambdaRuntime/LambdaRuntimeError.swift +++ b/Sources/AWSLambdaRuntime/LambdaRuntimeClientError.swift @@ -12,7 +12,7 @@ // //===----------------------------------------------------------------------===// -package struct LambdaRuntimeError: Error { +package struct LambdaRuntimeClientError: Error { package enum Code { case closingRuntimeClient diff --git a/Tests/AWSLambdaRuntimeTests/LambdaRuntimeTests.swift b/Tests/AWSLambdaRuntimeTests/LambdaRuntimeTests.swift new file mode 100644 index 00000000..bacd1b90 --- /dev/null +++ b/Tests/AWSLambdaRuntimeTests/LambdaRuntimeTests.swift @@ -0,0 +1,38 @@ +import Foundation +import Logging +import NIOCore +import Synchronization +import Testing + +@testable import AWSLambdaRuntime + +@Suite("LambdaRuntimeTests") +final class LambdaRuntimeTests { + + @Test("LambdaRuntime can only be initialized once") + func testLambdaRuntimeInitializationFatalError() throws { + + // First initialization should succeed + try _ = LambdaRuntime(handler: MockHandler(), eventLoop: Lambda.defaultEventLoop, logger: Logger(label: "Test")) + + // Second initialization should trigger LambdaRuntimeError + #expect(throws: LambdaRuntimeError.self) { + try _ = LambdaRuntime( + handler: MockHandler(), + eventLoop: Lambda.defaultEventLoop, + logger: Logger(label: "Test") + ) + } + + } +} + +struct MockHandler: StreamingLambdaHandler { + mutating func handle( + _ event: NIOCore.ByteBuffer, + responseWriter: some AWSLambdaRuntime.LambdaResponseStreamWriter, + context: AWSLambdaRuntime.LambdaContext + ) async throws { + + } +} From a63a6394288296db7abf3e7bf123363a5750d678 Mon Sep 17 00:00:00 2001 From: Sebastien Stormacq Date: Fri, 14 Mar 2025 13:57:43 +0100 Subject: [PATCH 02/23] fix license header --- .../AWSLambdaRuntimeTests/LambdaRuntimeTests.swift | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/Tests/AWSLambdaRuntimeTests/LambdaRuntimeTests.swift b/Tests/AWSLambdaRuntimeTests/LambdaRuntimeTests.swift index bacd1b90..12cf4ae0 100644 --- a/Tests/AWSLambdaRuntimeTests/LambdaRuntimeTests.swift +++ b/Tests/AWSLambdaRuntimeTests/LambdaRuntimeTests.swift @@ -1,3 +1,17 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the SwiftAWSLambdaRuntime open source project +// +// Copyright (c) 2025 Apple Inc. and the SwiftAWSLambdaRuntime project authors +// 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 Foundation import Logging import NIOCore From 40b612720c2c9df36c9e4329fa24c7d63bdc4f87 Mon Sep 17 00:00:00 2001 From: Sebastien Stormacq Date: Fri, 14 Mar 2025 14:04:16 +0100 Subject: [PATCH 03/23] convert Mutex to NIOLockedValueBox --- Sources/AWSLambdaRuntime/LambdaRuntime.swift | 21 +++++++++++++++----- 1 file changed, 16 insertions(+), 5 deletions(-) diff --git a/Sources/AWSLambdaRuntime/LambdaRuntime.swift b/Sources/AWSLambdaRuntime/LambdaRuntime.swift index c1aed07d..17a5beeb 100644 --- a/Sources/AWSLambdaRuntime/LambdaRuntime.swift +++ b/Sources/AWSLambdaRuntime/LambdaRuntime.swift @@ -15,7 +15,9 @@ import Logging import NIOConcurrencyHelpers import NIOCore -import Synchronization + +// To be re-enabled when we will be able to use Mutex on Linux +// import Synchronization #if canImport(FoundationEssentials) import FoundationEssentials @@ -25,14 +27,16 @@ import Foundation // This is our gardian to ensure only one LambdaRuntime is initialized // We use a Mutex here to ensure thread safety -// We don't use LambdaRuntime<> as the type here, as we don't know the concrete type that will be used -private let _singleton = Mutex(false) +// We use Bool instead of LambdaRuntime as the type here, as we don't know the concrete type that will be used +// We would love to use Mutex here, but this sadly crashes the compiler today (on Linux). +// private let _singleton = Mutex(false) +private let _singleton = NIOLockedValueBox(false) public enum LambdaRuntimeError: Error { case moreThanOneLambdaRuntimeInstance } // 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. +// sadly crashes the compiler today (on Linux). 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 let handlerMutex: NIOLockedValueBox @@ -46,7 +50,14 @@ public final class LambdaRuntime: @unchecked Sendable where Handler: St ) throws(LambdaRuntimeError) { do { - try _singleton.withLock { + // try _singleton.withLock { + // let alreadyCreated = $0 + // guard alreadyCreated == false else { + // throw LambdaRuntimeError.moreThanOneLambdaRuntimeInstance + // } + // $0 = true + // } + try _singleton.withLockedValue { let alreadyCreated = $0 guard alreadyCreated == false else { throw LambdaRuntimeError.moreThanOneLambdaRuntimeInstance From d474eba60c2b9a3f724fab7235be26f5c5aa1926 Mon Sep 17 00:00:00 2001 From: Sebastien Stormacq Date: Sat, 15 Mar 2025 09:24:17 +0100 Subject: [PATCH 04/23] Replace NIOLockedValueBox with Mutex --- Sources/AWSLambdaRuntime/LambdaRuntime.swift | 29 +++++--------------- 1 file changed, 7 insertions(+), 22 deletions(-) diff --git a/Sources/AWSLambdaRuntime/LambdaRuntime.swift b/Sources/AWSLambdaRuntime/LambdaRuntime.swift index 17a5beeb..58ab317a 100644 --- a/Sources/AWSLambdaRuntime/LambdaRuntime.swift +++ b/Sources/AWSLambdaRuntime/LambdaRuntime.swift @@ -15,9 +15,7 @@ import Logging import NIOConcurrencyHelpers import NIOCore - -// To be re-enabled when we will be able to use Mutex on Linux -// import Synchronization +import Synchronization #if canImport(FoundationEssentials) import FoundationEssentials @@ -28,18 +26,12 @@ import Foundation // This is our gardian to ensure only one LambdaRuntime is initialized // We use a Mutex here to ensure thread safety // We use Bool instead of LambdaRuntime as the type here, as we don't know the concrete type that will be used -// We would love to use Mutex here, but this sadly crashes the compiler today (on Linux). -// private let _singleton = Mutex(false) -private let _singleton = NIOLockedValueBox(false) +private let _singleton = Mutex(false) public enum LambdaRuntimeError: Error { case moreThanOneLambdaRuntimeInstance } -// 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 (on Linux). -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 - let handlerMutex: NIOLockedValueBox +public final class LambdaRuntime: Sendable where Handler: StreamingLambdaHandler { + let handlerMutex: Mutex let logger: Logger let eventLoop: EventLoop @@ -50,14 +42,7 @@ public final class LambdaRuntime: @unchecked Sendable where Handler: St ) throws(LambdaRuntimeError) { do { - // try _singleton.withLock { - // let alreadyCreated = $0 - // guard alreadyCreated == false else { - // throw LambdaRuntimeError.moreThanOneLambdaRuntimeInstance - // } - // $0 = true - // } - try _singleton.withLockedValue { + try _singleton.withLock { let alreadyCreated = $0 guard alreadyCreated == false else { throw LambdaRuntimeError.moreThanOneLambdaRuntimeInstance @@ -70,7 +55,7 @@ public final class LambdaRuntime: @unchecked Sendable where Handler: St fatalError("An unknown error occurred: \(error)") } - self.handlerMutex = NIOLockedValueBox(handler) + self.handlerMutex = Mutex(handler) self.eventLoop = eventLoop // by setting the log level here, we understand it can not be changed dynamically at runtime @@ -83,7 +68,7 @@ public final class LambdaRuntime: @unchecked Sendable where Handler: St } public func run() async throws { - let handler = self.handlerMutex.withLockedValue { handler in + let handler = self.handlerMutex.withLock { handler in let result = handler handler = nil return result From 46e76f354401d3c50c1eb76b6a7e55c738fcbc23 Mon Sep 17 00:00:00 2001 From: Sebastien Stormacq Date: Sat, 15 Mar 2025 09:30:34 +0100 Subject: [PATCH 05/23] revert replacing NIOLockedValueBox by Mutex --- Sources/AWSLambdaRuntime/LambdaRuntime.swift | 29 +++++++++++++++----- 1 file changed, 22 insertions(+), 7 deletions(-) diff --git a/Sources/AWSLambdaRuntime/LambdaRuntime.swift b/Sources/AWSLambdaRuntime/LambdaRuntime.swift index 58ab317a..a487d3f9 100644 --- a/Sources/AWSLambdaRuntime/LambdaRuntime.swift +++ b/Sources/AWSLambdaRuntime/LambdaRuntime.swift @@ -15,7 +15,9 @@ import Logging import NIOConcurrencyHelpers import NIOCore -import Synchronization + +// To be re-enabled when we will be able to use Mutex on Linux +// import Synchronization #if canImport(FoundationEssentials) import FoundationEssentials @@ -26,12 +28,18 @@ import Foundation // This is our gardian to ensure only one LambdaRuntime is initialized // We use a Mutex here to ensure thread safety // We use Bool instead of LambdaRuntime as the type here, as we don't know the concrete type that will be used -private let _singleton = Mutex(false) +// We would love to use Mutex here, but this sadly crashes the compiler today (on Linux). +// private let _singleton = Mutex(false) +private let _singleton: NIOLockedValueBox = NIOLockedValueBox(false) public enum LambdaRuntimeError: Error { case moreThanOneLambdaRuntimeInstance } -public final class LambdaRuntime: Sendable where Handler: StreamingLambdaHandler { - let handlerMutex: Mutex +// 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 (on Linux). +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 + let handlerMutex: NIOLockedValueBox let logger: Logger let eventLoop: EventLoop @@ -42,7 +50,14 @@ public final class LambdaRuntime: Sendable where Handler: StreamingLamb ) throws(LambdaRuntimeError) { do { - try _singleton.withLock { + // try _singleton.withLock { + // let alreadyCreated = $0 + // guard alreadyCreated == false else { + // throw LambdaRuntimeError.moreThanOneLambdaRuntimeInstance + // } + // $0 = true + // } + try _singleton.withLockedValue { let alreadyCreated = $0 guard alreadyCreated == false else { throw LambdaRuntimeError.moreThanOneLambdaRuntimeInstance @@ -55,7 +70,7 @@ public final class LambdaRuntime: Sendable where Handler: StreamingLamb fatalError("An unknown error occurred: \(error)") } - self.handlerMutex = Mutex(handler) + self.handlerMutex = NIOLockedValueBox(handler) self.eventLoop = eventLoop // by setting the log level here, we understand it can not be changed dynamically at runtime @@ -68,7 +83,7 @@ public final class LambdaRuntime: Sendable where Handler: StreamingLamb } public func run() async throws { - let handler = self.handlerMutex.withLock { handler in + let handler = self.handlerMutex.withLockedValue { handler in let result = handler handler = nil return result From 299b9bca5dda44d8c713864b6a47c89daa281ec4 Mon Sep 17 00:00:00 2001 From: Sebastien Stormacq Date: Sat, 15 Mar 2025 10:04:52 +0100 Subject: [PATCH 06/23] remove typed throw (workaround for https://github.com/swiftlang/swift/issues/80020) --- Sources/AWSLambdaRuntime/LambdaRuntime.swift | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/Sources/AWSLambdaRuntime/LambdaRuntime.swift b/Sources/AWSLambdaRuntime/LambdaRuntime.swift index a487d3f9..b1034ce8 100644 --- a/Sources/AWSLambdaRuntime/LambdaRuntime.swift +++ b/Sources/AWSLambdaRuntime/LambdaRuntime.swift @@ -47,7 +47,10 @@ public final class LambdaRuntime: @unchecked Sendable where Handler: St handler: sending Handler, eventLoop: EventLoop = Lambda.defaultEventLoop, logger: Logger = Logger(label: "LambdaRuntime") - ) throws(LambdaRuntimeError) { + ) throws { + // technically, this initializer only throws LambdaRuntime Error but the below line crashes the compiler on Linux + // https://github.com/swiftlang/swift/issues/80020 + // ) throws(LambdaRuntimeError) { do { // try _singleton.withLock { From e3a3851d36ef63b5487e04469a9bd5df16879f77 Mon Sep 17 00:00:00 2001 From: Sebastien Stormacq Date: Sat, 15 Mar 2025 10:15:37 +0100 Subject: [PATCH 07/23] fix integration tests --- Examples/BackgroundTasks/Sources/main.swift | 2 +- Examples/Streaming/Sources/main.swift | 2 +- readme.md | 4 ++-- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/Examples/BackgroundTasks/Sources/main.swift b/Examples/BackgroundTasks/Sources/main.swift index 1985fc34..3b8cef9e 100644 --- a/Examples/BackgroundTasks/Sources/main.swift +++ b/Examples/BackgroundTasks/Sources/main.swift @@ -53,5 +53,5 @@ struct BackgroundProcessingHandler: LambdaWithBackgroundProcessingHandler { } let adapter = LambdaCodableAdapter(handler: BackgroundProcessingHandler()) -let runtime = LambdaRuntime.init(handler: adapter) +let runtime = try LambdaRuntime.init(handler: adapter) try await runtime.run() diff --git a/Examples/Streaming/Sources/main.swift b/Examples/Streaming/Sources/main.swift index ce92560c..26866546 100644 --- a/Examples/Streaming/Sources/main.swift +++ b/Examples/Streaming/Sources/main.swift @@ -32,5 +32,5 @@ struct SendNumbersWithPause: StreamingLambdaHandler { } } -let runtime = LambdaRuntime.init(handler: SendNumbersWithPause()) +let runtime = try LambdaRuntime.init(handler: SendNumbersWithPause()) try await runtime.run() diff --git a/readme.md b/readme.md index 37596ed2..3c42cff8 100644 --- a/readme.md +++ b/readme.md @@ -248,7 +248,7 @@ struct SendNumbersWithPause: StreamingLambdaHandler { } } -let runtime = LambdaRuntime.init(handler: SendNumbersWithPause()) +let runtime = try LambdaRuntime.init(handler: SendNumbersWithPause()) try await runtime.run() ``` @@ -328,7 +328,7 @@ struct BackgroundProcessingHandler: LambdaWithBackgroundProcessingHandler { } let adapter = LambdaCodableAdapter(handler: BackgroundProcessingHandler()) -let runtime = LambdaRuntime.init(handler: adapter) +let runtime = try LambdaRuntime.init(handler: adapter) try await runtime.run() ``` From 4ebf24f035f7567e57b416f6685a73b3c071682c Mon Sep 17 00:00:00 2001 From: Sebastien Stormacq Date: Sat, 15 Mar 2025 10:40:24 +0100 Subject: [PATCH 08/23] Replace NIOLockedValueBox with Mutex --- Sources/AWSLambdaRuntime/LambdaRuntime.swift | 17 +++-------------- 1 file changed, 3 insertions(+), 14 deletions(-) diff --git a/Sources/AWSLambdaRuntime/LambdaRuntime.swift b/Sources/AWSLambdaRuntime/LambdaRuntime.swift index b1034ce8..32f84c47 100644 --- a/Sources/AWSLambdaRuntime/LambdaRuntime.swift +++ b/Sources/AWSLambdaRuntime/LambdaRuntime.swift @@ -15,9 +15,7 @@ import Logging import NIOConcurrencyHelpers import NIOCore - -// To be re-enabled when we will be able to use Mutex on Linux -// import Synchronization +import Synchronization #if canImport(FoundationEssentials) import FoundationEssentials @@ -28,9 +26,7 @@ import Foundation // This is our gardian to ensure only one LambdaRuntime is initialized // We use a Mutex here to ensure thread safety // We use Bool instead of LambdaRuntime as the type here, as we don't know the concrete type that will be used -// We would love to use Mutex here, but this sadly crashes the compiler today (on Linux). -// private let _singleton = Mutex(false) -private let _singleton: NIOLockedValueBox = NIOLockedValueBox(false) +private let _singleton = Mutex(false) public enum LambdaRuntimeError: Error { case moreThanOneLambdaRuntimeInstance } @@ -53,14 +49,7 @@ public final class LambdaRuntime: @unchecked Sendable where Handler: St // ) throws(LambdaRuntimeError) { do { - // try _singleton.withLock { - // let alreadyCreated = $0 - // guard alreadyCreated == false else { - // throw LambdaRuntimeError.moreThanOneLambdaRuntimeInstance - // } - // $0 = true - // } - try _singleton.withLockedValue { + try _singleton.withLock { let alreadyCreated = $0 guard alreadyCreated == false else { throw LambdaRuntimeError.moreThanOneLambdaRuntimeInstance From 3aee427aa4bdb34d26d201814da26bf2958df1a5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A9bastien=20Stormacq?= Date: Wed, 19 Mar 2025 20:38:46 +0100 Subject: [PATCH 09/23] use Atomic instead of Mutex. Make the atomix check on run() instead of init() --- Examples/BackgroundTasks/Sources/main.swift | 2 +- Examples/Streaming/Sources/main.swift | 2 +- .../ControlPlaneRequest.swift | 10 +-- .../FoundationSupport/Lambda+JSON.swift | 12 +--- Sources/AWSLambdaRuntime/LambdaHandlers.swift | 18 +----- Sources/AWSLambdaRuntime/LambdaRuntime.swift | 64 +++++++++++-------- .../LambdaRuntimeClient.swift | 32 +++++----- ...ntError.swift => LambdaRuntimeError.swift} | 13 ++-- .../LambdaRuntimeTests.swift | 57 +++++++++++++---- 9 files changed, 117 insertions(+), 93 deletions(-) rename Sources/AWSLambdaRuntime/{LambdaRuntimeClientError.swift => LambdaRuntimeError.swift} (81%) diff --git a/Examples/BackgroundTasks/Sources/main.swift b/Examples/BackgroundTasks/Sources/main.swift index 3b8cef9e..1985fc34 100644 --- a/Examples/BackgroundTasks/Sources/main.swift +++ b/Examples/BackgroundTasks/Sources/main.swift @@ -53,5 +53,5 @@ struct BackgroundProcessingHandler: LambdaWithBackgroundProcessingHandler { } let adapter = LambdaCodableAdapter(handler: BackgroundProcessingHandler()) -let runtime = try LambdaRuntime.init(handler: adapter) +let runtime = LambdaRuntime.init(handler: adapter) try await runtime.run() diff --git a/Examples/Streaming/Sources/main.swift b/Examples/Streaming/Sources/main.swift index 26866546..ce92560c 100644 --- a/Examples/Streaming/Sources/main.swift +++ b/Examples/Streaming/Sources/main.swift @@ -32,5 +32,5 @@ struct SendNumbersWithPause: StreamingLambdaHandler { } } -let runtime = try LambdaRuntime.init(handler: SendNumbersWithPause()) +let runtime = LambdaRuntime.init(handler: SendNumbersWithPause()) try await runtime.run() diff --git a/Sources/AWSLambdaRuntime/ControlPlaneRequest.swift b/Sources/AWSLambdaRuntime/ControlPlaneRequest.swift index b15e1dbd..b3ce3d52 100644 --- a/Sources/AWSLambdaRuntime/ControlPlaneRequest.swift +++ b/Sources/AWSLambdaRuntime/ControlPlaneRequest.swift @@ -36,19 +36,19 @@ package struct InvocationMetadata: Hashable { package let clientContext: String? package let cognitoIdentity: String? - package init(headers: HTTPHeaders) throws(LambdaRuntimeClientError) { - guard let requestID = headers.first(name: AmazonHeaders.requestID), !requestID.isEmpty else { - throw LambdaRuntimeClientError(code: .nextInvocationMissingHeaderRequestID) + package init(headers: HTTPHeaders) throws(LambdaRuntimeError) { + guard let requestID: String = headers.first(name: AmazonHeaders.requestID), !requestID.isEmpty else { + throw LambdaRuntimeError(code: .nextInvocationMissingHeaderRequestID) } guard let deadline = headers.first(name: AmazonHeaders.deadline), let unixTimeInMilliseconds = Int64(deadline) else { - throw LambdaRuntimeClientError(code: .nextInvocationMissingHeaderDeadline) + throw LambdaRuntimeError(code: .nextInvocationMissingHeaderDeadline) } guard let invokedFunctionARN = headers.first(name: AmazonHeaders.invokedFunctionARN) else { - throw LambdaRuntimeClientError(code: .nextInvocationMissingHeaderInvokeFuctionARN) + throw LambdaRuntimeError(code: .nextInvocationMissingHeaderInvokeFuctionARN) } self.requestID = requestID diff --git a/Sources/AWSLambdaRuntime/FoundationSupport/Lambda+JSON.swift b/Sources/AWSLambdaRuntime/FoundationSupport/Lambda+JSON.swift index aedf7782..9bd4d30f 100644 --- a/Sources/AWSLambdaRuntime/FoundationSupport/Lambda+JSON.swift +++ b/Sources/AWSLambdaRuntime/FoundationSupport/Lambda+JSON.swift @@ -108,11 +108,7 @@ extension LambdaRuntime { handler: LambdaHandlerAdapter(handler: ClosureHandler(body: body)) ) - do { - try self.init(handler: handler) - } catch { - fatalError("Failed to initialize LambdaRuntime: \(error)") - } + self.init(handler: handler) } /// Initialize an instance with a `LambdaHandler` defined in the form of a closure **with a `Void` return type**. @@ -136,11 +132,7 @@ extension LambdaRuntime { handler: LambdaHandlerAdapter(handler: ClosureHandler(body: body)) ) - do { - try self.init(handler: handler) - } catch { - fatalError("Failed to initialize LambdaRuntime: \(error)") - } + self.init(handler: handler) } } #endif // trait: FoundationJSONSupport diff --git a/Sources/AWSLambdaRuntime/LambdaHandlers.swift b/Sources/AWSLambdaRuntime/LambdaHandlers.swift index e8f9dac1..2bb78cf3 100644 --- a/Sources/AWSLambdaRuntime/LambdaHandlers.swift +++ b/Sources/AWSLambdaRuntime/LambdaHandlers.swift @@ -179,11 +179,7 @@ extension LambdaRuntime { public convenience init( body: @Sendable @escaping (ByteBuffer, LambdaResponseStreamWriter, LambdaContext) async throws -> Void ) where Handler == StreamingClosureHandler { - do { - try self.init(handler: StreamingClosureHandler(body: body)) - } catch { - fatalError("Failed to initialize LambdaRuntime: \(error)") - } + self.init(handler: StreamingClosureHandler(body: body)) } /// Initialize an instance with a ``LambdaHandler`` defined in the form of a closure **with a non-`Void` return type**, an encoder, and a decoder. @@ -217,11 +213,7 @@ extension LambdaRuntime { decoder: decoder, handler: streamingAdapter ) - do { - try self.init(handler: codableWrapper) - } catch { - fatalError("Failed to initialize LambdaRuntime: \(error)") - } + self.init(handler: codableWrapper) } /// Initialize an instance with a ``LambdaHandler`` defined in the form of a closure **with a `Void` return type**, an encoder, and a decoder. @@ -245,10 +237,6 @@ extension LambdaRuntime { decoder: decoder, handler: LambdaHandlerAdapter(handler: ClosureHandler(body: body)) ) - do { - try self.init(handler: handler) - } catch { - fatalError("Failed to initialize LambdaRuntime: \(error)") - } + self.init(handler: handler) } } diff --git a/Sources/AWSLambdaRuntime/LambdaRuntime.swift b/Sources/AWSLambdaRuntime/LambdaRuntime.swift index 32f84c47..c70d28b2 100644 --- a/Sources/AWSLambdaRuntime/LambdaRuntime.swift +++ b/Sources/AWSLambdaRuntime/LambdaRuntime.swift @@ -23,13 +23,10 @@ import FoundationEssentials import Foundation #endif -// This is our gardian to ensure only one LambdaRuntime is initialized -// We use a Mutex here to ensure thread safety -// We use Bool instead of LambdaRuntime as the type here, as we don't know the concrete type that will be used -private let _singleton = Mutex(false) -public enum LambdaRuntimeError: Error { - case moreThanOneLambdaRuntimeInstance -} +// This is our gardian to ensure only one LambdaRuntime is running at the time +// We use an Atomic here to ensure thread safety +private let _isRunning = Atomic(false) + // 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 (on Linux). @@ -43,25 +40,7 @@ public final class LambdaRuntime: @unchecked Sendable where Handler: St handler: sending Handler, eventLoop: EventLoop = Lambda.defaultEventLoop, logger: Logger = Logger(label: "LambdaRuntime") - ) throws { - // technically, this initializer only throws LambdaRuntime Error but the below line crashes the compiler on Linux - // https://github.com/swiftlang/swift/issues/80020 - // ) throws(LambdaRuntimeError) { - - do { - try _singleton.withLock { - let alreadyCreated = $0 - guard alreadyCreated == false else { - throw LambdaRuntimeError.moreThanOneLambdaRuntimeInstance - } - $0 = true - } - } catch _ as LambdaRuntimeError { - throw LambdaRuntimeError.moreThanOneLambdaRuntimeInstance - } catch { - fatalError("An unknown error occurred: \(error)") - } - + ) { self.handlerMutex = NIOLockedValueBox(handler) self.eventLoop = eventLoop @@ -74,7 +53,36 @@ public final class LambdaRuntime: @unchecked Sendable where Handler: St self.logger.debug("LambdaRuntime initialized") } + /// Make sure only one run() is called at a time public 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: .relaxed) + + // if the original value was already true, run() is already running + if original { + throw LambdaRuntimeError(code: .runtimeCanOnlyBeStartedOnce) + } + + try await withTaskCancellationHandler { + // call the internal _run() method + do { + try await self._run() + } catch { + // when we catch an error, flip back the global variable to false + _isRunning.store(false, ordering: .relaxed) + throw error + } + } onCancel: { + // when task is cancelled, flip back the global variable to false + _isRunning.store(false, ordering: .relaxed) + } + + // when we're done without error and without cancellation, flip back the global variable to false + _isRunning.store(false, ordering: .relaxed) + } + + private func _run() async throws { let handler = self.handlerMutex.withLockedValue { handler in let result = handler handler = nil @@ -82,7 +90,7 @@ public final class LambdaRuntime: @unchecked Sendable where Handler: St } guard let handler else { - throw LambdaRuntimeClientError(code: .runtimeCanOnlyBeStartedOnce) + throw LambdaRuntimeError(code: .runtimeCanOnlyBeStartedOnce) } // are we running inside an AWS Lambda runtime environment ? @@ -92,7 +100,7 @@ public final class LambdaRuntime: @unchecked Sendable where Handler: St let ipAndPort = runtimeEndpoint.split(separator: ":", maxSplits: 1) let ip = String(ipAndPort[0]) - guard let port = Int(ipAndPort[1]) else { throw LambdaRuntimeClientError(code: .invalidPort) } + guard let port = Int(ipAndPort[1]) else { throw LambdaRuntimeError(code: .invalidPort) } try await LambdaRuntimeClient.withRuntimeClient( configuration: .init(ip: ip, port: port), diff --git a/Sources/AWSLambdaRuntime/LambdaRuntimeClient.swift b/Sources/AWSLambdaRuntime/LambdaRuntimeClient.swift index 98fe0c25..196cbeb1 100644 --- a/Sources/AWSLambdaRuntime/LambdaRuntimeClient.swift +++ b/Sources/AWSLambdaRuntime/LambdaRuntimeClient.swift @@ -134,7 +134,7 @@ final actor LambdaRuntimeClient: LambdaRuntimeClientProtocol { case .connecting(let continuations): for continuation in continuations { - continuation.resume(throwing: LambdaRuntimeClientError(code: .closingRuntimeClient)) + continuation.resume(throwing: LambdaRuntimeError(code: .closingRuntimeClient)) } self.connectionState = .connecting([]) @@ -173,7 +173,7 @@ final actor LambdaRuntimeClient: LambdaRuntimeClientProtocol { private func write(_ buffer: NIOCore.ByteBuffer) async throws { switch self.lambdaState { case .idle, .sentResponse: - throw LambdaRuntimeClientError(code: .writeAfterFinishHasBeenSent) + throw LambdaRuntimeError(code: .writeAfterFinishHasBeenSent) case .waitingForNextInvocation: fatalError("Invalid state: \(self.lambdaState)") @@ -194,7 +194,7 @@ final actor LambdaRuntimeClient: LambdaRuntimeClientProtocol { private func writeAndFinish(_ buffer: NIOCore.ByteBuffer?) async throws { switch self.lambdaState { case .idle, .sentResponse: - throw LambdaRuntimeClientError(code: .finishAfterFinishHasBeenSent) + throw LambdaRuntimeError(code: .finishAfterFinishHasBeenSent) case .waitingForNextInvocation: fatalError("Invalid state: \(self.lambdaState)") @@ -261,7 +261,7 @@ final actor LambdaRuntimeClient: LambdaRuntimeClientProtocol { case (.connecting(let array), .notClosing): self.connectionState = .disconnected for continuation in array { - continuation.resume(throwing: LambdaRuntimeClientError(code: .lostConnectionToControlPlane)) + continuation.resume(throwing: LambdaRuntimeError(code: .lostConnectionToControlPlane)) } case (.connecting(let array), .closing(let continuation)): @@ -394,7 +394,7 @@ extension LambdaRuntimeClient: LambdaChannelHandlerDelegate { } for continuation in continuations { - continuation.resume(throwing: LambdaRuntimeClientError(code: .connectionToControlPlaneLost)) + continuation.resume(throwing: LambdaRuntimeError(code: .connectionToControlPlaneLost)) } case .connected(let stateChannel, _): @@ -489,7 +489,7 @@ private final class LambdaChannelHandler fatalError("Invalid state: \(self.state)") case .disconnected: - throw LambdaRuntimeClientError(code: .connectionToControlPlaneLost) + throw LambdaRuntimeError(code: .connectionToControlPlaneLost) } } @@ -528,10 +528,10 @@ private final class LambdaChannelHandler ) case .disconnected: - throw LambdaRuntimeClientError(code: .connectionToControlPlaneLost) + throw LambdaRuntimeError(code: .connectionToControlPlaneLost) case .closing: - throw LambdaRuntimeClientError(code: .connectionToControlPlaneGoingAway) + throw LambdaRuntimeError(code: .connectionToControlPlaneGoingAway) } } @@ -553,13 +553,13 @@ private final class LambdaChannelHandler case .connected(_, .idle), .connected(_, .sentResponse): - throw LambdaRuntimeClientError(code: .writeAfterFinishHasBeenSent) + throw LambdaRuntimeError(code: .writeAfterFinishHasBeenSent) case .disconnected: - throw LambdaRuntimeClientError(code: .connectionToControlPlaneLost) + throw LambdaRuntimeError(code: .connectionToControlPlaneLost) case .closing: - throw LambdaRuntimeClientError(code: .connectionToControlPlaneGoingAway) + throw LambdaRuntimeError(code: .connectionToControlPlaneGoingAway) } } @@ -586,13 +586,13 @@ private final class LambdaChannelHandler } case .connected(_, .sentResponse): - throw LambdaRuntimeClientError(code: .finishAfterFinishHasBeenSent) + throw LambdaRuntimeError(code: .finishAfterFinishHasBeenSent) case .disconnected: - throw LambdaRuntimeClientError(code: .connectionToControlPlaneLost) + throw LambdaRuntimeError(code: .connectionToControlPlaneLost) case .closing: - throw LambdaRuntimeClientError(code: .connectionToControlPlaneGoingAway) + throw LambdaRuntimeError(code: .connectionToControlPlaneGoingAway) } } @@ -759,7 +759,7 @@ extension LambdaChannelHandler: ChannelInboundHandler { self.delegate.connectionWillClose(channel: context.channel) context.close(promise: nil) continuation.resume( - throwing: LambdaRuntimeClientError(code: .invocationMissingMetadata, underlying: error) + throwing: LambdaRuntimeError(code: .invocationMissingMetadata, underlying: error) ) } @@ -769,7 +769,7 @@ extension LambdaChannelHandler: ChannelInboundHandler { continuation.resume() } else { self.state = .connected(context, .idle) - continuation.resume(throwing: LambdaRuntimeClientError(code: .unexpectedStatusCodeForRequest)) + continuation.resume(throwing: LambdaRuntimeError(code: .unexpectedStatusCodeForRequest)) } case .disconnected, .closing, .connected(_, _): diff --git a/Sources/AWSLambdaRuntime/LambdaRuntimeClientError.swift b/Sources/AWSLambdaRuntime/LambdaRuntimeError.swift similarity index 81% rename from Sources/AWSLambdaRuntime/LambdaRuntimeClientError.swift rename to Sources/AWSLambdaRuntime/LambdaRuntimeError.swift index 3b7ebff2..1b9cfe02 100644 --- a/Sources/AWSLambdaRuntime/LambdaRuntimeClientError.swift +++ b/Sources/AWSLambdaRuntime/LambdaRuntimeError.swift @@ -12,8 +12,10 @@ // //===----------------------------------------------------------------------===// -package struct LambdaRuntimeClientError: Error { - package enum Code { +public struct LambdaRuntimeError: Error { + public enum Code: Sendable { + + /// internal error codes for LambdaRuntimeClient case closingRuntimeClient case connectionToControlPlaneLost @@ -32,6 +34,9 @@ package struct LambdaRuntimeClientError: Error { case missingLambdaRuntimeAPIEnvironmentVariable case runtimeCanOnlyBeStartedOnce case invalidPort + + /// public error codes for LambdaRuntime + case moreThanOneLambdaRuntimeInstance } package init(code: Code, underlying: (any Error)? = nil) { @@ -39,7 +44,7 @@ package struct LambdaRuntimeClientError: Error { self.underlying = underlying } - package var code: Code - package var underlying: (any Error)? + public var code: Code + public var underlying: (any Error)? } diff --git a/Tests/AWSLambdaRuntimeTests/LambdaRuntimeTests.swift b/Tests/AWSLambdaRuntimeTests/LambdaRuntimeTests.swift index 12cf4ae0..c5647e7e 100644 --- a/Tests/AWSLambdaRuntimeTests/LambdaRuntimeTests.swift +++ b/Tests/AWSLambdaRuntimeTests/LambdaRuntimeTests.swift @@ -23,21 +23,52 @@ import Testing @Suite("LambdaRuntimeTests") final class LambdaRuntimeTests { - @Test("LambdaRuntime can only be initialized once") - func testLambdaRuntimeInitializationFatalError() throws { - - // First initialization should succeed - try _ = LambdaRuntime(handler: MockHandler(), eventLoop: Lambda.defaultEventLoop, logger: Logger(label: "Test")) - - // Second initialization should trigger LambdaRuntimeError - #expect(throws: LambdaRuntimeError.self) { - try _ = LambdaRuntime( - handler: MockHandler(), - eventLoop: Lambda.defaultEventLoop, - logger: Logger(label: "Test") - ) + @Test("LambdaRuntime can only be run once") + func testLambdaRuntimerunOnce() async throws { + + // First runtime + let runtime1 = LambdaRuntime( + handler: MockHandler(), + eventLoop: Lambda.defaultEventLoop, + logger: Logger(label: "Runtime1") + ) + + // Second runtime + let runtime2 = LambdaRuntime( + handler: MockHandler(), + eventLoop: Lambda.defaultEventLoop, + logger: Logger(label: "Runtime1") + ) + + // start the first runtime + let task1 = Task { + try await runtime1.run() + } + + // wait a small amount to ensure runtime1 task is started + try await Task.sleep(for: .seconds(1)) + + // Running the second runtime should trigger LambdaRuntimeError + await #expect(throws: LambdaRuntimeError.self) { + try await runtime2.run() } + // cancel runtime 1 / task 1 + print("--- cancelling ---") + task1.cancel() + + // Running the second runtime should work now + await #expect(throws: Never.self) { + + let nonReturningTask = Task.detached(priority: .userInitiated) { + try await runtime2.run() + } + + // Set timeout and cancel the runtime 2 + try await Task.sleep(for: .seconds(2)) + nonReturningTask.cancel() + + } } } From 0509eaa1920cc77ecfd258d845d1cf11e28c1b65 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A9bastien=20Stormacq?= Date: Wed, 19 Mar 2025 20:43:04 +0100 Subject: [PATCH 10/23] revert `try` on `runtime.init()` in doc --- readme.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/readme.md b/readme.md index 3c42cff8..37596ed2 100644 --- a/readme.md +++ b/readme.md @@ -248,7 +248,7 @@ struct SendNumbersWithPause: StreamingLambdaHandler { } } -let runtime = try LambdaRuntime.init(handler: SendNumbersWithPause()) +let runtime = LambdaRuntime.init(handler: SendNumbersWithPause()) try await runtime.run() ``` @@ -328,7 +328,7 @@ struct BackgroundProcessingHandler: LambdaWithBackgroundProcessingHandler { } let adapter = LambdaCodableAdapter(handler: BackgroundProcessingHandler()) -let runtime = try LambdaRuntime.init(handler: adapter) +let runtime = LambdaRuntime.init(handler: adapter) try await runtime.run() ``` From aa6395fd1eaa98591c87ccd704aba74bd2861170 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A9bastien=20Stormacq?= Date: Wed, 19 Mar 2025 20:43:54 +0100 Subject: [PATCH 11/23] revert unwanted change --- Sources/AWSLambdaRuntime/ControlPlaneRequest.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Sources/AWSLambdaRuntime/ControlPlaneRequest.swift b/Sources/AWSLambdaRuntime/ControlPlaneRequest.swift index b3ce3d52..29016b0e 100644 --- a/Sources/AWSLambdaRuntime/ControlPlaneRequest.swift +++ b/Sources/AWSLambdaRuntime/ControlPlaneRequest.swift @@ -37,7 +37,7 @@ package struct InvocationMetadata: Hashable { package let cognitoIdentity: String? package init(headers: HTTPHeaders) throws(LambdaRuntimeError) { - guard let requestID: String = headers.first(name: AmazonHeaders.requestID), !requestID.isEmpty else { + guard let requestID = headers.first(name: AmazonHeaders.requestID), !requestID.isEmpty else { throw LambdaRuntimeError(code: .nextInvocationMissingHeaderRequestID) } From 066ab76f3c8a0d8e81c2389660aafb8cc82f20e1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A9bastien=20Stormacq?= Date: Wed, 19 Mar 2025 20:44:30 +0100 Subject: [PATCH 12/23] revert unwanted change --- Sources/AWSLambdaRuntime/LambdaHandlers.swift | 2 ++ 1 file changed, 2 insertions(+) diff --git a/Sources/AWSLambdaRuntime/LambdaHandlers.swift b/Sources/AWSLambdaRuntime/LambdaHandlers.swift index 2bb78cf3..89edad2b 100644 --- a/Sources/AWSLambdaRuntime/LambdaHandlers.swift +++ b/Sources/AWSLambdaRuntime/LambdaHandlers.swift @@ -213,6 +213,7 @@ extension LambdaRuntime { decoder: decoder, handler: streamingAdapter ) + self.init(handler: codableWrapper) } @@ -237,6 +238,7 @@ extension LambdaRuntime { decoder: decoder, handler: LambdaHandlerAdapter(handler: ClosureHandler(body: body)) ) + self.init(handler: handler) } } From e01b25d03b2538149493854f56e09df1cecd86fe Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A9bastien=20Stormacq?= Date: Wed, 19 Mar 2025 20:45:11 +0100 Subject: [PATCH 13/23] swift-format --- Sources/AWSLambdaRuntime/LambdaHandlers.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Sources/AWSLambdaRuntime/LambdaHandlers.swift b/Sources/AWSLambdaRuntime/LambdaHandlers.swift index 89edad2b..d6e0b373 100644 --- a/Sources/AWSLambdaRuntime/LambdaHandlers.swift +++ b/Sources/AWSLambdaRuntime/LambdaHandlers.swift @@ -238,7 +238,7 @@ extension LambdaRuntime { decoder: decoder, handler: LambdaHandlerAdapter(handler: ClosureHandler(body: body)) ) - + self.init(handler: handler) } } From 57e23961df69958e96d63df99fe46c74ae84bd2f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A9bastien=20Stormacq?= Date: Fri, 21 Mar 2025 07:54:13 +0100 Subject: [PATCH 14/23] Update Sources/AWSLambdaRuntime/LambdaRuntime.swift Co-authored-by: Fabian Fett --- Sources/AWSLambdaRuntime/LambdaRuntime.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Sources/AWSLambdaRuntime/LambdaRuntime.swift b/Sources/AWSLambdaRuntime/LambdaRuntime.swift index c70d28b2..59b3fefd 100644 --- a/Sources/AWSLambdaRuntime/LambdaRuntime.swift +++ b/Sources/AWSLambdaRuntime/LambdaRuntime.swift @@ -57,7 +57,7 @@ public final class LambdaRuntime: @unchecked Sendable where Handler: St public 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: .relaxed) + let (_, original) = _isRunning.compareExchange(expected: false, desired: true, ordering: .acquiringAndReleasing) // if the original value was already true, run() is already running if original { From 85c988db592b3da3c93f3f8eb9332a9cff2aa926 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A9bastien=20Stormacq?= Date: Fri, 21 Mar 2025 07:54:37 +0100 Subject: [PATCH 15/23] Update Sources/AWSLambdaRuntime/LambdaRuntime.swift Co-authored-by: Fabian Fett --- Sources/AWSLambdaRuntime/LambdaRuntime.swift | 23 +++++--------------- 1 file changed, 6 insertions(+), 17 deletions(-) diff --git a/Sources/AWSLambdaRuntime/LambdaRuntime.swift b/Sources/AWSLambdaRuntime/LambdaRuntime.swift index 59b3fefd..8d11e6d4 100644 --- a/Sources/AWSLambdaRuntime/LambdaRuntime.swift +++ b/Sources/AWSLambdaRuntime/LambdaRuntime.swift @@ -61,25 +61,14 @@ public final class LambdaRuntime: @unchecked Sendable where Handler: St // if the original value was already true, run() is already running if original { - throw LambdaRuntimeError(code: .runtimeCanOnlyBeStartedOnce) + throw LambdaRuntimeError(code: .moreThanOneLambdaRuntimeInstance) } - - try await withTaskCancellationHandler { - // call the internal _run() method - do { - try await self._run() - } catch { - // when we catch an error, flip back the global variable to false - _isRunning.store(false, ordering: .relaxed) - throw error - } - } onCancel: { - // when task is cancelled, flip back the global variable to false - _isRunning.store(false, ordering: .relaxed) + + defer { + _isRunning.store(false, ordering: . releasing) } - - // when we're done without error and without cancellation, flip back the global variable to false - _isRunning.store(false, ordering: .relaxed) + + try await self._run() } private func _run() async throws { From ab80580af6a80ba5b8257fb28aa23d38dfa3c06f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A9bastien=20Stormacq?= Date: Fri, 21 Mar 2025 07:54:59 +0100 Subject: [PATCH 16/23] Update Sources/AWSLambdaRuntime/LambdaRuntimeError.swift Co-authored-by: Fabian Fett --- Sources/AWSLambdaRuntime/LambdaRuntimeError.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Sources/AWSLambdaRuntime/LambdaRuntimeError.swift b/Sources/AWSLambdaRuntime/LambdaRuntimeError.swift index 1b9cfe02..0d2d194d 100644 --- a/Sources/AWSLambdaRuntime/LambdaRuntimeError.swift +++ b/Sources/AWSLambdaRuntime/LambdaRuntimeError.swift @@ -13,7 +13,7 @@ //===----------------------------------------------------------------------===// public struct LambdaRuntimeError: Error { - public enum Code: Sendable { + enum Code: Sendable { /// internal error codes for LambdaRuntimeClient case closingRuntimeClient From 10304cec2d3dfaeea5a261e713c87612ea176d74 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A9bastien=20Stormacq?= Date: Fri, 21 Mar 2025 07:55:09 +0100 Subject: [PATCH 17/23] Update Sources/AWSLambdaRuntime/LambdaRuntimeError.swift Co-authored-by: Fabian Fett --- Sources/AWSLambdaRuntime/LambdaRuntimeError.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Sources/AWSLambdaRuntime/LambdaRuntimeError.swift b/Sources/AWSLambdaRuntime/LambdaRuntimeError.swift index 0d2d194d..bca1eed2 100644 --- a/Sources/AWSLambdaRuntime/LambdaRuntimeError.swift +++ b/Sources/AWSLambdaRuntime/LambdaRuntimeError.swift @@ -44,7 +44,7 @@ public struct LambdaRuntimeError: Error { self.underlying = underlying } - public var code: Code + var code: Code public var underlying: (any Error)? } From c1e68fca423a08fef725b49a5e0fa302df9fcc24 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A9bastien=20Stormacq?= Date: Fri, 21 Mar 2025 07:55:21 +0100 Subject: [PATCH 18/23] Update Tests/AWSLambdaRuntimeTests/LambdaRuntimeTests.swift Co-authored-by: Fabian Fett --- Tests/AWSLambdaRuntimeTests/LambdaRuntimeTests.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Tests/AWSLambdaRuntimeTests/LambdaRuntimeTests.swift b/Tests/AWSLambdaRuntimeTests/LambdaRuntimeTests.swift index c5647e7e..662a0a19 100644 --- a/Tests/AWSLambdaRuntimeTests/LambdaRuntimeTests.swift +++ b/Tests/AWSLambdaRuntimeTests/LambdaRuntimeTests.swift @@ -21,7 +21,7 @@ import Testing @testable import AWSLambdaRuntime @Suite("LambdaRuntimeTests") -final class LambdaRuntimeTests { +struct LambdaRuntimeTests { @Test("LambdaRuntime can only be run once") func testLambdaRuntimerunOnce() async throws { From 918492a9910cd8be6bdc93c51e8a2a7530ba93f4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A9bastien=20Stormacq?= Date: Fri, 21 Mar 2025 07:56:09 +0100 Subject: [PATCH 19/23] Update Tests/AWSLambdaRuntimeTests/LambdaRuntimeTests.swift Co-authored-by: Fabian Fett --- .../LambdaRuntimeTests.swift | 32 +++++++++++-------- 1 file changed, 18 insertions(+), 14 deletions(-) diff --git a/Tests/AWSLambdaRuntimeTests/LambdaRuntimeTests.swift b/Tests/AWSLambdaRuntimeTests/LambdaRuntimeTests.swift index 662a0a19..b9dd3288 100644 --- a/Tests/AWSLambdaRuntimeTests/LambdaRuntimeTests.swift +++ b/Tests/AWSLambdaRuntimeTests/LambdaRuntimeTests.swift @@ -40,23 +40,27 @@ struct LambdaRuntimeTests { logger: Logger(label: "Runtime1") ) - // start the first runtime - let task1 = Task { - try await runtime1.run() - } - - // wait a small amount to ensure runtime1 task is started - try await Task.sleep(for: .seconds(1)) + try await withThrowingTaskGroup(of: Void.self) { taskGroup in + // start the first runtime + taskGroup.addTask { + await #expect(throws: Never.self) { + try await runtime1.run() + } + } + + // wait a small amount to ensure runtime1 task is started + try await Task.sleep(for: .seconds(1)) - // Running the second runtime should trigger LambdaRuntimeError - await #expect(throws: LambdaRuntimeError.self) { - try await runtime2.run() + // Running the second runtime should trigger LambdaRuntimeError + await #expect(throws: LambdaRuntimeError.self) { + try await runtime2.run() + } + + // cancel runtime 1 / task 1 + print("--- cancelling ---") + taskGroup.cancelAll() } - // cancel runtime 1 / task 1 - print("--- cancelling ---") - task1.cancel() - // Running the second runtime should work now await #expect(throws: Never.self) { From 6ada0414b92b11b0f13b79d7fa577d610b7510f7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A9bastien=20Stormacq?= Date: Fri, 21 Mar 2025 07:56:44 +0100 Subject: [PATCH 20/23] Update Tests/AWSLambdaRuntimeTests/LambdaRuntimeTests.swift Co-authored-by: Fabian Fett --- Tests/AWSLambdaRuntimeTests/LambdaRuntimeTests.swift | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/Tests/AWSLambdaRuntimeTests/LambdaRuntimeTests.swift b/Tests/AWSLambdaRuntimeTests/LambdaRuntimeTests.swift index b9dd3288..b7a3f6cb 100644 --- a/Tests/AWSLambdaRuntimeTests/LambdaRuntimeTests.swift +++ b/Tests/AWSLambdaRuntimeTests/LambdaRuntimeTests.swift @@ -62,16 +62,14 @@ struct LambdaRuntimeTests { } // Running the second runtime should work now - await #expect(throws: Never.self) { - - let nonReturningTask = Task.detached(priority: .userInitiated) { - try await runtime2.run() + try await withThrowingTaskGroup(of: Void.self) { taskGroup in + taskGroup.addTask { + await #expect(throws: Never.self) { try await runtime2.run() } } // Set timeout and cancel the runtime 2 try await Task.sleep(for: .seconds(2)) - nonReturningTask.cancel() - + taskGroup.cancelAll() } } } From 06df8316a7176d56da17748f18cd44b0e9de693d Mon Sep 17 00:00:00 2001 From: Sebastien Stormacq Date: Fri, 21 Mar 2025 08:09:43 +0100 Subject: [PATCH 21/23] add package visibility to Code --- Sources/AWSLambdaRuntime/LambdaRuntimeError.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Sources/AWSLambdaRuntime/LambdaRuntimeError.swift b/Sources/AWSLambdaRuntime/LambdaRuntimeError.swift index bca1eed2..c4166731 100644 --- a/Sources/AWSLambdaRuntime/LambdaRuntimeError.swift +++ b/Sources/AWSLambdaRuntime/LambdaRuntimeError.swift @@ -13,7 +13,7 @@ //===----------------------------------------------------------------------===// public struct LambdaRuntimeError: Error { - enum Code: Sendable { + package enum Code: Sendable { /// internal error codes for LambdaRuntimeClient case closingRuntimeClient From 50b9df8a73d0f6df8f23053111d6ea87e1323ac7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A9bastien=20Stormacq?= Date: Mon, 24 Mar 2025 09:11:20 +0100 Subject: [PATCH 22/23] swift format --- Sources/AWSLambdaRuntime/LambdaRuntime.swift | 6 +++--- Tests/AWSLambdaRuntimeTests/LambdaRuntimeTests.swift | 4 ++-- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/Sources/AWSLambdaRuntime/LambdaRuntime.swift b/Sources/AWSLambdaRuntime/LambdaRuntime.swift index 8d11e6d4..b0ebe0ca 100644 --- a/Sources/AWSLambdaRuntime/LambdaRuntime.swift +++ b/Sources/AWSLambdaRuntime/LambdaRuntime.swift @@ -63,11 +63,11 @@ public final class LambdaRuntime: @unchecked Sendable where Handler: St if original { throw LambdaRuntimeError(code: .moreThanOneLambdaRuntimeInstance) } - + defer { - _isRunning.store(false, ordering: . releasing) + _isRunning.store(false, ordering: .releasing) } - + try await self._run() } diff --git a/Tests/AWSLambdaRuntimeTests/LambdaRuntimeTests.swift b/Tests/AWSLambdaRuntimeTests/LambdaRuntimeTests.swift index b7a3f6cb..d5a5bec9 100644 --- a/Tests/AWSLambdaRuntimeTests/LambdaRuntimeTests.swift +++ b/Tests/AWSLambdaRuntimeTests/LambdaRuntimeTests.swift @@ -47,7 +47,7 @@ struct LambdaRuntimeTests { try await runtime1.run() } } - + // wait a small amount to ensure runtime1 task is started try await Task.sleep(for: .seconds(1)) @@ -55,7 +55,7 @@ struct LambdaRuntimeTests { await #expect(throws: LambdaRuntimeError.self) { try await runtime2.run() } - + // cancel runtime 1 / task 1 print("--- cancelling ---") taskGroup.cancelAll() From fa0fd88315e216d830586165a29ed754d08211b3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A9bastien=20Stormacq?= Date: Mon, 24 Mar 2025 09:12:59 +0100 Subject: [PATCH 23/23] remove print statement --- Tests/AWSLambdaRuntimeTests/LambdaRuntimeTests.swift | 1 - 1 file changed, 1 deletion(-) diff --git a/Tests/AWSLambdaRuntimeTests/LambdaRuntimeTests.swift b/Tests/AWSLambdaRuntimeTests/LambdaRuntimeTests.swift index d5a5bec9..bd3ca6d3 100644 --- a/Tests/AWSLambdaRuntimeTests/LambdaRuntimeTests.swift +++ b/Tests/AWSLambdaRuntimeTests/LambdaRuntimeTests.swift @@ -57,7 +57,6 @@ struct LambdaRuntimeTests { } // cancel runtime 1 / task 1 - print("--- cancelling ---") taskGroup.cancelAll() }