Skip to content

Commit d1a9276

Browse files
authored
add initialization context (#126)
motivation: in more complex initialization scearios you may want access to a logger or other utilities changes: * introduce new InitializationContext type that could be extended in the future without breaking the API in semantic-major way * instead of passing in EventLoop to the handler factory, pass in a context that includes a Logger, an EventLoop and a ByteBufferAllocator * fix a bug where we dont hop back to the event loop when coming back from the handler * adjust tests to the new signature
1 parent 246088e commit d1a9276

File tree

9 files changed

+89
-33
lines changed

9 files changed

+89
-33
lines changed

Sources/AWSLambdaRuntimeCore/Lambda.swift

+8-8
Original file line numberDiff line numberDiff line change
@@ -27,8 +27,8 @@ public enum Lambda {
2727

2828
/// `ByteBufferLambdaHandler` factory.
2929
///
30-
/// A function that takes a `EventLoop` and returns an `EventLoopFuture` of a `ByteBufferLambdaHandler`
31-
public typealias HandlerFactory = (EventLoop) -> EventLoopFuture<Handler>
30+
/// A function that takes a `InitializationContext` and returns an `EventLoopFuture` of a `ByteBufferLambdaHandler`
31+
public typealias HandlerFactory = (InitializationContext) -> EventLoopFuture<Handler>
3232

3333
/// Run a Lambda defined by implementing the `LambdaHandler` protocol.
3434
///
@@ -58,7 +58,7 @@ public enum Lambda {
5858
/// - factory: A `ByteBufferLambdaHandler` factory.
5959
///
6060
/// - note: This is a blocking operation that will run forever, as its lifecycle is managed by the AWS Lambda Runtime Engine.
61-
public static func run(_ factory: @escaping (EventLoop) throws -> Handler) {
61+
public static func run(_ factory: @escaping (InitializationContext) throws -> Handler) {
6262
self.run(factory: factory)
6363
}
6464

@@ -73,19 +73,19 @@ public enum Lambda {
7373
// for testing and internal use
7474
@discardableResult
7575
internal static func run(configuration: Configuration = .init(), handler: Handler) -> Result<Int, Error> {
76-
self.run(configuration: configuration, factory: { $0.makeSucceededFuture(handler) })
76+
self.run(configuration: configuration, factory: { $0.eventLoop.makeSucceededFuture(handler) })
7777
}
7878

7979
// for testing and internal use
8080
@discardableResult
81-
internal static func run(configuration: Configuration = .init(), factory: @escaping (EventLoop) throws -> Handler) -> Result<Int, Error> {
82-
self.run(configuration: configuration, factory: { eventloop -> EventLoopFuture<Handler> in
83-
let promise = eventloop.makePromise(of: Handler.self)
81+
internal static func run(configuration: Configuration = .init(), factory: @escaping (InitializationContext) throws -> Handler) -> Result<Int, Error> {
82+
self.run(configuration: configuration, factory: { context -> EventLoopFuture<Handler> in
83+
let promise = context.eventLoop.makePromise(of: Handler.self)
8484
// if we have a callback based handler factory, we offload the creation of the handler
8585
// onto the default offload queue, to ensure that the eventloop is never blocked.
8686
Lambda.defaultOffloadQueue.async {
8787
do {
88-
promise.succeed(try factory(eventloop))
88+
promise.succeed(try factory(context))
8989
} catch {
9090
promise.fail(error)
9191
}

Sources/AWSLambdaRuntimeCore/LambdaContext.swift

+33-2
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,36 @@ import Dispatch
1616
import Logging
1717
import NIO
1818

19+
// MARK: - InitializationContext
20+
21+
extension Lambda {
22+
/// Lambda runtime initialization context.
23+
/// The Lambda runtime generates and passes the `InitializationContext` to the Lambda factory as an argument.
24+
public final class InitializationContext {
25+
/// `Logger` to log with
26+
///
27+
/// - note: The `LogLevel` can be configured using the `LOG_LEVEL` environment variable.
28+
public let logger: Logger
29+
30+
/// The `EventLoop` the Lambda is executed on. Use this to schedule work with.
31+
///
32+
/// - note: The `EventLoop` is shared with the Lambda runtime engine and should be handled with extra care.
33+
/// Most importantly the `EventLoop` must never be blocked.
34+
public let eventLoop: EventLoop
35+
36+
/// `ByteBufferAllocator` to allocate `ByteBuffer`
37+
public let allocator: ByteBufferAllocator
38+
39+
internal init(logger: Logger, eventLoop: EventLoop, allocator: ByteBufferAllocator) {
40+
self.eventLoop = eventLoop
41+
self.logger = logger
42+
self.allocator = allocator
43+
}
44+
}
45+
}
46+
47+
// MARK: - Context
48+
1949
extension Lambda {
2050
/// Lambda runtime context.
2151
/// The Lambda runtime generates and passes the `Context` to the Lambda handler as an argument.
@@ -61,7 +91,8 @@ extension Lambda {
6191
cognitoIdentity: String? = nil,
6292
clientContext: String? = nil,
6393
logger: Logger,
64-
eventLoop: EventLoop) {
94+
eventLoop: EventLoop,
95+
allocator: ByteBufferAllocator) {
6596
self.requestID = requestID
6697
self.traceID = traceID
6798
self.invokedFunctionARN = invokedFunctionARN
@@ -70,7 +101,7 @@ extension Lambda {
70101
self.deadline = deadline
71102
// utility
72103
self.eventLoop = eventLoop
73-
self.allocator = ByteBufferAllocator()
104+
self.allocator = allocator
74105
// mutate logger with context
75106
var logger = logger
76107
logger[metadataKey: "awsRequestID"] = .string(requestID)

Sources/AWSLambdaRuntimeCore/LambdaRunner.swift

+29-9
Original file line numberDiff line numberDiff line change
@@ -21,12 +21,14 @@ extension Lambda {
2121
internal final class Runner {
2222
private let runtimeClient: RuntimeClient
2323
private let eventLoop: EventLoop
24+
private let allocator: ByteBufferAllocator
2425

2526
private var isGettingNextInvocation = false
2627

2728
init(eventLoop: EventLoop, configuration: Configuration) {
2829
self.eventLoop = eventLoop
2930
self.runtimeClient = RuntimeClient(eventLoop: self.eventLoop, configuration: configuration.runtimeEngine)
31+
self.allocator = ByteBufferAllocator()
3032
}
3133

3234
/// Run the user provided initializer. This *must* only be called once.
@@ -36,13 +38,22 @@ extension Lambda {
3638
logger.debug("initializing lambda")
3739
// 1. create the handler from the factory
3840
// 2. report initialization error if one occured
39-
return factory(self.eventLoop).hop(to: self.eventLoop).peekError { error in
40-
self.runtimeClient.reportInitializationError(logger: logger, error: error).peekError { reportingError in
41-
// We're going to bail out because the init failed, so there's not a lot we can do other than log
42-
// that we couldn't report this error back to the runtime.
43-
logger.error("failed reporting initialization error to lambda runtime engine: \(reportingError)")
41+
let context = InitializationContext(logger: logger,
42+
eventLoop: self.eventLoop,
43+
allocator: self.allocator)
44+
return factory(context)
45+
// Hopping back to "our" EventLoop is importnant in case the factory returns a future
46+
// that originated from a foreign EventLoop/EventLoopGroup.
47+
// This can happen if the factory uses a library (let's say a database client) that manages its own threads/loops
48+
// for whatever reason and returns a future that originated from that foreign EventLoop.
49+
.hop(to: self.eventLoop)
50+
.peekError { error in
51+
self.runtimeClient.reportInitializationError(logger: logger, error: error).peekError { reportingError in
52+
// We're going to bail out because the init failed, so there's not a lot we can do other than log
53+
// that we couldn't report this error back to the runtime.
54+
logger.error("failed reporting initialization error to lambda runtime engine: \(reportingError)")
55+
}
4456
}
45-
}
4657
}
4758

4859
func run(logger: Logger, handler: Handler) -> EventLoopFuture<Void> {
@@ -54,9 +65,17 @@ extension Lambda {
5465
}.flatMap { invocation, event in
5566
// 2. send invocation to handler
5667
self.isGettingNextInvocation = false
57-
let context = Context(logger: logger, eventLoop: self.eventLoop, invocation: invocation)
68+
let context = Context(logger: logger,
69+
eventLoop: self.eventLoop,
70+
allocator: self.allocator,
71+
invocation: invocation)
5872
logger.debug("sending invocation to lambda handler \(handler)")
5973
return handler.handle(context: context, event: event)
74+
// Hopping back to "our" EventLoop is importnant in case the handler returns a future that
75+
// originiated from a foreign EventLoop/EventLoopGroup.
76+
// This can happen if the handler uses a library (lets say a DB client) that manages its own threads/loops
77+
// for whatever reason and returns a future that originated from that foreign EventLoop.
78+
.hop(to: self.eventLoop)
6079
.mapResult { result in
6180
if case .failure(let error) = result {
6281
logger.warning("lambda handler returned an error: \(error)")
@@ -82,15 +101,16 @@ extension Lambda {
82101
}
83102

84103
private extension Lambda.Context {
85-
convenience init(logger: Logger, eventLoop: EventLoop, invocation: Lambda.Invocation) {
104+
convenience init(logger: Logger, eventLoop: EventLoop, allocator: ByteBufferAllocator, invocation: Lambda.Invocation) {
86105
self.init(requestID: invocation.requestID,
87106
traceID: invocation.traceID,
88107
invokedFunctionARN: invocation.invokedFunctionARN,
89108
deadline: DispatchWallTime(millisSinceEpoch: invocation.deadlineInMillisSinceEpoch),
90109
cognitoIdentity: invocation.cognitoIdentity,
91110
clientContext: invocation.clientContext,
92111
logger: logger,
93-
eventLoop: eventLoop)
112+
eventLoop: eventLoop,
113+
allocator: allocator)
94114
}
95115
}
96116

Sources/AWSLambdaTesting/Lambda+Testing.swift

+2-1
Original file line numberDiff line numberDiff line change
@@ -102,7 +102,8 @@ extension Lambda {
102102
invokedFunctionARN: config.invokedFunctionARN,
103103
deadline: .now() + config.timeout,
104104
logger: logger,
105-
eventLoop: eventLoop)
105+
eventLoop: eventLoop,
106+
allocator: ByteBufferAllocator())
106107

107108
return try eventLoop.flatSubmit {
108109
handler.handle(context: context, event: event)

Tests/AWSLambdaRuntimeCoreTests/Lambda+StringTest.swift

+1-1
Original file line numberDiff line numberDiff line change
@@ -185,7 +185,7 @@ class StringLambdaTest: XCTestCase {
185185
typealias In = String
186186
typealias Out = String
187187

188-
init(eventLoop: EventLoop) throws {
188+
init(context: Lambda.InitializationContext) throws {
189189
throw TestError("kaboom")
190190
}
191191

Tests/AWSLambdaRuntimeCoreTests/LambdaRuntimeClientTest.swift

+2-2
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,7 @@ class LambdaRuntimeClientTest: XCTestCase {
3030

3131
func testBootstrapFailure() {
3232
let behavior = Behavior()
33-
XCTAssertThrowsError(try runLambda(behavior: behavior, factory: { $0.makeFailedFuture(TestError("boom")) })) { error in
33+
XCTAssertThrowsError(try runLambda(behavior: behavior, factory: { $0.eventLoop.makeFailedFuture(TestError("boom")) })) { error in
3434
XCTAssertEqual(error as? TestError, TestError("boom"))
3535
}
3636
XCTAssertEqual(behavior.state, 1)
@@ -186,7 +186,7 @@ class LambdaRuntimeClientTest: XCTestCase {
186186
.failure(.internalServerError)
187187
}
188188
}
189-
XCTAssertThrowsError(try runLambda(behavior: Behavior(), factory: { $0.makeFailedFuture(TestError("boom")) })) { error in
189+
XCTAssertThrowsError(try runLambda(behavior: Behavior(), factory: { $0.eventLoop.makeFailedFuture(TestError("boom")) })) { error in
190190
XCTAssertEqual(error as? TestError, TestError("boom"))
191191
}
192192
}

Tests/AWSLambdaRuntimeCoreTests/LambdaTest.swift

+11-8
Original file line numberDiff line numberDiff line change
@@ -51,7 +51,7 @@ class LambdaTest: XCTestCase {
5151

5252
var initialized = false
5353

54-
init(eventLoop: EventLoop) {
54+
init(context: Lambda.InitializationContext) {
5555
XCTAssertFalse(self.initialized)
5656
self.initialized = true
5757
}
@@ -72,7 +72,7 @@ class LambdaTest: XCTestCase {
7272
XCTAssertNoThrow(try server.start().wait())
7373
defer { XCTAssertNoThrow(try server.stop().wait()) }
7474

75-
let result = Lambda.run(factory: { $0.makeFailedFuture(TestError("kaboom")) })
75+
let result = Lambda.run(factory: { $0.eventLoop.makeFailedFuture(TestError("kaboom")) })
7676
assertLambdaLifecycleResult(result, shouldFailWithError: TestError("kaboom"))
7777
}
7878

@@ -85,7 +85,7 @@ class LambdaTest: XCTestCase {
8585
typealias In = String
8686
typealias Out = Void
8787

88-
init(eventLoop: EventLoop) throws {
88+
init(context: Lambda.InitializationContext) throws {
8989
throw TestError("kaboom")
9090
}
9191

@@ -124,7 +124,7 @@ class LambdaTest: XCTestCase {
124124
XCTAssertNoThrow(try server.start().wait())
125125
defer { XCTAssertNoThrow(try server.stop().wait()) }
126126

127-
let result = Lambda.run(factory: { $0.makeFailedFuture(TestError("kaboom")) })
127+
let result = Lambda.run(factory: { $0.eventLoop.makeFailedFuture(TestError("kaboom")) })
128128
assertLambdaLifecycleResult(result, shouldFailWithError: TestError("kaboom"))
129129
}
130130

@@ -143,7 +143,7 @@ class LambdaTest: XCTestCase {
143143
usleep(100_000)
144144
kill(getpid(), signal.rawValue)
145145
}
146-
let result = Lambda.run(configuration: configuration, factory: { $0.makeSucceededFuture(EchoHandler()) })
146+
let result = Lambda.run(configuration: configuration, factory: { $0.eventLoop.makeSucceededFuture(EchoHandler()) })
147147

148148
switch result {
149149
case .success(let invocationCount):
@@ -263,7 +263,8 @@ class LambdaTest: XCTestCase {
263263
cognitoIdentity: nil,
264264
clientContext: nil,
265265
logger: Logger(label: "test"),
266-
eventLoop: MultiThreadedEventLoopGroup(numberOfThreads: 1).next())
266+
eventLoop: MultiThreadedEventLoopGroup(numberOfThreads: 1).next(),
267+
allocator: ByteBufferAllocator())
267268
XCTAssertGreaterThan(context.deadline, .now())
268269

269270
let expiredContext = Lambda.Context(requestID: context.requestID,
@@ -273,7 +274,8 @@ class LambdaTest: XCTestCase {
273274
cognitoIdentity: context.cognitoIdentity,
274275
clientContext: context.clientContext,
275276
logger: context.logger,
276-
eventLoop: context.eventLoop)
277+
eventLoop: context.eventLoop,
278+
allocator: context.allocator)
277279
XCTAssertLessThan(expiredContext.deadline, .now())
278280
}
279281

@@ -285,7 +287,8 @@ class LambdaTest: XCTestCase {
285287
cognitoIdentity: nil,
286288
clientContext: nil,
287289
logger: Logger(label: "test"),
288-
eventLoop: MultiThreadedEventLoopGroup(numberOfThreads: 1).next())
290+
eventLoop: MultiThreadedEventLoopGroup(numberOfThreads: 1).next(),
291+
allocator: ByteBufferAllocator())
289292
XCTAssertLessThanOrEqual(context.getRemainingTime(), .seconds(1))
290293
XCTAssertGreaterThan(context.getRemainingTime(), .milliseconds(800))
291294
}

Tests/AWSLambdaRuntimeCoreTests/Utils.swift

+1-1
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@ import NIO
1818
import XCTest
1919

2020
func runLambda(behavior: LambdaServerBehavior, handler: Lambda.Handler) throws {
21-
try runLambda(behavior: behavior, factory: { $0.makeSucceededFuture(handler) })
21+
try runLambda(behavior: behavior, factory: { $0.eventLoop.makeSucceededFuture(handler) })
2222
}
2323

2424
func runLambda(behavior: LambdaServerBehavior, factory: @escaping Lambda.HandlerFactory) throws {

Tests/AWSLambdaRuntimeTests/Lambda+CodeableTest.swift

+2-1
Original file line numberDiff line numberDiff line change
@@ -72,7 +72,8 @@ class CodableLambdaTest: XCTestCase {
7272
cognitoIdentity: nil,
7373
clientContext: nil,
7474
logger: Logger(label: "test"),
75-
eventLoop: self.eventLoopGroup.next())
75+
eventLoop: self.eventLoopGroup.next(),
76+
allocator: ByteBufferAllocator())
7677
}
7778
}
7879

0 commit comments

Comments
 (0)