Skip to content

Commit 551b3f4

Browse files
authored
Merge branch 'main' into sebsto/new-plugin-proposal
2 parents 26afac8 + de38324 commit 551b3f4

16 files changed

+586
-180
lines changed

.github/workflows/pull_request.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@ jobs:
2323
with:
2424
linux_5_9_enabled: false
2525
linux_5_10_enabled: false
26-
linux_nightly_6_0_arguments_override: "--explicit-target-dependency-import-check error"
26+
linux_nightly_next_arguments_override: "--explicit-target-dependency-import-check error"
2727
linux_nightly_main_arguments_override: "--explicit-target-dependency-import-check error"
2828

2929
integration-tests:

.gitignore

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,4 +11,5 @@ Package.resolved
1111
.serverless
1212
.vscode
1313
Makefile
14-
.devcontainer
14+
.devcontainer
15+
.amazonq

[email protected]

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,11 @@ let package = Package(
5656
.byName(name: "AWSLambdaRuntime"),
5757
.product(name: "NIOTestUtils", package: "swift-nio"),
5858
.product(name: "NIOFoundationCompat", package: "swift-nio"),
59+
],
60+
swiftSettings: [
61+
.define("FoundationJSONSupport"),
62+
.define("ServiceLifecycleSupport"),
63+
.define("LocalServerSupport"),
5964
]
6065
),
6166
// for perf testing

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: 76 additions & 56 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
//
33
// This source file is part of the SwiftAWSLambdaRuntime open source project
44
//
5-
// Copyright (c) 2020 Apple Inc. and the SwiftAWSLambdaRuntime project authors
5+
// Copyright (c) 2025 Apple Inc. and the SwiftAWSLambdaRuntime project authors
66
// Licensed under Apache License v2.0
77
//
88
// See LICENSE.txt for license information
@@ -76,7 +76,7 @@ extension Lambda {
7676
/// 1. POST /invoke - the client posts the event to the lambda function
7777
///
7878
/// This server passes the data received from /invoke POST request to the lambda function (GET /next) and then forwards the response back to the client.
79-
private struct LambdaHTTPServer {
79+
internal struct LambdaHTTPServer {
8080
private let invocationEndpoint: String
8181

8282
private let invocationPool = Pool<LocalServerInvocation>()
@@ -166,17 +166,21 @@ private struct LambdaHTTPServer {
166166
// consumed by iterating the group or by exiting the group. Since, we are never consuming
167167
// the results of the group we need the group to automatically discard them; otherwise, this
168168
// would result in a memory leak over time.
169-
try await withThrowingDiscardingTaskGroup { taskGroup in
170-
try await channel.executeThenClose { inbound in
171-
for try await connectionChannel in inbound {
172-
173-
taskGroup.addTask {
174-
logger.trace("Handling a new connection")
175-
await server.handleConnection(channel: connectionChannel, logger: logger)
176-
logger.trace("Done handling the connection")
169+
try await withTaskCancellationHandler {
170+
try await withThrowingDiscardingTaskGroup { taskGroup in
171+
try await channel.executeThenClose { inbound in
172+
for try await connectionChannel in inbound {
173+
174+
taskGroup.addTask {
175+
logger.trace("Handling a new connection")
176+
await server.handleConnection(channel: connectionChannel, logger: logger)
177+
logger.trace("Done handling the connection")
178+
}
177179
}
178180
}
179181
}
182+
} onCancel: {
183+
channel.channel.close(promise: nil)
180184
}
181185
return .serverReturned(.success(()))
182186
} catch {
@@ -230,38 +234,42 @@ private struct LambdaHTTPServer {
230234
// Note that this method is non-throwing and we are catching any error.
231235
// We do this since we don't want to tear down the whole server when a single connection
232236
// encounters an error.
233-
do {
234-
try await channel.executeThenClose { inbound, outbound in
235-
for try await inboundData in inbound {
236-
switch inboundData {
237-
case .head(let head):
238-
requestHead = head
239-
240-
case .body(let body):
241-
requestBody.setOrWriteImmutableBuffer(body)
242-
243-
case .end:
244-
precondition(requestHead != nil, "Received .end without .head")
245-
// process the request
246-
let response = try await self.processRequest(
247-
head: requestHead,
248-
body: requestBody,
249-
logger: logger
250-
)
251-
// send the responses
252-
try await self.sendResponse(
253-
response: response,
254-
outbound: outbound,
255-
logger: logger
256-
)
257-
258-
requestHead = nil
259-
requestBody = nil
237+
await withTaskCancellationHandler {
238+
do {
239+
try await channel.executeThenClose { inbound, outbound in
240+
for try await inboundData in inbound {
241+
switch inboundData {
242+
case .head(let head):
243+
requestHead = head
244+
245+
case .body(let body):
246+
requestBody.setOrWriteImmutableBuffer(body)
247+
248+
case .end:
249+
precondition(requestHead != nil, "Received .end without .head")
250+
// process the request
251+
let response = try await self.processRequest(
252+
head: requestHead,
253+
body: requestBody,
254+
logger: logger
255+
)
256+
// send the responses
257+
try await self.sendResponse(
258+
response: response,
259+
outbound: outbound,
260+
logger: logger
261+
)
262+
263+
requestHead = nil
264+
requestBody = nil
265+
}
260266
}
261267
}
268+
} catch {
269+
logger.error("Hit error: \(error)")
262270
}
263-
} catch {
264-
logger.error("Hit error: \(error)")
271+
} onCancel: {
272+
channel.channel.close(promise: nil)
265273
}
266274
}
267275

@@ -426,7 +434,7 @@ private struct LambdaHTTPServer {
426434
/// A shared data structure to store the current invocation or response requests and the continuation objects.
427435
/// This data structure is shared between instances of the HTTPHandler
428436
/// (one instance to serve requests from the Lambda function and one instance to serve requests from the client invoking the lambda function).
429-
private final class Pool<T>: AsyncSequence, AsyncIteratorProtocol, Sendable where T: Sendable {
437+
internal final class Pool<T>: AsyncSequence, AsyncIteratorProtocol, Sendable where T: Sendable {
430438
typealias Element = T
431439

432440
enum State: ~Copyable {
@@ -462,26 +470,38 @@ private struct LambdaHTTPServer {
462470
return nil
463471
}
464472

465-
return try await withCheckedThrowingContinuation { (continuation: CheckedContinuation<T, any Error>) in
466-
let nextAction = self.lock.withLock { state -> T? in
467-
switch consume state {
468-
case .buffer(var buffer):
469-
if let first = buffer.popFirst() {
470-
state = .buffer(buffer)
471-
return first
472-
} else {
473-
state = .continuation(continuation)
474-
return nil
475-
}
473+
return try await withTaskCancellationHandler {
474+
try await withCheckedThrowingContinuation { (continuation: CheckedContinuation<T, any Error>) in
475+
let nextAction = self.lock.withLock { state -> T? in
476+
switch consume state {
477+
case .buffer(var buffer):
478+
if let first = buffer.popFirst() {
479+
state = .buffer(buffer)
480+
return first
481+
} else {
482+
state = .continuation(continuation)
483+
return nil
484+
}
476485

477-
case .continuation:
478-
fatalError("Concurrent invocations to next(). This is illegal.")
486+
case .continuation:
487+
fatalError("Concurrent invocations to next(). This is illegal.")
488+
}
479489
}
480-
}
481490

482-
guard let nextAction else { return }
491+
guard let nextAction else { return }
483492

484-
continuation.resume(returning: nextAction)
493+
continuation.resume(returning: nextAction)
494+
}
495+
} onCancel: {
496+
self.lock.withLock { state in
497+
switch consume state {
498+
case .buffer(let buffer):
499+
state = .buffer(buffer)
500+
case .continuation(let continuation):
501+
continuation?.resume(throwing: CancellationError())
502+
state = .buffer([])
503+
}
504+
}
485505
}
486506
}
487507

Sources/AWSLambdaRuntime/LambdaHandlers.swift

Lines changed: 14 additions & 5 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.
@@ -20,7 +21,7 @@ import NIOCore
2021
/// Background work can also be executed after returning the response. After closing the response stream by calling
2122
/// ``LambdaResponseStreamWriter/finish()`` or ``LambdaResponseStreamWriter/writeAndFinish(_:)``,
2223
/// the ``handle(_:responseWriter:context:)`` function is free to execute any background work.
23-
public protocol StreamingLambdaHandler {
24+
public protocol StreamingLambdaHandler: _Lambda_SendableMetatype {
2425
/// The handler function -- implement the business logic of the Lambda function here.
2526
/// - Parameters:
2627
/// - event: The invocation's input data.
@@ -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+ServiceLifecycle.swift

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,5 +15,11 @@
1515
#if ServiceLifecycleSupport
1616
import ServiceLifecycle
1717

18-
extension LambdaRuntime: Service {}
18+
extension LambdaRuntime: Service {
19+
public func run() async throws {
20+
try await cancelWhenGracefulShutdown {
21+
try await self._run()
22+
}
23+
}
24+
}
1925
#endif

Sources/AWSLambdaRuntime/LambdaRuntime.swift

Lines changed: 13 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -46,13 +46,24 @@ public final class LambdaRuntime<Handler>: @unchecked Sendable where Handler: St
4646
// developers have to wait for AWS Lambda to dispose and recreate a runtime environment to pickup a change
4747
// this approach is less flexible but more performant than reading the value of the environment variable at each invocation
4848
var log = logger
49-
log.logLevel = Lambda.env("LOG_LEVEL").flatMap(Logger.Level.init) ?? .info
49+
50+
// use the LOG_LEVEL environment variable to set the log level.
51+
// if the environment variable is not set, use the default log level from the logger provided
52+
log.logLevel = Lambda.env("LOG_LEVEL").flatMap(Logger.Level.init) ?? logger.logLevel
53+
5054
self.logger = log
5155
self.logger.debug("LambdaRuntime initialized")
5256
}
5357

58+
#if !ServiceLifecycleSupport
59+
@inlinable
60+
internal func run() async throws {
61+
try await _run()
62+
}
63+
#endif
64+
5465
@inlinable
55-
public func run() async throws {
66+
internal func _run() async throws {
5667
let handler = self.handlerMutex.withLockedValue { handler in
5768
let result = handler
5869
handler = nil

0 commit comments

Comments
 (0)