Skip to content

Commit 456569a

Browse files
committed
make LambdaRuntime a singleton without breaking the API
1 parent eb634fa commit 456569a

File tree

7 files changed

+110
-31
lines changed

7 files changed

+110
-31
lines changed

Sources/AWSLambdaRuntime/ControlPlaneRequest.swift

+4-4
Original file line numberDiff line numberDiff line change
@@ -36,19 +36,19 @@ package struct InvocationMetadata: Hashable {
3636
package let clientContext: String?
3737
package let cognitoIdentity: String?
3838

39-
package init(headers: HTTPHeaders) throws(LambdaRuntimeError) {
39+
package init(headers: HTTPHeaders) throws(LambdaRuntimeClientError) {
4040
guard let requestID = headers.first(name: AmazonHeaders.requestID), !requestID.isEmpty else {
41-
throw LambdaRuntimeError(code: .nextInvocationMissingHeaderRequestID)
41+
throw LambdaRuntimeClientError(code: .nextInvocationMissingHeaderRequestID)
4242
}
4343

4444
guard let deadline = headers.first(name: AmazonHeaders.deadline),
4545
let unixTimeInMilliseconds = Int64(deadline)
4646
else {
47-
throw LambdaRuntimeError(code: .nextInvocationMissingHeaderDeadline)
47+
throw LambdaRuntimeClientError(code: .nextInvocationMissingHeaderDeadline)
4848
}
4949

5050
guard let invokedFunctionARN = headers.first(name: AmazonHeaders.invokedFunctionARN) else {
51-
throw LambdaRuntimeError(code: .nextInvocationMissingHeaderInvokeFuctionARN)
51+
throw LambdaRuntimeClientError(code: .nextInvocationMissingHeaderInvokeFuctionARN)
5252
}
5353

5454
self.requestID = requestID

Sources/AWSLambdaRuntime/FoundationSupport/Lambda+JSON.swift

+10-2
Original file line numberDiff line numberDiff line change
@@ -108,7 +108,11 @@ extension LambdaRuntime {
108108
handler: LambdaHandlerAdapter(handler: ClosureHandler(body: body))
109109
)
110110

111-
self.init(handler: handler)
111+
do {
112+
try self.init(handler: handler)
113+
} catch {
114+
fatalError("Failed to initialize LambdaRuntime: \(error)")
115+
}
112116
}
113117

114118
/// Initialize an instance with a `LambdaHandler` defined in the form of a closure **with a `Void` return type**.
@@ -132,7 +136,11 @@ extension LambdaRuntime {
132136
handler: LambdaHandlerAdapter(handler: ClosureHandler(body: body))
133137
)
134138

135-
self.init(handler: handler)
139+
do {
140+
try self.init(handler: handler)
141+
} catch {
142+
fatalError("Failed to initialize LambdaRuntime: \(error)")
143+
}
136144
}
137145
}
138146
#endif // trait: FoundationJSONSupport

Sources/AWSLambdaRuntime/LambdaHandlers.swift

+15-5
Original file line numberDiff line numberDiff line change
@@ -179,7 +179,11 @@ extension LambdaRuntime {
179179
public convenience init(
180180
body: @Sendable @escaping (ByteBuffer, LambdaResponseStreamWriter, LambdaContext) async throws -> Void
181181
) where Handler == StreamingClosureHandler {
182-
self.init(handler: StreamingClosureHandler(body: body))
182+
do {
183+
try self.init(handler: StreamingClosureHandler(body: body))
184+
} catch {
185+
fatalError("Failed to initialize LambdaRuntime: \(error)")
186+
}
183187
}
184188

185189
/// 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 {
213217
decoder: decoder,
214218
handler: streamingAdapter
215219
)
216-
217-
self.init(handler: codableWrapper)
220+
do {
221+
try self.init(handler: codableWrapper)
222+
} catch {
223+
fatalError("Failed to initialize LambdaRuntime: \(error)")
224+
}
218225
}
219226

220227
/// 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 {
238245
decoder: decoder,
239246
handler: LambdaHandlerAdapter(handler: ClosureHandler(body: body))
240247
)
241-
242-
self.init(handler: handler)
248+
do {
249+
try self.init(handler: handler)
250+
} catch {
251+
fatalError("Failed to initialize LambdaRuntime: \(error)")
252+
}
243253
}
244254
}

Sources/AWSLambdaRuntime/LambdaRuntime.swift

+26-3
Original file line numberDiff line numberDiff line change
@@ -15,13 +15,21 @@
1515
import Logging
1616
import NIOConcurrencyHelpers
1717
import NIOCore
18+
import Synchronization
1819

1920
#if canImport(FoundationEssentials)
2021
import FoundationEssentials
2122
#else
2223
import Foundation
2324
#endif
2425

26+
// This is our gardian to ensure only one LambdaRuntime is initialized
27+
// We use a Mutex here to ensure thread safety
28+
// We don't use LambdaRuntime<> as the type here, as we don't know the concrete type that will be used
29+
private let _singleton = Mutex<Bool>(false)
30+
public enum LambdaRuntimeError: Error {
31+
case moreThanOneLambdaRuntimeInstance
32+
}
2533
// We need `@unchecked` Sendable here, as `NIOLockedValueBox` does not understand `sending` today.
2634
// We don't want to use `NIOLockedValueBox` here anyway. We would love to use Mutex here, but this
2735
// sadly crashes the compiler today.
@@ -35,7 +43,22 @@ public final class LambdaRuntime<Handler>: @unchecked Sendable where Handler: St
3543
handler: sending Handler,
3644
eventLoop: EventLoop = Lambda.defaultEventLoop,
3745
logger: Logger = Logger(label: "LambdaRuntime")
38-
) {
46+
) throws(LambdaRuntimeError) {
47+
48+
do {
49+
try _singleton.withLock {
50+
let alreadyCreated = $0
51+
guard alreadyCreated == false else {
52+
throw LambdaRuntimeError.moreThanOneLambdaRuntimeInstance
53+
}
54+
$0 = true
55+
}
56+
} catch _ as LambdaRuntimeError {
57+
throw LambdaRuntimeError.moreThanOneLambdaRuntimeInstance
58+
} catch {
59+
fatalError("An unknown error occurred: \(error)")
60+
}
61+
3962
self.handlerMutex = NIOLockedValueBox(handler)
4063
self.eventLoop = eventLoop
4164

@@ -56,7 +79,7 @@ public final class LambdaRuntime<Handler>: @unchecked Sendable where Handler: St
5679
}
5780

5881
guard let handler else {
59-
throw LambdaRuntimeError(code: .runtimeCanOnlyBeStartedOnce)
82+
throw LambdaRuntimeClientError(code: .runtimeCanOnlyBeStartedOnce)
6083
}
6184

6285
// are we running inside an AWS Lambda runtime environment ?
@@ -66,7 +89,7 @@ public final class LambdaRuntime<Handler>: @unchecked Sendable where Handler: St
6689

6790
let ipAndPort = runtimeEndpoint.split(separator: ":", maxSplits: 1)
6891
let ip = String(ipAndPort[0])
69-
guard let port = Int(ipAndPort[1]) else { throw LambdaRuntimeError(code: .invalidPort) }
92+
guard let port = Int(ipAndPort[1]) else { throw LambdaRuntimeClientError(code: .invalidPort) }
7093

7194
try await LambdaRuntimeClient.withRuntimeClient(
7295
configuration: .init(ip: ip, port: port),

Sources/AWSLambdaRuntime/LambdaRuntimeClient.swift

+16-16
Original file line numberDiff line numberDiff line change
@@ -134,7 +134,7 @@ final actor LambdaRuntimeClient: LambdaRuntimeClientProtocol {
134134

135135
case .connecting(let continuations):
136136
for continuation in continuations {
137-
continuation.resume(throwing: LambdaRuntimeError(code: .closingRuntimeClient))
137+
continuation.resume(throwing: LambdaRuntimeClientError(code: .closingRuntimeClient))
138138
}
139139
self.connectionState = .connecting([])
140140

@@ -173,7 +173,7 @@ final actor LambdaRuntimeClient: LambdaRuntimeClientProtocol {
173173
private func write(_ buffer: NIOCore.ByteBuffer) async throws {
174174
switch self.lambdaState {
175175
case .idle, .sentResponse:
176-
throw LambdaRuntimeError(code: .writeAfterFinishHasBeenSent)
176+
throw LambdaRuntimeClientError(code: .writeAfterFinishHasBeenSent)
177177

178178
case .waitingForNextInvocation:
179179
fatalError("Invalid state: \(self.lambdaState)")
@@ -194,7 +194,7 @@ final actor LambdaRuntimeClient: LambdaRuntimeClientProtocol {
194194
private func writeAndFinish(_ buffer: NIOCore.ByteBuffer?) async throws {
195195
switch self.lambdaState {
196196
case .idle, .sentResponse:
197-
throw LambdaRuntimeError(code: .finishAfterFinishHasBeenSent)
197+
throw LambdaRuntimeClientError(code: .finishAfterFinishHasBeenSent)
198198

199199
case .waitingForNextInvocation:
200200
fatalError("Invalid state: \(self.lambdaState)")
@@ -261,7 +261,7 @@ final actor LambdaRuntimeClient: LambdaRuntimeClientProtocol {
261261
case (.connecting(let array), .notClosing):
262262
self.connectionState = .disconnected
263263
for continuation in array {
264-
continuation.resume(throwing: LambdaRuntimeError(code: .lostConnectionToControlPlane))
264+
continuation.resume(throwing: LambdaRuntimeClientError(code: .lostConnectionToControlPlane))
265265
}
266266

267267
case (.connecting(let array), .closing(let continuation)):
@@ -394,7 +394,7 @@ extension LambdaRuntimeClient: LambdaChannelHandlerDelegate {
394394
}
395395

396396
for continuation in continuations {
397-
continuation.resume(throwing: LambdaRuntimeError(code: .connectionToControlPlaneLost))
397+
continuation.resume(throwing: LambdaRuntimeClientError(code: .connectionToControlPlaneLost))
398398
}
399399

400400
case .connected(let stateChannel, _):
@@ -489,7 +489,7 @@ private final class LambdaChannelHandler<Delegate: LambdaChannelHandlerDelegate>
489489
fatalError("Invalid state: \(self.state)")
490490

491491
case .disconnected:
492-
throw LambdaRuntimeError(code: .connectionToControlPlaneLost)
492+
throw LambdaRuntimeClientError(code: .connectionToControlPlaneLost)
493493
}
494494
}
495495

@@ -528,10 +528,10 @@ private final class LambdaChannelHandler<Delegate: LambdaChannelHandlerDelegate>
528528
)
529529

530530
case .disconnected:
531-
throw LambdaRuntimeError(code: .connectionToControlPlaneLost)
531+
throw LambdaRuntimeClientError(code: .connectionToControlPlaneLost)
532532

533533
case .closing:
534-
throw LambdaRuntimeError(code: .connectionToControlPlaneGoingAway)
534+
throw LambdaRuntimeClientError(code: .connectionToControlPlaneGoingAway)
535535
}
536536
}
537537

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

554554
case .connected(_, .idle),
555555
.connected(_, .sentResponse):
556-
throw LambdaRuntimeError(code: .writeAfterFinishHasBeenSent)
556+
throw LambdaRuntimeClientError(code: .writeAfterFinishHasBeenSent)
557557

558558
case .disconnected:
559-
throw LambdaRuntimeError(code: .connectionToControlPlaneLost)
559+
throw LambdaRuntimeClientError(code: .connectionToControlPlaneLost)
560560

561561
case .closing:
562-
throw LambdaRuntimeError(code: .connectionToControlPlaneGoingAway)
562+
throw LambdaRuntimeClientError(code: .connectionToControlPlaneGoingAway)
563563
}
564564
}
565565

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

588588
case .connected(_, .sentResponse):
589-
throw LambdaRuntimeError(code: .finishAfterFinishHasBeenSent)
589+
throw LambdaRuntimeClientError(code: .finishAfterFinishHasBeenSent)
590590

591591
case .disconnected:
592-
throw LambdaRuntimeError(code: .connectionToControlPlaneLost)
592+
throw LambdaRuntimeClientError(code: .connectionToControlPlaneLost)
593593

594594
case .closing:
595-
throw LambdaRuntimeError(code: .connectionToControlPlaneGoingAway)
595+
throw LambdaRuntimeClientError(code: .connectionToControlPlaneGoingAway)
596596
}
597597
}
598598

@@ -759,7 +759,7 @@ extension LambdaChannelHandler: ChannelInboundHandler {
759759
self.delegate.connectionWillClose(channel: context.channel)
760760
context.close(promise: nil)
761761
continuation.resume(
762-
throwing: LambdaRuntimeError(code: .invocationMissingMetadata, underlying: error)
762+
throwing: LambdaRuntimeClientError(code: .invocationMissingMetadata, underlying: error)
763763
)
764764
}
765765

@@ -769,7 +769,7 @@ extension LambdaChannelHandler: ChannelInboundHandler {
769769
continuation.resume()
770770
} else {
771771
self.state = .connected(context, .idle)
772-
continuation.resume(throwing: LambdaRuntimeError(code: .unexpectedStatusCodeForRequest))
772+
continuation.resume(throwing: LambdaRuntimeClientError(code: .unexpectedStatusCodeForRequest))
773773
}
774774

775775
case .disconnected, .closing, .connected(_, _):

Sources/AWSLambdaRuntime/LambdaRuntimeError.swift renamed to Sources/AWSLambdaRuntime/LambdaRuntimeClientError.swift

+1-1
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@
1212
//
1313
//===----------------------------------------------------------------------===//
1414

15-
package struct LambdaRuntimeError: Error {
15+
package struct LambdaRuntimeClientError: Error {
1616
package enum Code {
1717
case closingRuntimeClient
1818

Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
import Foundation
2+
import Logging
3+
import NIOCore
4+
import Synchronization
5+
import Testing
6+
7+
@testable import AWSLambdaRuntime
8+
9+
@Suite("LambdaRuntimeTests")
10+
final class LambdaRuntimeTests {
11+
12+
@Test("LambdaRuntime can only be initialized once")
13+
func testLambdaRuntimeInitializationFatalError() throws {
14+
15+
// First initialization should succeed
16+
try _ = LambdaRuntime(handler: MockHandler(), eventLoop: Lambda.defaultEventLoop, logger: Logger(label: "Test"))
17+
18+
// Second initialization should trigger LambdaRuntimeError
19+
#expect(throws: LambdaRuntimeError.self) {
20+
try _ = LambdaRuntime(
21+
handler: MockHandler(),
22+
eventLoop: Lambda.defaultEventLoop,
23+
logger: Logger(label: "Test")
24+
)
25+
}
26+
27+
}
28+
}
29+
30+
struct MockHandler: StreamingLambdaHandler {
31+
mutating func handle(
32+
_ event: NIOCore.ByteBuffer,
33+
responseWriter: some AWSLambdaRuntime.LambdaResponseStreamWriter,
34+
context: AWSLambdaRuntime.LambdaContext
35+
) async throws {
36+
37+
}
38+
}

0 commit comments

Comments
 (0)