1313//===----------------------------------------------------------------------===//
1414
1515import Logging
16- import NIOConcurrencyHelpers
1716import NIOCore
17+ import Synchronization
1818
1919#if canImport(FoundationEssentials)
2020import FoundationEssentials
2121#else
2222import Foundation
2323#endif
2424
25- // We need `@unchecked` Sendable here, as `NIOLockedValueBox` does not understand `sending` today.
26- // We don't want to use `NIOLockedValueBox` here anyway. We would love to use Mutex here, but this
27- // sadly crashes the compiler today.
28- public final class LambdaRuntime < Handler > : @ unchecked Sendable where Handler : StreamingLambdaHandler {
29- // TODO: We want to change this to Mutex as soon as this doesn't crash the Swift compiler on Linux anymore
25+ // This is our guardian to ensure only one LambdaRuntime is running at the time
26+ // We use an Atomic here to ensure thread safety
27+ private let _isRunning = Atomic < Bool > ( false )
28+
29+ public final class LambdaRuntime < Handler > : Sendable where Handler : StreamingLambdaHandler {
3030 @usableFromInline
31- let handlerMutex : NIOLockedValueBox < Handler ? >
31+ /// we protect the handler behind a Mutex to ensure that we only ever have one copy of it
32+ let handlerStorage : SendingStorage < Handler >
3233 @usableFromInline
3334 let logger : Logger
3435 @usableFromInline
@@ -39,7 +40,7 @@ public final class LambdaRuntime<Handler>: @unchecked Sendable where Handler: St
3940 eventLoop: EventLoop = Lambda . defaultEventLoop,
4041 logger: Logger = Logger ( label: " LambdaRuntime " )
4142 ) {
42- self . handlerMutex = NIOLockedValueBox ( handler)
43+ self . handlerStorage = SendingStorage ( handler)
4344 self . eventLoop = eventLoop
4445
4546 // by setting the log level here, we understand it can not be changed dynamically at runtime
@@ -62,14 +63,24 @@ public final class LambdaRuntime<Handler>: @unchecked Sendable where Handler: St
6263 }
6364 #endif
6465
65- @inlinable
66+ /// Make sure only one run() is called at a time
67+ // @inlinable
6668 internal func _run( ) async throws {
67- let handler = self . handlerMutex. withLockedValue { handler in
68- let result = handler
69- handler = nil
70- return result
69+
70+ // we use an atomic global variable to ensure only one LambdaRuntime is running at the time
71+ let ( _, original) = _isRunning. compareExchange ( expected: false , desired: true , ordering: . acquiringAndReleasing)
72+
73+ // if the original value was already true, run() is already running
74+ if original {
75+ throw LambdaRuntimeError ( code: . runtimeCanOnlyBeStartedOnce)
76+ }
77+
78+ defer {
79+ _isRunning. store ( false , ordering: . releasing)
7180 }
7281
82+ // The handler can be non-sendable, we want to ensure we only ever have one copy of it
83+ let handler = try ? self . handlerStorage. get ( )
7384 guard let handler else {
7485 throw LambdaRuntimeError ( code: . runtimeCanOnlyBeStartedOnce)
7586 }
@@ -100,8 +111,10 @@ public final class LambdaRuntime<Handler>: @unchecked Sendable where Handler: St
100111 #if LocalServerSupport
101112 // we're not running on Lambda and we're compiled in DEBUG mode,
102113 // let's start a local server for testing
103- try await Lambda . withLocalServer ( invocationEndpoint: Lambda . env ( " LOCAL_LAMBDA_SERVER_INVOCATION_ENDPOINT " ) )
104- {
114+ try await Lambda . withLocalServer (
115+ invocationEndpoint: Lambda . env ( " LOCAL_LAMBDA_SERVER_INVOCATION_ENDPOINT " ) ,
116+ logger: self . logger
117+ ) {
105118
106119 try await LambdaRuntimeClient . withRuntimeClient (
107120 configuration: . init( ip: " 127.0.0.1 " , port: 7000 ) ,
0 commit comments