Skip to content

Commit a051851

Browse files
authored
refactor bootstrapping (#28)
motivation: make initialization logic more robust, allowing setup at contructor time and also async bootstrap changes: * remove the initialize method and replace it with a factory pattern. * update core API and logic to support new initialization flow. * add tests to various initialization flows
1 parent d046617 commit a051851

12 files changed

+264
-154
lines changed

Sources/SwiftAwsLambda/Lambda+Codable.swift

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,7 @@ extension Lambda {
2626

2727
// for testing
2828
internal static func run<In: Decodable, Out: Encodable>(configuration: Configuration = .init(), closure: @escaping LambdaCodableClosure<In, Out>) -> LambdaLifecycleResult {
29-
return self.run(handler: LambdaClosureWrapper(closure), configuration: configuration)
29+
return self.run(configuration: configuration, handler: LambdaClosureWrapper(closure))
3030
}
3131
}
3232

@@ -64,7 +64,7 @@ public class LambdaCodableCodec<In: Decodable, Out: Encodable> {
6464

6565
/// Default implementation of `Encodable` -> `[UInt8]` encoding and `[UInt8]` -> `Decodable' decoding
6666
public extension LambdaCodableHandler {
67-
func handle(context: Lambda.Context, payload: [UInt8], callback: @escaping (LambdaResult) -> Void) {
67+
func handle(context: Lambda.Context, payload: [UInt8], callback: @escaping LambdaCallback) {
6868
switch self.codec.decode(payload) {
6969
case .failure(let error):
7070
return callback(.failure(Errors.requestDecoding(error)))

Sources/SwiftAwsLambda/Lambda+String.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@ extension Lambda {
2323

2424
// for testing
2525
internal static func run(configuration: Configuration = .init(), _ closure: @escaping LambdaStringClosure) -> LambdaLifecycleResult {
26-
return self.run(handler: LambdaClosureWrapper(closure), configuration: configuration)
26+
return self.run(configuration: configuration, handler: LambdaClosureWrapper(closure))
2727
}
2828
}
2929

Sources/SwiftAwsLambda/Lambda.swift

Lines changed: 87 additions & 43 deletions
Original file line numberDiff line numberDiff line change
@@ -41,32 +41,69 @@ public enum Lambda {
4141
self.run(handler: handler)
4242
}
4343

44+
/// Run a Lambda defined by implementing the `LambdaHandler` protocol via a `LambdaHandlerFactory`.
45+
///
46+
/// - note: This is a blocking operation that will run forever, as it's lifecycle is managed by the AWS Lambda Runtime Engine.
47+
@inlinable
48+
public static func run(_ factory: @escaping LambdaHandlerFactory) {
49+
self.run(factory: factory)
50+
}
51+
52+
/// Run a Lambda defined by implementing the `LambdaHandler` protocol via a factory.
53+
///
54+
/// - note: This is a blocking operation that will run forever, as it's lifecycle is managed by the AWS Lambda Runtime Engine.
55+
@inlinable
56+
public static func run(_ factory: @escaping (EventLoop) throws -> LambdaHandler) {
57+
self.run(factory: factory)
58+
}
59+
4460
// for testing and internal use
4561
@usableFromInline
4662
@discardableResult
4763
internal static func run(configuration: Configuration = .init(), closure: @escaping LambdaClosure) -> LambdaLifecycleResult {
48-
return self.run(handler: LambdaClosureWrapper(closure), configuration: configuration)
64+
return self.run(configuration: configuration, handler: LambdaClosureWrapper(closure))
65+
}
66+
67+
// for testing and internal use
68+
@usableFromInline
69+
@discardableResult
70+
internal static func run(configuration: Configuration = .init(), handler: LambdaHandler) -> LambdaLifecycleResult {
71+
return self.run(configuration: configuration, factory: { _, callback in callback(.success(handler)) })
72+
}
73+
74+
// for testing and internal use
75+
@usableFromInline
76+
@discardableResult
77+
internal static func run(configuration: Configuration = .init(), factory: @escaping (EventLoop) throws -> LambdaHandler) -> LambdaLifecycleResult {
78+
return self.run(configuration: configuration, factory: { (eventloop: EventLoop, callback: (Result<LambdaHandler, Error>) -> Void) -> Void in
79+
do {
80+
let handler = try factory(eventloop)
81+
callback(.success(handler))
82+
} catch {
83+
callback(.failure(error))
84+
}
85+
})
4986
}
5087

5188
// for testing and internal use
5289
@usableFromInline
5390
@discardableResult
54-
internal static func run(handler: LambdaHandler, configuration: Configuration = .init()) -> LambdaLifecycleResult {
91+
internal static func run(configuration: Configuration = .init(), factory: @escaping LambdaHandlerFactory) -> LambdaLifecycleResult {
5592
do {
5693
let eventLoopGroup = MultiThreadedEventLoopGroup(numberOfThreads: 1) // only need one thread, will improve performance
5794
defer { try! eventLoopGroup.syncShutdownGracefully() }
58-
let result = try self.runAsync(eventLoopGroup: eventLoopGroup, handler: handler, configuration: configuration).wait()
95+
let result = try self.runAsync(eventLoopGroup: eventLoopGroup, configuration: configuration, factory: factory).wait()
5996
return .success(result)
6097
} catch {
6198
return .failure(error)
6299
}
63100
}
64101

65-
internal static func runAsync(eventLoopGroup: EventLoopGroup, handler: LambdaHandler, configuration: Configuration) -> EventLoopFuture<Int> {
102+
internal static func runAsync(eventLoopGroup: EventLoopGroup, configuration: Configuration, factory: @escaping LambdaHandlerFactory) -> EventLoopFuture<Int> {
66103
Backtrace.install()
67104
var logger = Logger(label: "Lambda")
68105
logger.logLevel = configuration.general.logLevel
69-
let lifecycle = Lifecycle(eventLoop: eventLoopGroup.next(), logger: logger, configuration: configuration, handler: handler)
106+
let lifecycle = Lifecycle(eventLoop: eventLoopGroup.next(), logger: logger, configuration: configuration, factory: factory)
70107
let signalSource = trap(signal: configuration.lifecycle.stopSignal) { signal in
71108
logger.info("intercepted signal: \(signal)")
72109
lifecycle.stop()
@@ -132,31 +169,33 @@ public enum Lambda {
132169
private let eventLoop: EventLoop
133170
private let logger: Logger
134171
private let configuration: Configuration
135-
private let handler: LambdaHandler
172+
private let factory: LambdaHandlerFactory
136173

137-
private var _state = LifecycleState.idle
174+
private var _state = State.idle
138175
private let stateLock = Lock()
139176

140-
init(eventLoop: EventLoop, logger: Logger, configuration: Configuration, handler: LambdaHandler) {
177+
init(eventLoop: EventLoop, logger: Logger, configuration: Configuration, factory: @escaping LambdaHandlerFactory) {
141178
self.eventLoop = eventLoop
142179
self.logger = logger
143180
self.configuration = configuration
144-
self.handler = handler
181+
self.factory = factory
145182
}
146183

147184
deinit {
148-
precondition(self.state == .shutdown, "invalid state \(self.state)")
185+
guard case .shutdown = self.state else {
186+
preconditionFailure("invalid state \(self.state)")
187+
}
149188
}
150189

151-
private var state: LifecycleState {
190+
private var state: State {
152191
get {
153192
return self.stateLock.withLock {
154193
self._state
155194
}
156195
}
157196
set {
158197
self.stateLock.withLockVoid {
159-
precondition(newValue.rawValue > _state.rawValue, "invalid state \(newValue) after \(self._state)")
198+
precondition(newValue.order > _state.order, "invalid state \(newValue) after \(self._state)")
160199
self._state = newValue
161200
}
162201
}
@@ -167,10 +206,10 @@ public enum Lambda {
167206
self.state = .initializing
168207
var logger = self.logger
169208
logger[metadataKey: "lifecycleId"] = .string(self.configuration.lifecycle.id)
170-
let runner = LambdaRunner(eventLoop: self.eventLoop, configuration: self.configuration, lambdaHandler: self.handler)
171-
return runner.initialize(logger: logger).flatMap { _ in
172-
self.state = .active
173-
return self.run(runner: runner)
209+
let runner = LambdaRunner(eventLoop: self.eventLoop, configuration: self.configuration)
210+
return runner.initialize(logger: logger, factory: self.factory).flatMap { handler in
211+
self.state = .active(runner, handler)
212+
return self.run()
174213
}
175214
}
176215

@@ -185,18 +224,18 @@ public enum Lambda {
185224
}
186225

187226
@inline(__always)
188-
private func run(runner: LambdaRunner) -> EventLoopFuture<Int> {
227+
private func run() -> EventLoopFuture<Int> {
189228
let promise = self.eventLoop.makePromise(of: Int.self)
190229

191230
func _run(_ count: Int) {
192231
switch self.state {
193-
case .active:
232+
case .active(let runner, let handler):
194233
if self.configuration.lifecycle.maxTimes > 0, count >= self.configuration.lifecycle.maxTimes {
195234
return promise.succeed(count)
196235
}
197236
var logger = self.logger
198237
logger[metadataKey: "lifecycleIteration"] = "\(count)"
199-
runner.run(logger: logger).whenComplete { result in
238+
runner.run(logger: logger, handler: handler).whenComplete { result in
200239
switch result {
201240
case .success:
202241
// recursive! per aws lambda runtime spec the polling requests are to be done one at a time
@@ -216,6 +255,29 @@ public enum Lambda {
216255

217256
return promise.futureResult
218257
}
258+
259+
private enum State {
260+
case idle
261+
case initializing
262+
case active(LambdaRunner, LambdaHandler)
263+
case stopping
264+
case shutdown
265+
266+
internal var order: Int {
267+
switch self {
268+
case .idle:
269+
return 0
270+
case .initializing:
271+
return 1
272+
case .active:
273+
return 2
274+
case .stopping:
275+
return 3
276+
case .shutdown:
277+
return 4
278+
}
279+
}
280+
}
219281
}
220282

221283
@usableFromInline
@@ -293,44 +355,26 @@ public enum Lambda {
293355
return "\(Configuration.self)\n \(self.general))\n \(self.lifecycle)\n \(self.runtimeEngine)"
294356
}
295357
}
296-
297-
private enum LifecycleState: Int {
298-
case idle
299-
case initializing
300-
case active
301-
case stopping
302-
case shutdown
303-
}
304358
}
305359

306-
/// A result type for a Lambda that returns a `[UInt8]`.
307360
public typealias LambdaResult = Result<[UInt8], Error>
308361

309362
public typealias LambdaCallback = (LambdaResult) -> Void
310363

311-
/// A processing closure for a Lambda that takes a `[UInt8]` and returns a `LambdaResult` result type asynchronously.
364+
/// A processing closure for a Lambda that takes a `[UInt8]` and returns a `LambdaResult` result type asynchronously via`LambdaCallback` .
312365
public typealias LambdaClosure = (Lambda.Context, [UInt8], LambdaCallback) -> Void
313366

314-
/// A result type for a Lambda initialization.
315-
public typealias LambdaInitResult = Result<Void, Error>
316-
317367
/// A callback to provide the result of Lambda initialization.
318-
public typealias LambdaInitCallBack = (LambdaInitResult) -> Void
368+
public typealias LambdaInitCallBack = (Result<LambdaHandler, Error>) -> Void
369+
370+
public typealias LambdaHandlerFactory = (EventLoop, LambdaInitCallBack) -> Void
319371

320-
/// A processing protocol for a Lambda that takes a `[UInt8]` and returns a `LambdaResult` result type asynchronously.
372+
/// A processing protocol for a Lambda that takes a `[UInt8]` and returns a `LambdaResult` result type asynchronously via `LambdaCallback`.
321373
public protocol LambdaHandler {
322-
/// Initializes the `LambdaHandler`.
323-
func initialize(callback: @escaping LambdaInitCallBack)
374+
/// Handles the Lambda request.
324375
func handle(context: Lambda.Context, payload: [UInt8], callback: @escaping LambdaCallback)
325376
}
326377

327-
extension LambdaHandler {
328-
@inlinable
329-
public func initialize(callback: @escaping LambdaInitCallBack) {
330-
callback(.success(()))
331-
}
332-
}
333-
334378
@usableFromInline
335379
internal typealias LambdaLifecycleResult = Result<Int, Error>
336380

Sources/SwiftAwsLambda/LambdaRunner.swift

Lines changed: 25 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -19,28 +19,26 @@ import NIO
1919
/// LambdaRunner manages the Lambda runtime workflow, or business logic.
2020
internal struct LambdaRunner {
2121
private let runtimeClient: LambdaRuntimeClient
22-
private let lambdaHandler: LambdaHandler
2322
private let eventLoop: EventLoop
2423
private let lifecycleId: String
2524
private let offload: Bool
2625

27-
init(eventLoop: EventLoop, configuration: Lambda.Configuration, lambdaHandler: LambdaHandler) {
26+
init(eventLoop: EventLoop, configuration: Lambda.Configuration) {
2827
self.eventLoop = eventLoop
2928
self.runtimeClient = LambdaRuntimeClient(eventLoop: self.eventLoop, configuration: configuration.runtimeEngine)
30-
self.lambdaHandler = lambdaHandler
3129
self.lifecycleId = configuration.lifecycle.id
3230
self.offload = configuration.runtimeEngine.offload
3331
}
3432

3533
/// Run the user provided initializer. This *must* only be called once.
3634
///
37-
/// - Returns: An `EventLoopFuture<Void>` fulfilled with the outcome of the initialization.
38-
func initialize(logger: Logger) -> EventLoopFuture<Void> {
35+
/// - Returns: An `EventLoopFuture<LambdaHandler>` fulfilled with the outcome of the initialization.
36+
func initialize(logger: Logger, factory: @escaping LambdaHandlerFactory) -> EventLoopFuture<LambdaHandler> {
3937
logger.debug("initializing lambda")
40-
// We need to use `flatMap` instead of `whenFailure` to ensure we complete reporting the result before stopping.
41-
return self.lambdaHandler.initialize(eventLoop: self.eventLoop,
42-
lifecycleId: self.lifecycleId,
43-
offload: self.offload).peekError { error in
38+
// 1. create the handler from the factory
39+
let future = bootstrap(eventLoop: self.eventLoop, lifecycleId: self.lifecycleId, offload: self.offload, factory: factory)
40+
// 2. report initialization error if one occured
41+
return future.peekError { error in
4442
self.runtimeClient.reportInitializationError(logger: logger, error: error).peekError { reportingError in
4543
// We're going to bail out because the init failed, so there's not a lot we can do other than log
4644
// that we couldn't report this error back to the runtime.
@@ -49,24 +47,24 @@ internal struct LambdaRunner {
4947
}
5048
}
5149

52-
func run(logger: Logger) -> EventLoopFuture<Void> {
50+
func run(logger: Logger, handler: LambdaHandler) -> EventLoopFuture<Void> {
5351
logger.debug("lambda invocation sequence starting")
5452
// 1. request work from lambda runtime engine
5553
return self.runtimeClient.requestWork(logger: logger).peekError { error in
5654
logger.error("could not fetch work from lambda runtime engine: \(error)")
5755
}.flatMap { invocation, payload in
5856
// 2. send work to handler
5957
let context = Lambda.Context(logger: logger, eventLoop: self.eventLoop, invocation: invocation)
60-
logger.debug("sending work to lambda handler \(self.lambdaHandler)")
58+
logger.debug("sending work to lambda handler \(handler)")
6159

6260
// TODO: This is just for now, so that we can work with ByteBuffers only
6361
// in the LambdaRuntimeClient
6462
let bytes = [UInt8](payload.readableBytesView)
65-
return self.lambdaHandler.handle(eventLoop: self.eventLoop,
66-
lifecycleId: self.lifecycleId,
67-
offload: self.offload,
68-
context: context,
69-
payload: bytes)
63+
return handler.handle(eventLoop: self.eventLoop,
64+
lifecycleId: self.lifecycleId,
65+
offload: self.offload,
66+
context: context,
67+
payload: bytes)
7068
.map {
7169
// TODO: This mapping shall be removed as soon as the LambdaHandler protocol
7270
// works with ByteBuffer? instead of [UInt8]
@@ -93,24 +91,24 @@ internal struct LambdaRunner {
9391
}
9492
}
9593

96-
private extension LambdaHandler {
97-
func initialize(eventLoop: EventLoop, lifecycleId: String, offload: Bool) -> EventLoopFuture<Void> {
94+
private func bootstrap(eventLoop: EventLoop, lifecycleId: String, offload: Bool, factory: @escaping LambdaHandlerFactory) -> EventLoopFuture<LambdaHandler> {
95+
let promise = eventLoop.makePromise(of: LambdaHandler.self)
96+
if offload {
9897
// offloading so user code never blocks the eventloop
99-
let promise = eventLoop.makePromise(of: Void.self)
100-
if offload {
101-
DispatchQueue(label: "lambda-\(lifecycleId)").async {
102-
self.initialize { promise.completeWith($0) }
103-
}
104-
} else {
105-
self.initialize { promise.completeWith($0) }
98+
DispatchQueue(label: "lambda-\(lifecycleId)").async {
99+
factory(eventLoop, promise.completeWith)
106100
}
107-
return promise.futureResult
101+
} else {
102+
factory(eventLoop, promise.completeWith)
108103
}
104+
return promise.futureResult
105+
}
109106

107+
private extension LambdaHandler {
110108
func handle(eventLoop: EventLoop, lifecycleId: String, offload: Bool, context: Lambda.Context, payload: [UInt8]) -> EventLoopFuture<LambdaResult> {
111-
// offloading so user code never blocks the eventloop
112109
let promise = eventLoop.makePromise(of: LambdaResult.self)
113110
if offload {
111+
// offloading so user code never blocks the eventloop
114112
DispatchQueue(label: "lambda-\(lifecycleId)").async {
115113
self.handle(context: context, payload: payload) { result in
116114
promise.succeed(result)

Tests/SwiftAwsLambdaTests/Lambda+CodeableTest+XCTest.swift

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@ extension CodableLambdaTest {
2929
("testFailure", testFailure),
3030
("testClosureSuccess", testClosureSuccess),
3131
("testClosureFailure", testClosureFailure),
32+
("testBootstrapFailure", testBootstrapFailure),
3233
]
3334
}
3435
}

0 commit comments

Comments
 (0)