Skip to content

Commit 6d858c2

Browse files
authored
Merge branch 'swift-server:main' into sebsto/servicelifecycle
2 parents a88de47 + 4a7d95e commit 6d858c2

File tree

11 files changed

+730
-61
lines changed

11 files changed

+730
-61
lines changed

Examples/HelloJSON/Package.swift

Lines changed: 1 addition & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -15,10 +15,7 @@ let package = Package(
1515
// during CI, the dependency on local version of swift-aws-lambda-runtime is added dynamically below
1616
.package(
1717
url: "https://github.com/swift-server/swift-aws-lambda-runtime.git",
18-
branch: "ff-package-traits",
19-
traits: [
20-
.trait(name: "FoundationJSONSupport")
21-
]
18+
branch: "main"
2219
)
2320
],
2421
targets: [

Plugins/Documentation.docc/Proposals/0001-v2-plugins.md

Lines changed: 531 additions & 0 deletions
Large diffs are not rendered by default.

Sources/AWSLambdaRuntime/FoundationSupport/Lambda+JSON.swift

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,8 @@ import class Foundation.JSONDecoder
2323
import class Foundation.JSONEncoder
2424
#endif
2525

26+
import Logging
27+
2628
public struct LambdaJSONEventDecoder: LambdaEventDecoder {
2729
@usableFromInline let jsonDecoder: JSONDecoder
2830

@@ -87,10 +89,12 @@ extension LambdaRuntime {
8789
/// - Parameters:
8890
/// - decoder: The decoder object that will be used to decode the incoming `ByteBuffer` event into the generic `Event` type. `JSONDecoder()` used as default.
8991
/// - encoder: The encoder object that will be used to encode the generic `Output` into a `ByteBuffer`. `JSONEncoder()` used as default.
92+
/// - logger: The logger to use for the runtime. Defaults to a logger with label "LambdaRuntime".
9093
/// - body: The handler in the form of a closure.
9194
public convenience init<Event: Decodable, Output>(
9295
decoder: JSONDecoder = JSONDecoder(),
9396
encoder: JSONEncoder = JSONEncoder(),
97+
logger: Logger = Logger(label: "LambdaRuntime"),
9498
body: sending @escaping (Event, LambdaContext) async throws -> Output
9599
)
96100
where
@@ -108,14 +112,16 @@ extension LambdaRuntime {
108112
handler: LambdaHandlerAdapter(handler: ClosureHandler(body: body))
109113
)
110114

111-
self.init(handler: handler)
115+
self.init(handler: handler, logger: logger)
112116
}
113117

114118
/// Initialize an instance with a `LambdaHandler` defined in the form of a closure **with a `Void` return type**.
115119
/// - Parameter body: The handler in the form of a closure.
116120
/// - Parameter decoder: The decoder object that will be used to decode the incoming `ByteBuffer` event into the generic `Event` type. `JSONDecoder()` used as default.
121+
/// - Parameter logger: The logger to use for the runtime. Defaults to a logger with label "LambdaRuntime".
117122
public convenience init<Event: Decodable>(
118123
decoder: JSONDecoder = JSONDecoder(),
124+
logger: Logger = Logger(label: "LambdaRuntime"),
119125
body: sending @escaping (Event, LambdaContext) async throws -> Void
120126
)
121127
where
@@ -132,7 +138,7 @@ extension LambdaRuntime {
132138
handler: LambdaHandlerAdapter(handler: ClosureHandler(body: body))
133139
)
134140

135-
self.init(handler: handler)
141+
self.init(handler: handler, logger: logger)
136142
}
137143
}
138144
#endif // trait: FoundationJSONSupport

Sources/AWSLambdaRuntime/Lambda+LocalServer.swift

Lines changed: 18 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -48,11 +48,9 @@ extension Lambda {
4848
@usableFromInline
4949
static func withLocalServer(
5050
invocationEndpoint: String? = nil,
51+
logger: Logger,
5152
_ body: sending @escaping () async throws -> Void
5253
) async throws {
53-
var logger = Logger(label: "LocalServer")
54-
logger.logLevel = Lambda.env("LOG_LEVEL").flatMap(Logger.Level.init) ?? .info
55-
5654
try await LambdaHTTPServer.withLocalServer(
5755
invocationEndpoint: invocationEndpoint,
5856
logger: logger
@@ -93,7 +91,7 @@ internal struct LambdaHTTPServer {
9391
case serverReturned(Swift.Result<Void, any Error>)
9492
}
9593

96-
struct UnsafeTransferBox<Value>: @unchecked Sendable {
94+
fileprivate struct UnsafeTransferBox<Value>: @unchecked Sendable {
9795
let value: Value
9896

9997
init(value: sending Value) {
@@ -133,6 +131,7 @@ internal struct LambdaHTTPServer {
133131
}
134132
}
135133

134+
// it's ok to keep this at `info` level because it is only used for local testing and unit tests
136135
logger.info(
137136
"Server started and listening",
138137
metadata: [
@@ -202,12 +201,18 @@ internal struct LambdaHTTPServer {
202201
return result
203202

204203
case .serverReturned(let result):
205-
logger.error(
206-
"Server shutdown before closure completed",
207-
metadata: [
208-
"error": "\(result.maybeError != nil ? "\(result.maybeError!)" : "none")"
209-
]
210-
)
204+
205+
if result.maybeError is CancellationError {
206+
logger.trace("Server's task cancelled")
207+
} else {
208+
logger.error(
209+
"Server shutdown before closure completed",
210+
metadata: [
211+
"error": "\(result.maybeError != nil ? "\(result.maybeError!)" : "none")"
212+
]
213+
)
214+
}
215+
211216
switch await group.next()! {
212217
case .closureResult(let result):
213218
return result
@@ -265,9 +270,12 @@ internal struct LambdaHTTPServer {
265270
}
266271
}
267272
}
273+
} catch let error as CancellationError {
274+
logger.trace("The task was cancelled", metadata: ["error": "\(error)"])
268275
} catch {
269276
logger.error("Hit error: \(error)")
270277
}
278+
271279
} onCancel: {
272280
channel.channel.close(promise: nil)
273281
}

Sources/AWSLambdaRuntime/LambdaHandlers.swift

Lines changed: 13 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@
1212
//
1313
//===----------------------------------------------------------------------===//
1414

15+
import Logging
1516
import NIOCore
1617

1718
/// The base handler protocol that receives a `ByteBuffer` representing the incoming event and returns the response as a `ByteBuffer` too.
@@ -175,17 +176,22 @@ public struct ClosureHandler<Event: Decodable, Output>: LambdaHandler {
175176

176177
extension LambdaRuntime {
177178
/// Initialize an instance with a ``StreamingLambdaHandler`` in the form of a closure.
178-
/// - Parameter body: The handler in the form of a closure.
179+
/// - Parameter
180+
/// - logger: The logger to use for the runtime. Defaults to a logger with label "LambdaRuntime".
181+
/// - body: The handler in the form of a closure.
179182
public convenience init(
183+
logger: Logger = Logger(label: "LambdaRuntime"),
180184
body: @Sendable @escaping (ByteBuffer, LambdaResponseStreamWriter, LambdaContext) async throws -> Void
185+
181186
) where Handler == StreamingClosureHandler {
182-
self.init(handler: StreamingClosureHandler(body: body))
187+
self.init(handler: StreamingClosureHandler(body: body), logger: logger)
183188
}
184189

185190
/// Initialize an instance with a ``LambdaHandler`` defined in the form of a closure **with a non-`Void` return type**, an encoder, and a decoder.
186191
/// - Parameters:
187192
/// - encoder: The encoder object that will be used to encode the generic `Output` into a `ByteBuffer`.
188193
/// - decoder: The decoder object that will be used to decode the incoming `ByteBuffer` event into the generic `Event` type.
194+
/// - logger: The logger to use for the runtime. Defaults to a logger with label "LambdaRuntime".
189195
/// - body: The handler in the form of a closure.
190196
public convenience init<
191197
Event: Decodable,
@@ -195,6 +201,7 @@ extension LambdaRuntime {
195201
>(
196202
encoder: sending Encoder,
197203
decoder: sending Decoder,
204+
logger: Logger = Logger(label: "LambdaRuntime"),
198205
body: sending @escaping (Event, LambdaContext) async throws -> Output
199206
)
200207
where
@@ -214,15 +221,17 @@ extension LambdaRuntime {
214221
handler: streamingAdapter
215222
)
216223

217-
self.init(handler: codableWrapper)
224+
self.init(handler: codableWrapper, logger: logger)
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.
221228
/// - Parameters:
222229
/// - decoder: The decoder object that will be used to decode the incoming `ByteBuffer` event into the generic `Event` type.
230+
/// - logger: The logger to use for the runtime. Defaults to a logger with label "LambdaRuntime".
223231
/// - body: The handler in the form of a closure.
224232
public convenience init<Event: Decodable, Decoder: LambdaEventDecoder>(
225233
decoder: sending Decoder,
234+
logger: Logger = Logger(label: "LambdaRuntime"),
226235
body: sending @escaping (Event, LambdaContext) async throws -> Void
227236
)
228237
where
@@ -239,6 +248,6 @@ extension LambdaRuntime {
239248
handler: LambdaHandlerAdapter(handler: ClosureHandler(body: body))
240249
)
241250

242-
self.init(handler: handler)
251+
self.init(handler: handler, logger: logger)
243252
}
244253
}

Sources/AWSLambdaRuntime/LambdaRuntime.swift

Lines changed: 33 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -13,22 +13,23 @@
1313
//===----------------------------------------------------------------------===//
1414

1515
import Logging
16-
import NIOConcurrencyHelpers
1716
import NIOCore
17+
import Synchronization
1818

1919
#if canImport(FoundationEssentials)
2020
import FoundationEssentials
2121
#else
2222
import 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,14 +40,18 @@ 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
4647
// developers have to wait for AWS Lambda to dispose and recreate a runtime environment to pickup a change
4748
// this approach is less flexible but more performant than reading the value of the environment variable at each invocation
4849
var log = logger
49-
log.logLevel = Lambda.env("LOG_LEVEL").flatMap(Logger.Level.init) ?? .info
50+
51+
// use the LOG_LEVEL environment variable to set the log level.
52+
// if the environment variable is not set, use the default log level from the logger provided
53+
log.logLevel = Lambda.env("LOG_LEVEL").flatMap(Logger.Level.init) ?? logger.logLevel
54+
5055
self.logger = log
5156
self.logger.debug("LambdaRuntime initialized")
5257
}
@@ -58,14 +63,24 @@ public final class LambdaRuntime<Handler>: @unchecked Sendable where Handler: St
5863
}
5964
#endif
6065

61-
@inlinable
66+
/// Make sure only one run() is called at a time
67+
// @inlinable
6268
internal func _run() async throws {
63-
let handler = self.handlerMutex.withLockedValue { handler in
64-
let result = handler
65-
handler = nil
66-
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)
6776
}
6877

78+
defer {
79+
_isRunning.store(false, ordering: .releasing)
80+
}
81+
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()
6984
guard let handler else {
7085
throw LambdaRuntimeError(code: .runtimeCanOnlyBeStartedOnce)
7186
}
@@ -96,8 +111,10 @@ public final class LambdaRuntime<Handler>: @unchecked Sendable where Handler: St
96111
#if LocalServerSupport
97112
// we're not running on Lambda and we're compiled in DEBUG mode,
98113
// let's start a local server for testing
99-
try await Lambda.withLocalServer(invocationEndpoint: Lambda.env("LOCAL_LAMBDA_SERVER_INVOCATION_ENDPOINT"))
100-
{
114+
try await Lambda.withLocalServer(
115+
invocationEndpoint: Lambda.env("LOCAL_LAMBDA_SERVER_INVOCATION_ENDPOINT"),
116+
logger: self.logger
117+
) {
101118

102119
try await LambdaRuntimeClient.withRuntimeClient(
103120
configuration: .init(ip: "127.0.0.1", port: 7000),

Sources/AWSLambdaRuntime/LambdaRuntimeError.swift

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@
1616
package struct LambdaRuntimeError: Error {
1717
@usableFromInline
1818
package enum Code: Sendable {
19+
/// internal error codes for LambdaRuntimeClient
1920
case closingRuntimeClient
2021

2122
case connectionToControlPlaneLost

Sources/AWSLambdaRuntime/Utils.swift

Lines changed: 34 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -13,8 +13,11 @@
1313
//===----------------------------------------------------------------------===//
1414

1515
import Dispatch
16+
import NIOConcurrencyHelpers
1617
import NIOPosix
1718

19+
// import Synchronization
20+
1821
enum Consts {
1922
static let apiPrefix = "/2018-06-01"
2023
static let invocationURLPrefix = "\(apiPrefix)/runtime/invocation"
@@ -36,28 +39,6 @@ enum AmazonHeaders {
3639
static let invokedFunctionARN = "Lambda-Runtime-Invoked-Function-Arn"
3740
}
3841

39-
/// Helper function to trap signals
40-
func trap(signal sig: Signal, handler: @escaping (Signal) -> Void) -> DispatchSourceSignal {
41-
let signalSource = DispatchSource.makeSignalSource(signal: sig.rawValue, queue: DispatchQueue.global())
42-
signal(sig.rawValue, SIG_IGN)
43-
signalSource.setEventHandler(handler: {
44-
signalSource.cancel()
45-
handler(sig)
46-
})
47-
signalSource.resume()
48-
return signalSource
49-
}
50-
51-
enum Signal: Int32 {
52-
case HUP = 1
53-
case INT = 2
54-
case QUIT = 3
55-
case ABRT = 6
56-
case KILL = 9 // ignore-unacceptable-language
57-
case ALRM = 14
58-
case TERM = 15
59-
}
60-
6142
extension DispatchWallTime {
6243
@usableFromInline
6344
init(millisSinceEpoch: Int64) {
@@ -132,3 +113,34 @@ extension AmazonHeaders {
132113
return "\(version)-\(datePadding)\(dateValue)-\(identifier)"
133114
}
134115
}
116+
117+
/// Temporary storage for value being sent from one isolation domain to another
118+
// use NIOLockedValueBox instead of Mutex to avoid compiler crashes on 6.0
119+
// see https://github.com/swiftlang/swift/issues/78048
120+
@usableFromInline
121+
struct SendingStorage<Value>: ~Copyable, @unchecked Sendable {
122+
@usableFromInline
123+
struct ValueAlreadySentError: Error {
124+
@usableFromInline
125+
init() {}
126+
}
127+
128+
@usableFromInline
129+
// let storage: Mutex<Value?>
130+
let storage: NIOLockedValueBox<Value?>
131+
132+
@inlinable
133+
init(_ value: sending Value) {
134+
self.storage = .init(value)
135+
}
136+
137+
@inlinable
138+
func get() throws -> Value {
139+
// try self.storage.withLock {
140+
try self.storage.withLockedValue {
141+
guard let value = $0 else { throw ValueAlreadySentError() }
142+
$0 = nil
143+
return value
144+
}
145+
}
146+
}

Sources/MockServer/MockHTTPServer.swift

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -123,6 +123,7 @@ struct HttpServer {
123123
}
124124
}
125125
}
126+
// it's ok to keep this at `info` level because it is only used for local testing and unit tests
126127
logger.info("Server shutting down")
127128
}
128129

Tests/AWSLambdaRuntimeTests/LambdaRuntimeClientTests.swift

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -147,12 +147,11 @@ struct LambdaRuntimeClientTests {
147147
(event: String, context: LambdaContext) in
148148
"Hello \(event)"
149149
}
150-
var logger = Logger(label: "LambdaRuntime")
151-
logger.logLevel = .debug
150+
152151
let serviceGroup = ServiceGroup(
153152
services: [runtime],
154153
gracefulShutdownSignals: [.sigterm, .sigint],
155-
logger: logger
154+
logger: Logger(label: "TestLambdaRuntimeGracefulShutdown")
156155
)
157156
try await withThrowingTaskGroup(of: Void.self) { group in
158157
group.addTask {

0 commit comments

Comments
 (0)