Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[core] Only one LambdaRuntime.run() can be called at a time (fix #507) #508

Open
wants to merge 26 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 8 commits
Commits
Show all changes
26 commits
Select commit Hold shift + click to select a range
456569a
make LambdaRuntime a singleton without breaking the API
sebsto Mar 14, 2025
a63a639
fix license header
sebsto Mar 14, 2025
40b6127
convert Mutex to NIOLockedValueBox
sebsto Mar 14, 2025
d474eba
Replace NIOLockedValueBox with Mutex
sebsto Mar 15, 2025
46e76f3
revert replacing NIOLockedValueBox by Mutex
sebsto Mar 15, 2025
299b9bc
remove typed throw (workaround for https://github.com/swiftlang/swift…
sebsto Mar 15, 2025
e3a3851
fix integration tests
sebsto Mar 15, 2025
4ebf24f
Replace NIOLockedValueBox with Mutex
sebsto Mar 15, 2025
f3e65ac
Merge branch 'main' into sebsto/fix_507
sebsto Mar 18, 2025
3aee427
use Atomic instead of Mutex.
sebsto Mar 19, 2025
e445f90
Merge branch 'sebsto/fix_507' of github.com:sebsto/swift-aws-lambda-r…
sebsto Mar 19, 2025
0509eaa
revert `try` on `runtime.init()` in doc
sebsto Mar 19, 2025
aa6395f
revert unwanted change
sebsto Mar 19, 2025
066ab76
revert unwanted change
sebsto Mar 19, 2025
e01b25d
swift-format
sebsto Mar 19, 2025
23c4b64
Merge branch 'main' into sebsto/fix_507
sebsto Mar 20, 2025
57e2396
Update Sources/AWSLambdaRuntime/LambdaRuntime.swift
sebsto Mar 21, 2025
85c988d
Update Sources/AWSLambdaRuntime/LambdaRuntime.swift
sebsto Mar 21, 2025
ab80580
Update Sources/AWSLambdaRuntime/LambdaRuntimeError.swift
sebsto Mar 21, 2025
10304ce
Update Sources/AWSLambdaRuntime/LambdaRuntimeError.swift
sebsto Mar 21, 2025
c1e68fc
Update Tests/AWSLambdaRuntimeTests/LambdaRuntimeTests.swift
sebsto Mar 21, 2025
918492a
Update Tests/AWSLambdaRuntimeTests/LambdaRuntimeTests.swift
sebsto Mar 21, 2025
6ada041
Update Tests/AWSLambdaRuntimeTests/LambdaRuntimeTests.swift
sebsto Mar 21, 2025
06df831
add package visibility to Code
sebsto Mar 21, 2025
50b9df8
swift format
sebsto Mar 24, 2025
fa0fd88
remove print statement
sebsto Mar 24, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion Examples/BackgroundTasks/Sources/main.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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()
2 changes: 1 addition & 1 deletion Examples/Streaming/Sources/main.swift
Original file line number Diff line number Diff line change
Expand Up @@ -32,5 +32,5 @@ struct SendNumbersWithPause: StreamingLambdaHandler {
}
}

let runtime = LambdaRuntime.init(handler: SendNumbersWithPause())
let runtime = try LambdaRuntime.init(handler: SendNumbersWithPause())
try await runtime.run()
8 changes: 4 additions & 4 deletions Sources/AWSLambdaRuntime/ControlPlaneRequest.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
12 changes: 10 additions & 2 deletions Sources/AWSLambdaRuntime/FoundationSupport/Lambda+JSON.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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**.
Expand All @@ -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
20 changes: 15 additions & 5 deletions Sources/AWSLambdaRuntime/LambdaHandlers.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down Expand Up @@ -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.
Expand All @@ -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)")
}
}
}
34 changes: 30 additions & 4 deletions Sources/AWSLambdaRuntime/LambdaRuntime.swift
Original file line number Diff line number Diff line change
Expand Up @@ -15,16 +15,24 @@
import Logging
import NIOConcurrencyHelpers
import NIOCore
import Synchronization

#if canImport(FoundationEssentials)
import FoundationEssentials
#else
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<Handler> as the type here, as we don't know the concrete type that will be used
private let _singleton = Mutex<Bool>(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<Handler>: @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<Handler?>
Expand All @@ -35,7 +43,25 @@ public final class LambdaRuntime<Handler>: @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

Expand All @@ -56,7 +82,7 @@ public final class LambdaRuntime<Handler>: @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 ?
Expand All @@ -66,7 +92,7 @@ public final class LambdaRuntime<Handler>: @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),
Expand Down
32 changes: 16 additions & 16 deletions Sources/AWSLambdaRuntime/LambdaRuntimeClient.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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([])

Expand Down Expand Up @@ -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)")
Expand All @@ -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)")
Expand Down Expand Up @@ -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)):
Expand Down Expand Up @@ -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, _):
Expand Down Expand Up @@ -489,7 +489,7 @@ private final class LambdaChannelHandler<Delegate: LambdaChannelHandlerDelegate>
fatalError("Invalid state: \(self.state)")

case .disconnected:
throw LambdaRuntimeError(code: .connectionToControlPlaneLost)
throw LambdaRuntimeClientError(code: .connectionToControlPlaneLost)
}
}

Expand Down Expand Up @@ -528,10 +528,10 @@ private final class LambdaChannelHandler<Delegate: LambdaChannelHandlerDelegate>
)

case .disconnected:
throw LambdaRuntimeError(code: .connectionToControlPlaneLost)
throw LambdaRuntimeClientError(code: .connectionToControlPlaneLost)

case .closing:
throw LambdaRuntimeError(code: .connectionToControlPlaneGoingAway)
throw LambdaRuntimeClientError(code: .connectionToControlPlaneGoingAway)
}
}

Expand All @@ -553,13 +553,13 @@ private final class LambdaChannelHandler<Delegate: LambdaChannelHandlerDelegate>

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)
}
}

Expand All @@ -586,13 +586,13 @@ private final class LambdaChannelHandler<Delegate: LambdaChannelHandlerDelegate>
}

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)
}
}

Expand Down Expand Up @@ -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)
)
}

Expand All @@ -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(_, _):
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@
//
//===----------------------------------------------------------------------===//

package struct LambdaRuntimeError: Error {
package struct LambdaRuntimeClientError: Error {
package enum Code {
case closingRuntimeClient

Expand Down
52 changes: 52 additions & 0 deletions Tests/AWSLambdaRuntimeTests/LambdaRuntimeTests.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
//===----------------------------------------------------------------------===//
//
// 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
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 {

}
}
4 changes: 2 additions & 2 deletions readme.md
Original file line number Diff line number Diff line change
Expand Up @@ -248,7 +248,7 @@ struct SendNumbersWithPause: StreamingLambdaHandler {
}
}

let runtime = LambdaRuntime.init(handler: SendNumbersWithPause())
let runtime = try LambdaRuntime.init(handler: SendNumbersWithPause())
try await runtime.run()
```

Expand Down Expand Up @@ -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()
```

Expand Down
Loading