From ec0abbff38c0404e3fdf7c63be0c020154dd1135 Mon Sep 17 00:00:00 2001 From: tom doron Date: Sun, 8 Mar 2020 18:29:24 -0700 Subject: [PATCH 1/2] refactor init motivation: make initialization logic more robust, allowing setup at contructor time and also async bootstrap changes: * break apart "initialization" into two parts: * optional throwing constructor (provider) that takes an EventLoop * optional BootstrappedLambdaHandler protocol that takes an EventLoop and returns async * update core API and logic to support new initialization logic * add tests to various initialization flows --- Sources/SwiftAwsLambda/Lambda+Codable.swift | 3 +- Sources/SwiftAwsLambda/Lambda+String.swift | 4 +- Sources/SwiftAwsLambda/Lambda.swift | 99 ++++++++++++------- Sources/SwiftAwsLambda/LambdaRunner.swift | 59 ++++++----- .../Lambda+CodeableTest.swift | 6 +- .../Lambda+StringTest.swift | 6 +- .../LambdaRuntimeClientTest+XCTest.swift | 2 + .../LambdaRuntimeClientTest.swift | 38 ++++++- .../LambdaTest+XCTest.swift | 2 + Tests/SwiftAwsLambdaTests/LambdaTest.swift | 85 +++++++++++++--- Tests/SwiftAwsLambdaTests/Utils.swift | 28 +++--- 11 files changed, 243 insertions(+), 89 deletions(-) diff --git a/Sources/SwiftAwsLambda/Lambda+Codable.swift b/Sources/SwiftAwsLambda/Lambda+Codable.swift index 744bbc2f..aca6c8ad 100644 --- a/Sources/SwiftAwsLambda/Lambda+Codable.swift +++ b/Sources/SwiftAwsLambda/Lambda+Codable.swift @@ -13,6 +13,7 @@ //===----------------------------------------------------------------------===// import Foundation // for JSON +import NIO /// Extension to the `Lambda` companion to enable execution of Lambdas that take and return `Codable` payloads. /// This is the most common way to use this library in AWS Lambda, since its JSON based. @@ -26,7 +27,7 @@ extension Lambda { // for testing internal static func run(configuration: Configuration = .init(), closure: @escaping LambdaCodableClosure) -> LambdaLifecycleResult { - return self.run(handler: LambdaClosureWrapper(closure), configuration: configuration) + return self.run(configuration: configuration, handler: LambdaClosureWrapper(closure)) } } diff --git a/Sources/SwiftAwsLambda/Lambda+String.swift b/Sources/SwiftAwsLambda/Lambda+String.swift index dbc48601..699ab1ac 100644 --- a/Sources/SwiftAwsLambda/Lambda+String.swift +++ b/Sources/SwiftAwsLambda/Lambda+String.swift @@ -12,6 +12,8 @@ // //===----------------------------------------------------------------------===// +import NIO + /// Extension to the `Lambda` companion to enable execution of Lambdas that take and return `String` payloads. extension Lambda { /// Run a Lambda defined by implementing the `LambdaStringClosure` protocol. @@ -23,7 +25,7 @@ extension Lambda { // for testing internal static func run(configuration: Configuration = .init(), _ closure: @escaping LambdaStringClosure) -> LambdaLifecycleResult { - return self.run(handler: LambdaClosureWrapper(closure), configuration: configuration) + return self.run(configuration: configuration, handler: LambdaClosureWrapper(closure)) } } diff --git a/Sources/SwiftAwsLambda/Lambda.swift b/Sources/SwiftAwsLambda/Lambda.swift index 330a3109..8cdb9977 100644 --- a/Sources/SwiftAwsLambda/Lambda.swift +++ b/Sources/SwiftAwsLambda/Lambda.swift @@ -41,32 +41,47 @@ public enum Lambda { self.run(handler: handler) } + /// Run a Lambda defined by implementing the `LambdaHandler` protocol. + /// + /// - note: This is a blocking operation that will run forever, as it's lifecycle is managed by the AWS Lambda Runtime Engine. + @inlinable + public static func run(_ provider: @escaping LambdaHandlerProvider) { + self.run(provider: provider) + } + // for testing and internal use @usableFromInline @discardableResult internal static func run(configuration: Configuration = .init(), closure: @escaping LambdaClosure) -> LambdaLifecycleResult { - return self.run(handler: LambdaClosureWrapper(closure), configuration: configuration) + return self.run(configuration: configuration, handler: LambdaClosureWrapper(closure)) } // for testing and internal use @usableFromInline @discardableResult - internal static func run(handler: LambdaHandler, configuration: Configuration = .init()) -> LambdaLifecycleResult { + internal static func run(configuration: Configuration = .init(), handler: LambdaHandler) -> LambdaLifecycleResult { + return self.run(configuration: configuration, provider: { _ in handler }) + } + + // for testing and internal use + @usableFromInline + @discardableResult + internal static func run(configuration: Configuration = .init(), provider: @escaping LambdaHandlerProvider) -> LambdaLifecycleResult { do { let eventLoopGroup = MultiThreadedEventLoopGroup(numberOfThreads: 1) // only need one thread, will improve performance defer { try! eventLoopGroup.syncShutdownGracefully() } - let result = try self.runAsync(eventLoopGroup: eventLoopGroup, handler: handler, configuration: configuration).wait() + let result = try self.runAsync(eventLoopGroup: eventLoopGroup, configuration: configuration, provider: provider).wait() return .success(result) } catch { return .failure(error) } } - internal static func runAsync(eventLoopGroup: EventLoopGroup, handler: LambdaHandler, configuration: Configuration) -> EventLoopFuture { + internal static func runAsync(eventLoopGroup: EventLoopGroup, configuration: Configuration, provider: @escaping LambdaHandlerProvider) -> EventLoopFuture { Backtrace.install() var logger = Logger(label: "Lambda") logger.logLevel = configuration.general.logLevel - let lifecycle = Lifecycle(eventLoop: eventLoopGroup.next(), logger: logger, configuration: configuration, handler: handler) + let lifecycle = Lifecycle(eventLoop: eventLoopGroup.next(), logger: logger, configuration: configuration, provider: provider) let signalSource = trap(signal: configuration.lifecycle.stopSignal) { signal in logger.info("intercepted signal: \(signal)") lifecycle.stop() @@ -113,23 +128,25 @@ public enum Lambda { private let eventLoop: EventLoop private let logger: Logger private let configuration: Configuration - private let handler: LambdaHandler + private let provider: LambdaHandlerProvider - private var _state = LifecycleState.idle + private var _state = State.idle private let stateLock = Lock() - init(eventLoop: EventLoop, logger: Logger, configuration: Configuration, handler: LambdaHandler) { + init(eventLoop: EventLoop, logger: Logger, configuration: Configuration, provider: @escaping LambdaHandlerProvider) { self.eventLoop = eventLoop self.logger = logger self.configuration = configuration - self.handler = handler + self.provider = provider } deinit { - precondition(self.state == .shutdown, "invalid state \(self.state)") + guard case .shutdown = self.state else { + preconditionFailure("invalid state \(self.state)") + } } - private var state: LifecycleState { + private var state: State { get { return self.stateLock.withLock { self._state @@ -137,7 +154,7 @@ public enum Lambda { } set { self.stateLock.withLockVoid { - precondition(newValue.rawValue > _state.rawValue, "invalid state \(newValue) after \(self._state)") + precondition(newValue.order > _state.order, "invalid state \(newValue) after \(self._state)") self._state = newValue } } @@ -148,10 +165,10 @@ public enum Lambda { self.state = .initializing var logger = self.logger logger[metadataKey: "lifecycleId"] = .string(self.configuration.lifecycle.id) - let runner = LambdaRunner(eventLoop: self.eventLoop, configuration: self.configuration, lambdaHandler: self.handler) - return runner.initialize(logger: logger).flatMap { _ in - self.state = .active - return self.run(runner: runner) + let runner = LambdaRunner(eventLoop: self.eventLoop, configuration: self.configuration) + return runner.initialize(logger: logger, provider: self.provider).flatMap { handler in + self.state = .active(runner, handler) + return self.run() } } @@ -166,18 +183,18 @@ public enum Lambda { } @inline(__always) - private func run(runner: LambdaRunner) -> EventLoopFuture { + private func run() -> EventLoopFuture { let promise = self.eventLoop.makePromise(of: Int.self) func _run(_ count: Int) { switch self.state { - case .active: + case .active(let runner, let handler): if self.configuration.lifecycle.maxTimes > 0, count >= self.configuration.lifecycle.maxTimes { return promise.succeed(count) } var logger = self.logger logger[metadataKey: "lifecycleIteration"] = "\(count)" - runner.run(logger: logger).whenComplete { result in + runner.run(logger: logger, handler: handler).whenComplete { result in switch result { case .success: // recursive! per aws lambda runtime spec the polling requests are to be done one at a time @@ -197,6 +214,29 @@ public enum Lambda { return promise.futureResult } + + private enum State { + case idle + case initializing + case active(LambdaRunner, LambdaHandler) + case stopping + case shutdown + + internal var order: Int { + switch self { + case .idle: + return 0 + case .initializing: + return 1 + case .active: + return 2 + case .stopping: + return 3 + case .shutdown: + return 4 + } + } + } } @usableFromInline @@ -274,14 +314,6 @@ public enum Lambda { return "\(Configuration.self)\n \(self.general))\n \(self.lifecycle)\n \(self.runtimeEngine)" } } - - private enum LifecycleState: Int { - case idle - case initializing - case active - case stopping - case shutdown - } } /// A result type for a Lambda that returns a `[UInt8]`. @@ -298,18 +330,17 @@ public typealias LambdaInitResult = Result /// A callback to provide the result of Lambda initialization. public typealias LambdaInitCallBack = (LambdaInitResult) -> Void +public typealias LambdaHandlerProvider = (EventLoop) throws -> LambdaHandler + /// A processing protocol for a Lambda that takes a `[UInt8]` and returns a `LambdaResult` result type asynchronously. public protocol LambdaHandler { - /// Initializes the `LambdaHandler`. - func initialize(callback: @escaping LambdaInitCallBack) + /// Handles the Lambda request. func handle(context: Lambda.Context, payload: [UInt8], callback: @escaping LambdaCallback) } -extension LambdaHandler { - @inlinable - public func initialize(callback: @escaping LambdaInitCallBack) { - callback(.success(())) - } +public protocol BootstrappedLambdaHandler: LambdaHandler { + /// Bootstraps the `LambdaHandler`. + func bootstrap(callback: @escaping LambdaInitCallBack) } @usableFromInline diff --git a/Sources/SwiftAwsLambda/LambdaRunner.swift b/Sources/SwiftAwsLambda/LambdaRunner.swift index 448d81f1..f0fc608c 100644 --- a/Sources/SwiftAwsLambda/LambdaRunner.swift +++ b/Sources/SwiftAwsLambda/LambdaRunner.swift @@ -19,28 +19,41 @@ import NIO /// LambdaRunner manages the Lambda runtime workflow, or business logic. internal struct LambdaRunner { private let runtimeClient: LambdaRuntimeClient - private let lambdaHandler: LambdaHandler private let eventLoop: EventLoop private let lifecycleId: String private let offload: Bool - init(eventLoop: EventLoop, configuration: Lambda.Configuration, lambdaHandler: LambdaHandler) { + init(eventLoop: EventLoop, configuration: Lambda.Configuration) { self.eventLoop = eventLoop self.runtimeClient = LambdaRuntimeClient(eventLoop: self.eventLoop, configuration: configuration.runtimeEngine) - self.lambdaHandler = lambdaHandler self.lifecycleId = configuration.lifecycle.id self.offload = configuration.runtimeEngine.offload } /// Run the user provided initializer. This *must* only be called once. /// - /// - Returns: An `EventLoopFuture` fulfilled with the outcome of the initialization. - func initialize(logger: Logger) -> EventLoopFuture { + /// - Returns: An `EventLoopFuture` fulfilled with the outcome of the initialization. + func initialize(logger: Logger, provider: @escaping LambdaHandlerProvider) -> EventLoopFuture { logger.debug("initializing lambda") - // We need to use `flatMap` instead of `whenFailure` to ensure we complete reporting the result before stopping. - return self.lambdaHandler.initialize(eventLoop: self.eventLoop, - lifecycleId: self.lifecycleId, - offload: self.offload).peekError { error in + + let future: EventLoopFuture + do { + // 1. craete the handler from the provider + let handler = try provider(self.eventLoop) + // 2. bootstrap if needed + if let handler = handler as? BootstrappedLambdaHandler { + future = handler.bootstrap(eventLoop: self.eventLoop, + lifecycleId: self.lifecycleId, + offload: self.offload).map { handler } + } else { + future = self.eventLoop.makeSucceededFuture(handler) + } + } catch { + future = self.eventLoop.makeFailedFuture(error) + } + + // 3. report initialization error if one occured + return future.peekError { error in self.runtimeClient.reportInitializationError(logger: logger, error: error).peekError { reportingError in // We're going to bail out because the init failed, so there's not a lot we can do other than log // that we couldn't report this error back to the runtime. @@ -49,7 +62,7 @@ internal struct LambdaRunner { } } - func run(logger: Logger) -> EventLoopFuture { + func run(logger: Logger, handler: LambdaHandler) -> EventLoopFuture { logger.debug("lambda invocation sequence starting") // 1. request work from lambda runtime engine return self.runtimeClient.requestWork(logger: logger).peekError { error in @@ -57,16 +70,16 @@ internal struct LambdaRunner { }.flatMap { invocation, payload in // 2. send work to handler let context = Lambda.Context(logger: logger, eventLoop: self.eventLoop, invocation: invocation) - logger.debug("sending work to lambda handler \(self.lambdaHandler)") + logger.debug("sending work to lambda handler \(handler)") // TODO: This is just for now, so that we can work with ByteBuffers only // in the LambdaRuntimeClient let bytes = [UInt8](payload.readableBytesView) - return self.lambdaHandler.handle(eventLoop: self.eventLoop, - lifecycleId: self.lifecycleId, - offload: self.offload, - context: context, - payload: bytes) + return handler.handle(eventLoop: self.eventLoop, + lifecycleId: self.lifecycleId, + offload: self.offload, + context: context, + payload: bytes) .map { // TODO: This mapping shall be removed as soon as the LambdaHandler protocol // works with ByteBuffer? instead of [UInt8] @@ -93,24 +106,26 @@ internal struct LambdaRunner { } } -private extension LambdaHandler { - func initialize(eventLoop: EventLoop, lifecycleId: String, offload: Bool) -> EventLoopFuture { - // offloading so user code never blocks the eventloop +private extension BootstrappedLambdaHandler { + func bootstrap(eventLoop: EventLoop, lifecycleId: String, offload: Bool) -> EventLoopFuture { let promise = eventLoop.makePromise(of: Void.self) if offload { + // offloading so user code never blocks the eventloop DispatchQueue(label: "lambda-\(lifecycleId)").async { - self.initialize { promise.completeWith($0) } + self.bootstrap { promise.completeWith($0) } } } else { - self.initialize { promise.completeWith($0) } + self.bootstrap { promise.completeWith($0) } } return promise.futureResult } +} +private extension LambdaHandler { func handle(eventLoop: EventLoop, lifecycleId: String, offload: Bool, context: Lambda.Context, payload: [UInt8]) -> EventLoopFuture { - // offloading so user code never blocks the eventloop let promise = eventLoop.makePromise(of: LambdaResult.self) if offload { + // offloading so user code never blocks the eventloop DispatchQueue(label: "lambda-\(lifecycleId)").async { self.handle(context: context, payload: payload) { result in promise.succeed(result) diff --git a/Tests/SwiftAwsLambdaTests/Lambda+CodeableTest.swift b/Tests/SwiftAwsLambdaTests/Lambda+CodeableTest.swift index 04a2aec6..e1b84569 100644 --- a/Tests/SwiftAwsLambdaTests/Lambda+CodeableTest.swift +++ b/Tests/SwiftAwsLambdaTests/Lambda+CodeableTest.swift @@ -27,9 +27,10 @@ class CodableLambdaTest: XCTestCase { callback(.success(Response(requestId: payload.requestId))) } } + let maxTimes = Int.random(in: 1 ... 10) let configuration = Lambda.Configuration(lifecycle: .init(maxTimes: maxTimes)) - let result = Lambda.run(handler: Handler(), configuration: configuration) + let result = Lambda.run(configuration: configuration, handler: Handler()) assertLambdaLifecycleResult(result, shoudHaveRun: maxTimes) } @@ -43,9 +44,10 @@ class CodableLambdaTest: XCTestCase { callback(.failure(TestError("boom"))) } } + let maxTimes = Int.random(in: 1 ... 10) let configuration = Lambda.Configuration(lifecycle: .init(maxTimes: maxTimes)) - let result = Lambda.run(handler: Handler(), configuration: configuration) + let result = Lambda.run(configuration: configuration, handler: Handler()) assertLambdaLifecycleResult(result, shoudHaveRun: maxTimes) } diff --git a/Tests/SwiftAwsLambdaTests/Lambda+StringTest.swift b/Tests/SwiftAwsLambdaTests/Lambda+StringTest.swift index 3a17d425..62d0299f 100644 --- a/Tests/SwiftAwsLambdaTests/Lambda+StringTest.swift +++ b/Tests/SwiftAwsLambdaTests/Lambda+StringTest.swift @@ -27,9 +27,10 @@ class StringLambdaTest: XCTestCase { callback(.success(payload)) } } + let maxTimes = Int.random(in: 1 ... 10) let configuration = Lambda.Configuration(lifecycle: .init(maxTimes: maxTimes)) - let result = Lambda.run(handler: Handler(), configuration: configuration) + let result = Lambda.run(configuration: configuration, handler: Handler()) assertLambdaLifecycleResult(result, shoudHaveRun: maxTimes) } @@ -43,9 +44,10 @@ class StringLambdaTest: XCTestCase { callback(.failure(TestError("boom"))) } } + let maxTimes = Int.random(in: 1 ... 10) let configuration = Lambda.Configuration(lifecycle: .init(maxTimes: maxTimes)) - let result = Lambda.run(handler: Handler(), configuration: configuration) + let result = Lambda.run(configuration: configuration, handler: Handler()) assertLambdaLifecycleResult(result, shoudHaveRun: maxTimes) } diff --git a/Tests/SwiftAwsLambdaTests/LambdaRuntimeClientTest+XCTest.swift b/Tests/SwiftAwsLambdaTests/LambdaRuntimeClientTest+XCTest.swift index 1cde4609..acd4f5c3 100644 --- a/Tests/SwiftAwsLambdaTests/LambdaRuntimeClientTest+XCTest.swift +++ b/Tests/SwiftAwsLambdaTests/LambdaRuntimeClientTest+XCTest.swift @@ -27,12 +27,14 @@ extension LambdaRuntimeClientTest { return [ ("testSuccess", testSuccess), ("testFailure", testFailure), + ("testProviderFailure", testProviderFailure), ("testBootstrapFailure", testBootstrapFailure), ("testGetWorkServerInternalError", testGetWorkServerInternalError), ("testGetWorkServerNoBodyError", testGetWorkServerNoBodyError), ("testGetWorkServerMissingHeaderRequestIDError", testGetWorkServerMissingHeaderRequestIDError), ("testProcessResponseInternalServerError", testProcessResponseInternalServerError), ("testProcessErrorInternalServerError", testProcessErrorInternalServerError), + ("testProcessInitErrorOnProviderFailure", testProcessInitErrorOnProviderFailure), ("testProcessInitErrorOnBootstrapFailure", testProcessInitErrorOnBootstrapFailure), ] } diff --git a/Tests/SwiftAwsLambdaTests/LambdaRuntimeClientTest.swift b/Tests/SwiftAwsLambdaTests/LambdaRuntimeClientTest.swift index 20315724..c2a13bee 100644 --- a/Tests/SwiftAwsLambdaTests/LambdaRuntimeClientTest.swift +++ b/Tests/SwiftAwsLambdaTests/LambdaRuntimeClientTest.swift @@ -28,9 +28,17 @@ class LambdaRuntimeClientTest: XCTestCase { XCTAssertEqual(behavior.state, 10) } + func testProviderFailure() { + let behavior = Behavior() + XCTAssertThrowsError(try runLambda(behavior: behavior, provider: { _ in throw TestError("boom") })) { error in + XCTAssertEqual(error as? TestError, TestError("boom")) + } + XCTAssertEqual(behavior.state, 1) + } + func testBootstrapFailure() { let behavior = Behavior() - XCTAssertThrowsError(try runLambda(behavior: behavior, handler: FailedInitializerHandler("boom"))) { error in + XCTAssertThrowsError(try runLambda(behavior: behavior, handler: FailedBootstrapHandler("boom"))) { error in XCTAssertEqual(error as? TestError, TestError("boom")) } XCTAssertEqual(behavior.state, 1) @@ -165,6 +173,32 @@ class LambdaRuntimeClientTest: XCTestCase { } } + func testProcessInitErrorOnProviderFailure() { + struct Behavior: LambdaServerBehavior { + func getWork() -> GetWorkResult { + XCTFail("should not get work") + return .failure(.internalServerError) + } + + func processResponse(requestId: String, response: String?) -> Result { + XCTFail("should not report results") + return .failure(.internalServerError) + } + + func processError(requestId: String, error: ErrorResponse) -> Result { + XCTFail("should not report error") + return .failure(.internalServerError) + } + + func processInitError(error: ErrorResponse) -> Result { + return .failure(.internalServerError) + } + } + XCTAssertThrowsError(try runLambda(behavior: Behavior(), provider: { _ in throw TestError("boom") })) { error in + XCTAssertEqual(error as? TestError, TestError("boom")) + } + } + func testProcessInitErrorOnBootstrapFailure() { struct Behavior: LambdaServerBehavior { func getWork() -> GetWorkResult { @@ -186,7 +220,7 @@ class LambdaRuntimeClientTest: XCTestCase { return .failure(.internalServerError) } } - XCTAssertThrowsError(try runLambda(behavior: Behavior(), handler: FailedInitializerHandler("boom"))) { error in + XCTAssertThrowsError(try runLambda(behavior: Behavior(), handler: FailedBootstrapHandler("boom"))) { error in XCTAssertEqual(error as? TestError, TestError("boom")) } } diff --git a/Tests/SwiftAwsLambdaTests/LambdaTest+XCTest.swift b/Tests/SwiftAwsLambdaTests/LambdaTest+XCTest.swift index f65ce1fc..8610edc3 100644 --- a/Tests/SwiftAwsLambdaTests/LambdaTest+XCTest.swift +++ b/Tests/SwiftAwsLambdaTests/LambdaTest+XCTest.swift @@ -28,6 +28,8 @@ extension LambdaTest { ("testSuccess", testSuccess), ("testFailure", testFailure), ("testServerFailure", testServerFailure), + ("testProviderFailure", testProviderFailure), + ("testProviderFailureAndReportErrorFailure", testProviderFailureAndReportErrorFailure), ("testBootstrapFailure", testBootstrapFailure), ("testBootstrapFailureAndReportErrorFailure", testBootstrapFailureAndReportErrorFailure), ("testStartStop", testStartStop), diff --git a/Tests/SwiftAwsLambdaTests/LambdaTest.swift b/Tests/SwiftAwsLambdaTests/LambdaTest.swift index 48e1380c..336961f7 100644 --- a/Tests/SwiftAwsLambdaTests/LambdaTest.swift +++ b/Tests/SwiftAwsLambdaTests/LambdaTest.swift @@ -25,9 +25,9 @@ class LambdaTest: XCTestCase { let maxTimes = Int.random(in: 10 ... 20) let configuration = Lambda.Configuration(lifecycle: .init(maxTimes: maxTimes)) let handler = EchoHandler() - let result = Lambda.run(handler: handler, configuration: configuration) + let result = Lambda.run(configuration: configuration, handler: handler) assertLambdaLifecycleResult(result, shoudHaveRun: maxTimes) - XCTAssertEqual(handler.initializeCalls, 1) + XCTAssertEqual(handler.bootstrapped, 1) } func testFailure() { @@ -37,7 +37,7 @@ class LambdaTest: XCTestCase { let maxTimes = Int.random(in: 10 ... 20) let configuration = Lambda.Configuration(lifecycle: .init(maxTimes: maxTimes)) - let result = Lambda.run(handler: FailedHandler("boom"), configuration: configuration) + let result = Lambda.run(configuration: configuration, handler: FailedHandler("boom")) assertLambdaLifecycleResult(result, shoudHaveRun: maxTimes) } @@ -69,6 +69,66 @@ class LambdaTest: XCTestCase { assertLambdaLifecycleResult(result, shouldFailWithError: LambdaRuntimeClientError.badStatusCode(.internalServerError)) } + func testProviderFailure() { + let server = MockLambdaServer(behavior: Behavior()) + XCTAssertNoThrow(try server.start().wait()) + defer { XCTAssertNoThrow(try server.stop().wait()) } + + struct Behavior: LambdaServerBehavior { + func getWork() -> GetWorkResult { + XCTFail("should not get work") + return .failure(.internalServerError) + } + + func processResponse(requestId: String, response: String?) -> Result { + XCTFail("should not report a response") + return .failure(.internalServerError) + } + + func processError(requestId: String, error: ErrorResponse) -> Result { + XCTFail("should not report an error") + return .failure(.internalServerError) + } + + func processInitError(error: ErrorResponse) -> Result { + return .success(()) + } + } + + let result = Lambda.run(handler: FailedBootstrapHandler("kaboom")) + assertLambdaLifecycleResult(result, shouldFailWithError: TestError("kaboom")) + } + + func testProviderFailureAndReportErrorFailure() { + let server = MockLambdaServer(behavior: Behavior()) + XCTAssertNoThrow(try server.start().wait()) + defer { XCTAssertNoThrow(try server.stop().wait()) } + + struct Behavior: LambdaServerBehavior { + func getWork() -> GetWorkResult { + XCTFail("should not get work") + return .failure(.internalServerError) + } + + func processResponse(requestId: String, response: String?) -> Result { + XCTFail("should not report a response") + return .failure(.internalServerError) + } + + func processError(requestId: String, error: ErrorResponse) -> Result { + XCTFail("should not report an error") + return .failure(.internalServerError) + } + + func processInitError(error: ErrorResponse) -> Result { + return .failure(.internalServerError) + } + } + + let result = Lambda.run(provider: { _ in throw TestError("boom") }) + assertLambdaLifecycleResult(result, shouldFailWithError: TestError("boom")) + } + func testBootstrapFailure() { let server = MockLambdaServer(behavior: Behavior()) XCTAssertNoThrow(try server.start().wait()) @@ -95,7 +155,7 @@ class LambdaTest: XCTestCase { } } - let result = Lambda.run(handler: FailedInitializerHandler("kaboom")) + let result = Lambda.run(handler: FailedBootstrapHandler("kaboom")) assertLambdaLifecycleResult(result, shouldFailWithError: TestError("kaboom")) } @@ -125,7 +185,7 @@ class LambdaTest: XCTestCase { } } - let result = Lambda.run(handler: FailedInitializerHandler("kaboom")) + let result = Lambda.run(handler: FailedBootstrapHandler("kaboom")) assertLambdaLifecycleResult(result, shouldFailWithError: TestError("kaboom")) } @@ -146,7 +206,7 @@ class LambdaTest: XCTestCase { let eventLoopGroup = MultiThreadedEventLoopGroup(numberOfThreads: System.coreCount) defer { XCTAssertNoThrow(try eventLoopGroup.syncShutdownGracefully()) } - let future = Lambda.runAsync(eventLoopGroup: eventLoopGroup, handler: Handler(), configuration: configuration) + let future = Lambda.runAsync(eventLoopGroup: eventLoopGroup, configuration: configuration, provider: { _ in Handler() }) DispatchQueue(label: "test").async { usleep(100_000) kill(getpid(), signal.rawValue) @@ -164,9 +224,8 @@ class LambdaTest: XCTestCase { XCTAssertNoThrow(try server.start().wait()) defer { XCTAssertNoThrow(try server.stop().wait()) } - let configuration = Lambda.Configuration(lifecycle: .init(maxTimes: 1), - runtimeEngine: .init(requestTimeout: .milliseconds(timeout))) - let result = Lambda.run(handler: EchoHandler(), configuration: configuration) + let configuration = Lambda.Configuration(lifecycle: .init(maxTimes: 1), runtimeEngine: .init(requestTimeout: .milliseconds(timeout))) + let result = Lambda.run(configuration: configuration, handler: EchoHandler()) assertLambdaLifecycleResult(result, shouldFailWithError: LambdaRuntimeClientError.upstreamError("timeout")) } @@ -176,7 +235,7 @@ class LambdaTest: XCTestCase { defer { XCTAssertNoThrow(try server.stop().wait()) } let configuration = Lambda.Configuration(lifecycle: .init(maxTimes: 1)) - let result = Lambda.run(handler: EchoHandler(), configuration: configuration) + let result = Lambda.run(configuration: configuration, handler: EchoHandler()) assertLambdaLifecycleResult(result, shouldFailWithError: LambdaRuntimeClientError.upstreamError("connectionResetByPeer")) } @@ -187,7 +246,7 @@ class LambdaTest: XCTestCase { defer { XCTAssertNoThrow(try server.stop().wait()) } let configuration = Lambda.Configuration(lifecycle: .init(maxTimes: 1)) - let result = Lambda.run(handler: EchoHandler(), configuration: configuration) + let result = Lambda.run(configuration: configuration, handler: EchoHandler()) assertLambdaLifecycleResult(result, shoudHaveRun: 1) } @@ -198,7 +257,7 @@ class LambdaTest: XCTestCase { let maxTimes = 10 let configuration = Lambda.Configuration(lifecycle: .init(maxTimes: maxTimes)) - let result = Lambda.run(handler: EchoHandler(), configuration: configuration) + let result = Lambda.run(configuration: configuration, handler: EchoHandler()) assertLambdaLifecycleResult(result, shoudHaveRun: maxTimes) } @@ -209,7 +268,7 @@ class LambdaTest: XCTestCase { let maxTimes = 10 let configuration = Lambda.Configuration(lifecycle: .init(maxTimes: maxTimes)) - let result = Lambda.run(handler: EchoHandler(), configuration: configuration) + let result = Lambda.run(configuration: configuration, handler: EchoHandler()) assertLambdaLifecycleResult(result, shoudHaveRun: maxTimes) } } diff --git a/Tests/SwiftAwsLambdaTests/Utils.swift b/Tests/SwiftAwsLambdaTests/Utils.swift index 1363625f..42cd8f4b 100644 --- a/Tests/SwiftAwsLambdaTests/Utils.swift +++ b/Tests/SwiftAwsLambdaTests/Utils.swift @@ -18,23 +18,27 @@ import NIO import XCTest func runLambda(behavior: LambdaServerBehavior, handler: LambdaHandler) throws { + try runLambda(behavior: behavior, provider: { _ in handler }) +} + +func runLambda(behavior: LambdaServerBehavior, provider: @escaping LambdaHandlerProvider) throws { let eventLoopGroup = MultiThreadedEventLoopGroup(numberOfThreads: System.coreCount) defer { XCTAssertNoThrow(try eventLoopGroup.syncShutdownGracefully()) } let logger = Logger(label: "TestLogger") let configuration = Lambda.Configuration(runtimeEngine: .init(requestTimeout: .milliseconds(100))) - let runner = LambdaRunner(eventLoop: eventLoopGroup.next(), configuration: configuration, lambdaHandler: handler) + let runner = LambdaRunner(eventLoop: eventLoopGroup.next(), configuration: configuration) let server = try MockLambdaServer(behavior: behavior).start().wait() defer { XCTAssertNoThrow(try server.stop().wait()) } - try runner.initialize(logger: logger).flatMap { _ in - runner.run(logger: logger) + try runner.initialize(logger: logger, provider: provider).flatMap { handler in + runner.run(logger: logger, handler: handler) }.wait() } -final class EchoHandler: LambdaHandler { - var initializeCalls = 0 +final class EchoHandler: BootstrappedLambdaHandler { + var bootstrapped = 0 - public func initialize(callback: @escaping LambdaInitCallBack) { - self.initializeCalls += 1 + public func bootstrap(callback: @escaping LambdaInitCallBack) { + self.bootstrapped += 1 callback(.success(())) } @@ -55,19 +59,19 @@ struct FailedHandler: LambdaHandler { } } -struct FailedInitializerHandler: LambdaHandler { +struct FailedBootstrapHandler: BootstrappedLambdaHandler { private let reason: String public init(_ reason: String) { self.reason = reason } - func handle(context: Lambda.Context, payload: [UInt8], callback: @escaping LambdaCallback) { - callback(.failure(TestError("should not be called"))) + func bootstrap(callback: @escaping LambdaInitCallBack) { + callback(.failure(TestError(self.reason))) } - func initialize(callback: @escaping LambdaInitCallBack) { - callback(.failure(TestError(self.reason))) + func handle(context: Lambda.Context, payload: [UInt8], callback: @escaping LambdaCallback) { + callback(.failure(TestError("should not be called"))) } } From 5330176cf422940e947c1225500552716108964d Mon Sep 17 00:00:00 2001 From: tom doron Date: Mon, 9 Mar 2020 19:50:28 -0700 Subject: [PATCH 2/2] fixup --- Sources/SwiftAwsLambda/Lambda+Codable.swift | 1 - Sources/SwiftAwsLambda/Lambda+String.swift | 2 -- 2 files changed, 3 deletions(-) diff --git a/Sources/SwiftAwsLambda/Lambda+Codable.swift b/Sources/SwiftAwsLambda/Lambda+Codable.swift index aca6c8ad..306275ee 100644 --- a/Sources/SwiftAwsLambda/Lambda+Codable.swift +++ b/Sources/SwiftAwsLambda/Lambda+Codable.swift @@ -13,7 +13,6 @@ //===----------------------------------------------------------------------===// import Foundation // for JSON -import NIO /// Extension to the `Lambda` companion to enable execution of Lambdas that take and return `Codable` payloads. /// This is the most common way to use this library in AWS Lambda, since its JSON based. diff --git a/Sources/SwiftAwsLambda/Lambda+String.swift b/Sources/SwiftAwsLambda/Lambda+String.swift index 699ab1ac..8555368a 100644 --- a/Sources/SwiftAwsLambda/Lambda+String.swift +++ b/Sources/SwiftAwsLambda/Lambda+String.swift @@ -12,8 +12,6 @@ // //===----------------------------------------------------------------------===// -import NIO - /// Extension to the `Lambda` companion to enable execution of Lambdas that take and return `String` payloads. extension Lambda { /// Run a Lambda defined by implementing the `LambdaStringClosure` protocol.