Lambda factory as a protocol requirement. (#244)

This commit is contained in:
Fabian Fett
2022-01-13 19:10:20 +01:00
committed by GitHub
parent 5d235c0a3b
commit d06d22c0e0
15 changed files with 236 additions and 207 deletions
@@ -20,13 +20,16 @@ import NIOCore
// `EventLoopLambdaHandler` does not offload the Lambda processing to a separate thread
// while the closure-based handlers do.
struct MyLambda: EventLoopLambdaHandler {
@main
struct BenchmarkHandler: EventLoopLambdaHandler {
typealias Event = String
typealias Output = String
static func makeHandler(context: Lambda.InitializationContext) -> EventLoopFuture<Self> {
context.eventLoop.makeSucceededFuture(BenchmarkHandler())
}
func handle(_ event: String, context: LambdaContext) -> EventLoopFuture<String> {
context.eventLoop.makeSucceededFuture("hello, world!")
}
}
Lambda.run { $0.eventLoop.makeSucceededFuture(MyLambda()) }
@@ -20,13 +20,16 @@ import NIO
// `EventLoopLambdaHandler` does not offload the Lambda processing to a separate thread
// while the closure-based handlers do.
@main
struct BenchmarkHandler: EventLoopLambdaHandler {
typealias Event = String
typealias Output = String
static func makeHandler(context: Lambda.InitializationContext) -> EventLoopFuture<Self> {
context.eventLoop.makeSucceededFuture(BenchmarkHandler())
}
func handle(_ event: String, context: LambdaContext) -> EventLoopFuture<String> {
context.eventLoop.makeSucceededFuture("hello, world!")
}
}
Lambda.run { $0.eventLoop.makeSucceededFuture(BenchmarkHandler()) }
+18 -42
View File
@@ -24,27 +24,6 @@ import NIOCore
import NIOPosix
public enum Lambda {
public typealias Handler = ByteBufferLambdaHandler
/// `ByteBufferLambdaHandler` factory.
///
/// A function that takes a `InitializationContext` and returns an `EventLoopFuture` of a `ByteBufferLambdaHandler`
public typealias HandlerFactory = (InitializationContext) -> EventLoopFuture<Handler>
/// Run a Lambda defined by implementing the `LambdaHandler` protocol provided via a `LambdaHandlerFactory`.
/// Use this to initialize all your resources that you want to cache between invocations. This could be database connections and HTTP clients for example.
/// It is encouraged to use the given `EventLoop`'s conformance to `EventLoopGroup` when initializing NIO dependencies. This will improve overall performance.
///
/// - parameters:
/// - factory: A `ByteBufferLambdaHandler` factory.
///
/// - note: This is a blocking operation that will run forever, as its lifecycle is managed by the AWS Lambda Runtime Engine.
public static func run(_ factory: @escaping HandlerFactory) {
if case .failure(let error) = self.run(factory: factory) {
fatalError("\(error)")
}
}
/// Utility to access/read environment variables
public static func env(_ name: String) -> String? {
guard let value = getenv(name) else {
@@ -53,30 +32,27 @@ public enum Lambda {
return String(cString: value)
}
#if compiler(>=5.5) && canImport(_Concurrency)
// for testing and internal use
@available(macOS 12, iOS 15, tvOS 15, watchOS 8, *)
internal static func run<Handler: LambdaHandler>(configuration: Configuration = .init(), handlerType: Handler.Type) -> Result<Int, Error> {
self.run(configuration: configuration, factory: { context -> EventLoopFuture<ByteBufferLambdaHandler> in
let promise = context.eventLoop.makePromise(of: ByteBufferLambdaHandler.self)
promise.completeWithTask {
try await Handler(context: context)
}
return promise.futureResult
})
}
#endif
// for testing and internal use
internal static func run(configuration: Configuration = .init(), factory: @escaping HandlerFactory) -> Result<Int, Error> {
let _run = { (configuration: Configuration, factory: @escaping HandlerFactory) -> Result<Int, Error> in
/// Run a Lambda defined by implementing the ``ByteBufferLambdaHandler`` protocol.
/// The Runtime will manage the Lambdas application lifecycle automatically. It will invoke the
/// ``ByteBufferLambdaHandler/makeHandler(context:)`` to create a new Handler.
///
/// - parameters:
/// - configuration: A Lambda runtime configuration object
/// - handlerType: The Handler to create and invoke.
///
/// - note: This is a blocking operation that will run forever, as its lifecycle is managed by the AWS Lambda Runtime Engine.
internal static func run<Handler: ByteBufferLambdaHandler>(
configuration: Configuration = .init(),
handlerType: Handler.Type
) -> Result<Int, Error> {
let _run = { (configuration: Configuration) -> Result<Int, Error> in
Backtrace.install()
var logger = Logger(label: "Lambda")
logger.logLevel = configuration.general.logLevel
var result: Result<Int, Error>!
MultiThreadedEventLoopGroup.withCurrentThreadAsEventLoop { eventLoop in
let runtime = LambdaRuntime(eventLoop: eventLoop, logger: logger, configuration: configuration, factory: factory)
let runtime = LambdaRuntime<Handler>(eventLoop: eventLoop, logger: logger, configuration: configuration)
#if DEBUG
let signalSource = trap(signal: configuration.lifecycle.stopSignal) { signal in
logger.info("intercepted signal: \(signal)")
@@ -108,16 +84,16 @@ public enum Lambda {
if Lambda.env("LOCAL_LAMBDA_SERVER_ENABLED").flatMap(Bool.init) ?? false {
do {
return try Lambda.withLocalServer {
_run(configuration, factory)
_run(configuration)
}
} catch {
return .failure(error)
}
} else {
return _run(configuration, factory)
return _run(configuration)
}
#else
return _run(configuration, factory)
return _run(configuration)
#endif
}
}
@@ -20,7 +20,9 @@ import NIOCore
extension Lambda {
/// Lambda runtime initialization context.
/// The Lambda runtime generates and passes the `InitializationContext` to the Lambda factory as an argument.
/// The Lambda runtime generates and passes the `InitializationContext` to the Handlers
/// ``ByteBufferLambdaHandler/makeHandler(context:)`` or ``LambdaHandler/init(context:)``
/// as an argument.
public struct InitializationContext {
/// `Logger` to log with
///
@@ -18,7 +18,13 @@ import NIOCore
// MARK: - LambdaHandler
#if compiler(>=5.5) && canImport(_Concurrency)
/// Strongly typed, processing protocol for a Lambda that takes a user defined `Event` and returns a user defined `Output` async.
/// Strongly typed, processing protocol for a Lambda that takes a user defined
/// ``EventLoopLambdaHandler/Event`` and returns a user defined
/// ``EventLoopLambdaHandler/Output`` asynchronously.
///
/// - note: Most users should implement this protocol instead of the lower
/// level protocols ``EventLoopLambdaHandler`` and
/// ``ByteBufferLambdaHandler``.
@available(macOS 12, iOS 15, tvOS 15, watchOS 8, *)
public protocol LambdaHandler: EventLoopLambdaHandler {
/// The Lambda initialization method
@@ -42,6 +48,14 @@ public protocol LambdaHandler: EventLoopLambdaHandler {
@available(macOS 12, iOS 15, tvOS 15, watchOS 8, *)
extension LambdaHandler {
public static func makeHandler(context: Lambda.InitializationContext) -> EventLoopFuture<Self> {
let promise = context.eventLoop.makePromise(of: Self.self)
promise.completeWithTask {
try await Self(context: context)
}
return promise.futureResult
}
public func handle(_ event: Event, context: LambdaContext) -> EventLoopFuture<Output> {
let promise = context.eventLoop.makePromise(of: Output.self)
promise.completeWithTask {
@@ -51,25 +65,30 @@ extension LambdaHandler {
}
}
@available(macOS 12, iOS 15, tvOS 15, watchOS 8, *)
extension LambdaHandler {
public static func main() {
_ = Lambda.run(handlerType: Self.self)
}
}
#endif
// MARK: - EventLoopLambdaHandler
/// Strongly typed, `EventLoopFuture` based processing protocol for a Lambda that takes a user defined `Event` and returns a user defined `Output` asynchronously.
/// `EventLoopLambdaHandler` extends `ByteBufferLambdaHandler`, performing `ByteBuffer` -> `Event` decoding and `Output` -> `ByteBuffer` encoding.
/// Strongly typed, `EventLoopFuture` based processing protocol for a Lambda that takes a user
/// defined ``Event`` and returns a user defined ``Output`` asynchronously.
///
/// - note: To implement a Lambda, implement either `LambdaHandler` or the `EventLoopLambdaHandler` protocol.
/// The `LambdaHandler` will offload the Lambda execution to a `DispatchQueue` making processing safer but slower
/// The `EventLoopLambdaHandler` will execute the Lambda on the same `EventLoop` as the core runtime engine, making the processing faster but requires
/// more care from the implementation to never block the `EventLoop`.
/// ``EventLoopLambdaHandler`` extends ``ByteBufferLambdaHandler``, performing
/// `ByteBuffer` -> ``Event`` decoding and ``Output`` -> `ByteBuffer` encoding.
///
/// - note: To implement a Lambda, implement either ``LambdaHandler`` or the
/// ``EventLoopLambdaHandler`` protocol. The ``LambdaHandler`` will offload
/// the Lambda execution to an async Task making processing safer but slower (due to
/// fewer thread hops).
/// The ``EventLoopLambdaHandler`` will execute the Lambda on the same `EventLoop`
/// as the core runtime engine, making the processing faster but requires more care from the
/// implementation to never block the `EventLoop`. Implement this protocol only in performance
/// critical situations and implement ``LambdaHandler`` in all other circumstances.
public protocol EventLoopLambdaHandler: ByteBufferLambdaHandler {
/// The lambda functions input. In most cases this should be Codable. If your event originates from an
/// AWS service, have a look at [AWSLambdaEvents](https://github.com/swift-server/swift-aws-lambda-events),
/// which provides a number of commonly used AWS Event implementations.
associatedtype Event
/// The lambda functions output. Can be `Void`.
associatedtype Output
/// The Lambda handling method
@@ -135,9 +154,18 @@ extension EventLoopLambdaHandler where Output == Void {
/// An `EventLoopFuture` based processing protocol for a Lambda that takes a `ByteBuffer` and returns a `ByteBuffer?` asynchronously.
///
/// - note: This is a low level protocol designed to power the higher level `EventLoopLambdaHandler` and `LambdaHandler` based APIs.
/// - note: This is a low level protocol designed to power the higher level ``EventLoopLambdaHandler`` and
/// ``LambdaHandler`` based APIs.
/// Most users are not expected to use this protocol.
public protocol ByteBufferLambdaHandler {
/// Create your Lambda handler for the runtime.
///
/// Use this to initialize all your resources that you want to cache between invocations. This could be database
/// connections and HTTP clients for example. It is encouraged to use the given `EventLoop`'s conformance
/// to `EventLoopGroup` when initializing NIO dependencies. This will improve overall performance, as it
/// minimizes thread hopping.
static func makeHandler(context: Lambda.InitializationContext) -> EventLoopFuture<Self>
/// The Lambda handling method
/// Concrete Lambda handlers implement this method to provide the Lambda functionality.
///
@@ -163,6 +191,20 @@ extension ByteBufferLambdaHandler {
}
}
extension ByteBufferLambdaHandler {
/// Initializes and runs the lambda function.
///
/// If you precede your ``ByteBufferLambdaHandler`` conformer's declaration with the
/// [@main](https://docs.swift.org/swift-book/ReferenceManual/Attributes.html#ID626)
/// attribute, the system calls the conformer's `main()` method to launch the lambda function.
///
/// The lambda runtime provides a default implementation of the method that manages the launch
/// process.
public static func main() {
_ = Lambda.run(configuration: .init(), handlerType: Self.self)
}
}
@usableFromInline
enum CodecError: Error {
case requestDecoding(Error)
@@ -34,14 +34,14 @@ extension Lambda {
/// Run the user provided initializer. This *must* only be called once.
///
/// - Returns: An `EventLoopFuture<LambdaHandler>` fulfilled with the outcome of the initialization.
func initialize(logger: Logger, factory: @escaping HandlerFactory) -> EventLoopFuture<Handler> {
func initialize<Handler: ByteBufferLambdaHandler>(logger: Logger, handlerType: Handler.Type) -> EventLoopFuture<Handler> {
logger.debug("initializing lambda")
// 1. create the handler from the factory
// 2. report initialization error if one occured
let context = InitializationContext(logger: logger,
eventLoop: self.eventLoop,
allocator: self.allocator)
return factory(context)
return Handler.makeHandler(context: context)
// Hopping back to "our" EventLoop is important in case the factory returns a future
// that originated from a foreign EventLoop/EventLoopGroup.
// This can happen if the factory uses a library (let's say a database client) that manages its own threads/loops
@@ -56,7 +56,7 @@ extension Lambda {
}
}
func run(logger: Logger, handler: Handler) -> EventLoopFuture<Void> {
func run<Handler: ByteBufferLambdaHandler>(logger: Logger, handler: Handler) -> EventLoopFuture<Void> {
logger.debug("lambda invocation sequence starting")
// 1. request invocation from lambda runtime engine
self.isGettingNextInvocation = true
@@ -19,12 +19,11 @@ import NIOCore
/// `LambdaRuntime` manages the Lambda process lifecycle.
///
/// - note: It is intended to be used within a single `EventLoop`. For this reason this class is not thread safe.
public final class LambdaRuntime {
public final class LambdaRuntime<Handler: ByteBufferLambdaHandler> {
private let eventLoop: EventLoop
private let shutdownPromise: EventLoopPromise<Int>
private let logger: Logger
private let configuration: Lambda.Configuration
private let factory: Lambda.HandlerFactory
private var state = State.idle {
willSet {
@@ -38,17 +37,15 @@ public final class LambdaRuntime {
/// - parameters:
/// - eventLoop: An `EventLoop` to run the Lambda on.
/// - logger: A `Logger` to log the Lambda events.
/// - factory: A `LambdaHandlerFactory` to create the concrete Lambda handler.
public convenience init(eventLoop: EventLoop, logger: Logger, factory: @escaping Lambda.HandlerFactory) {
self.init(eventLoop: eventLoop, logger: logger, configuration: .init(), factory: factory)
public convenience init(eventLoop: EventLoop, logger: Logger) {
self.init(eventLoop: eventLoop, logger: logger, configuration: .init())
}
init(eventLoop: EventLoop, logger: Logger, configuration: Lambda.Configuration, factory: @escaping Lambda.HandlerFactory) {
init(eventLoop: EventLoop, logger: Logger, configuration: Lambda.Configuration) {
self.eventLoop = eventLoop
self.shutdownPromise = eventLoop.makePromise(of: Int.self)
self.logger = logger
self.configuration = configuration
self.factory = factory
}
deinit {
@@ -79,8 +76,8 @@ public final class LambdaRuntime {
logger[metadataKey: "lifecycleId"] = .string(self.configuration.lifecycle.id)
let runner = Lambda.Runner(eventLoop: self.eventLoop, configuration: self.configuration)
let startupFuture = runner.initialize(logger: logger, factory: self.factory)
startupFuture.flatMap { handler -> EventLoopFuture<(ByteBufferLambdaHandler, Result<Int, Error>)> in
let startupFuture = runner.initialize(logger: logger, handlerType: Handler.self)
startupFuture.flatMap { handler -> EventLoopFuture<(Handler, Result<Int, Error>)> in
// after the startup future has succeeded, we have a handler that we can use
// to `run` the lambda.
let finishedPromise = self.eventLoop.makePromise(of: Int.self)
@@ -88,7 +85,7 @@ public final class LambdaRuntime {
self.run(promise: finishedPromise)
return finishedPromise.futureResult.mapResult { (handler, $0) }
}
.flatMap { (handler, runnerResult) -> EventLoopFuture<Int> in
.flatMap { handler, runnerResult -> EventLoopFuture<Int> in
// after the lambda finishPromise has succeeded or failed we need to
// shutdown the handler
let shutdownContext = Lambda.ShutdownContext(logger: logger, eventLoop: self.eventLoop)
@@ -97,7 +94,7 @@ public final class LambdaRuntime {
// the runner result
logger.error("Error shutting down handler: \(error)")
throw Lambda.RuntimeError.shutdownError(shutdownError: error, runnerResult: runnerResult)
}.flatMapResult { (_) -> Result<Int, Error> in
}.flatMapResult { _ -> Result<Int, Error> in
// we had no error shutting down the lambda. let's return the runner's result
runnerResult
}
@@ -173,7 +170,7 @@ public final class LambdaRuntime {
private enum State {
case idle
case initializing
case active(Lambda.Runner, Lambda.Handler)
case active(Lambda.Runner, Handler)
case shuttingdown
case shutdown
@@ -159,6 +159,10 @@ class LambdaHandlerTest: XCTestCase {
typealias Event = String
typealias Output = String
static func makeHandler(context: Lambda.InitializationContext) -> EventLoopFuture<Handler> {
context.eventLoop.makeSucceededFuture(Handler())
}
func handle(_ event: String, context: LambdaContext) -> EventLoopFuture<String> {
context.eventLoop.makeSucceededFuture(event)
}
@@ -166,9 +170,7 @@ class LambdaHandlerTest: XCTestCase {
let maxTimes = Int.random(in: 1 ... 10)
let configuration = Lambda.Configuration(lifecycle: .init(maxTimes: maxTimes))
let result = Lambda.run(configuration: configuration, factory: { context in
context.eventLoop.makeSucceededFuture(Handler())
})
let result = Lambda.run(configuration: configuration, handlerType: Handler.self)
assertLambdaRuntimeResult(result, shoudHaveRun: maxTimes)
}
@@ -181,6 +183,10 @@ class LambdaHandlerTest: XCTestCase {
typealias Event = String
typealias Output = Void
static func makeHandler(context: Lambda.InitializationContext) -> EventLoopFuture<Handler> {
context.eventLoop.makeSucceededFuture(Handler())
}
func handle(_ event: String, context: LambdaContext) -> EventLoopFuture<Void> {
context.eventLoop.makeSucceededFuture(())
}
@@ -188,9 +194,7 @@ class LambdaHandlerTest: XCTestCase {
let maxTimes = Int.random(in: 1 ... 10)
let configuration = Lambda.Configuration(lifecycle: .init(maxTimes: maxTimes))
let result = Lambda.run(configuration: configuration, factory: { context in
context.eventLoop.makeSucceededFuture(Handler())
})
let result = Lambda.run(configuration: configuration, handlerType: Handler.self)
assertLambdaRuntimeResult(result, shoudHaveRun: maxTimes)
}
@@ -203,6 +207,10 @@ class LambdaHandlerTest: XCTestCase {
typealias Event = String
typealias Output = String
static func makeHandler(context: Lambda.InitializationContext) -> EventLoopFuture<Handler> {
context.eventLoop.makeSucceededFuture(Handler())
}
func handle(_ event: String, context: LambdaContext) -> EventLoopFuture<String> {
context.eventLoop.makeFailedFuture(TestError("boom"))
}
@@ -210,9 +218,7 @@ class LambdaHandlerTest: XCTestCase {
let maxTimes = Int.random(in: 1 ... 10)
let configuration = Lambda.Configuration(lifecycle: .init(maxTimes: maxTimes))
let result = Lambda.run(configuration: configuration, factory: { context in
context.eventLoop.makeSucceededFuture(Handler())
})
let result = Lambda.run(configuration: configuration, handlerType: Handler.self)
assertLambdaRuntimeResult(result, shoudHaveRun: maxTimes)
}
@@ -221,9 +227,21 @@ class LambdaHandlerTest: XCTestCase {
XCTAssertNoThrow(try server.start().wait())
defer { XCTAssertNoThrow(try server.stop().wait()) }
let result = Lambda.run(configuration: .init(), factory: { context in
context.eventLoop.makeFailedFuture(TestError("kaboom"))
})
struct Handler: EventLoopLambdaHandler {
typealias Event = String
typealias Output = String
static func makeHandler(context: Lambda.InitializationContext) -> EventLoopFuture<Handler> {
context.eventLoop.makeFailedFuture(TestError("kaboom"))
}
func handle(_ event: String, context: LambdaContext) -> EventLoopFuture<String> {
XCTFail("Must never be called")
return context.eventLoop.makeFailedFuture(TestError("boom"))
}
}
let result = Lambda.run(configuration: .init(), handlerType: Handler.self)
assertLambdaRuntimeResult(result, shouldFailWithError: TestError("kaboom"))
}
}
@@ -14,27 +14,48 @@
import AWSLambdaRuntimeCore
import NIOCore
import XCTest
struct EchoHandler: EventLoopLambdaHandler {
typealias Event = String
typealias Output = String
static func makeHandler(context: Lambda.InitializationContext) -> EventLoopFuture<EchoHandler> {
context.eventLoop.makeSucceededFuture(EchoHandler())
}
func handle(_ event: String, context: LambdaContext) -> EventLoopFuture<String> {
context.eventLoop.makeSucceededFuture(event)
}
}
struct FailedHandler: EventLoopLambdaHandler {
struct StartupError: Error {}
struct StartupErrorHandler: EventLoopLambdaHandler {
typealias Event = String
typealias Output = String
static func makeHandler(context: Lambda.InitializationContext) -> EventLoopFuture<StartupErrorHandler> {
context.eventLoop.makeFailedFuture(StartupError())
}
func handle(_ event: String, context: LambdaContext) -> EventLoopFuture<String> {
XCTFail("Must never be called")
return context.eventLoop.makeSucceededFuture(event)
}
}
struct RuntimeError: Error {}
struct RuntimeErrorHandler: EventLoopLambdaHandler {
typealias Event = String
typealias Output = Void
private let reason: String
init(_ reason: String) {
self.reason = reason
static func makeHandler(context: Lambda.InitializationContext) -> EventLoopFuture<RuntimeErrorHandler> {
context.eventLoop.makeSucceededFuture(RuntimeErrorHandler())
}
func handle(_ event: String, context: LambdaContext) -> EventLoopFuture<Void> {
context.eventLoop.makeFailedFuture(TestError(self.reason))
context.eventLoop.makeFailedFuture(RuntimeError())
}
}
@@ -40,12 +40,11 @@ class LambdaRunnerTest: XCTestCase {
return .failure(.internalServerError)
}
}
XCTAssertNoThrow(try runLambda(behavior: Behavior(), handler: EchoHandler()))
XCTAssertNoThrow(try runLambda(behavior: Behavior(), handlerType: EchoHandler.self))
}
func testFailure() {
struct Behavior: LambdaServerBehavior {
static let error = "boom"
let requestId = UUID().uuidString
func getInvocation() -> GetInvocationResult {
.success((requestId: self.requestId, event: "hello"))
@@ -58,7 +57,7 @@ class LambdaRunnerTest: XCTestCase {
func processError(requestId: String, error: ErrorResponse) -> Result<Void, ProcessErrorError> {
XCTAssertEqual(self.requestId, requestId, "expecting requestId to match")
XCTAssertEqual(Behavior.error, error.errorMessage, "expecting error to match")
XCTAssertEqual(String(describing: RuntimeError()), error.errorMessage, "expecting error to match")
return .success(())
}
@@ -67,6 +66,6 @@ class LambdaRunnerTest: XCTestCase {
return .failure(.internalServerError)
}
}
XCTAssertNoThrow(try runLambda(behavior: Behavior(), handler: FailedHandler(Behavior.error)))
XCTAssertNoThrow(try runLambda(behavior: Behavior(), handlerType: RuntimeErrorHandler.self))
}
}
@@ -24,20 +24,20 @@ import XCTest
class LambdaRuntimeClientTest: XCTestCase {
func testSuccess() {
let behavior = Behavior()
XCTAssertNoThrow(try runLambda(behavior: behavior, handler: EchoHandler()))
XCTAssertNoThrow(try runLambda(behavior: behavior, handlerType: EchoHandler.self))
XCTAssertEqual(behavior.state, 6)
}
func testFailure() {
let behavior = Behavior()
XCTAssertNoThrow(try runLambda(behavior: behavior, handler: FailedHandler("boom")))
XCTAssertNoThrow(try runLambda(behavior: behavior, handlerType: RuntimeErrorHandler.self))
XCTAssertEqual(behavior.state, 10)
}
func testBootstrapFailure() {
func testStartupFailure() {
let behavior = Behavior()
XCTAssertThrowsError(try runLambda(behavior: behavior, factory: { $0.eventLoop.makeFailedFuture(TestError("boom")) })) { error in
XCTAssertEqual(error as? TestError, TestError("boom"))
XCTAssertThrowsError(try runLambda(behavior: behavior, handlerType: StartupErrorHandler.self)) {
XCTAssert($0 is StartupError)
}
XCTAssertEqual(behavior.state, 1)
}
@@ -63,8 +63,8 @@ class LambdaRuntimeClientTest: XCTestCase {
return .failure(.internalServerError)
}
}
XCTAssertThrowsError(try runLambda(behavior: Behavior(), handler: EchoHandler())) { error in
XCTAssertEqual(error as? Lambda.RuntimeError, .badStatusCode(.internalServerError))
XCTAssertThrowsError(try runLambda(behavior: Behavior(), handlerType: EchoHandler.self)) {
XCTAssertEqual($0 as? Lambda.RuntimeError, .badStatusCode(.internalServerError))
}
}
@@ -89,8 +89,8 @@ class LambdaRuntimeClientTest: XCTestCase {
return .failure(.internalServerError)
}
}
XCTAssertThrowsError(try runLambda(behavior: Behavior(), handler: EchoHandler())) { error in
XCTAssertEqual(error as? Lambda.RuntimeError, .noBody)
XCTAssertThrowsError(try runLambda(behavior: Behavior(), handlerType: EchoHandler.self)) {
XCTAssertEqual($0 as? Lambda.RuntimeError, .noBody)
}
}
@@ -116,8 +116,8 @@ class LambdaRuntimeClientTest: XCTestCase {
return .failure(.internalServerError)
}
}
XCTAssertThrowsError(try runLambda(behavior: Behavior(), handler: EchoHandler())) { error in
XCTAssertEqual(error as? Lambda.RuntimeError, .invocationMissingHeader(AmazonHeaders.requestID))
XCTAssertThrowsError(try runLambda(behavior: Behavior(), handlerType: EchoHandler.self)) {
XCTAssertEqual($0 as? Lambda.RuntimeError, .invocationMissingHeader(AmazonHeaders.requestID))
}
}
@@ -141,8 +141,8 @@ class LambdaRuntimeClientTest: XCTestCase {
return .failure(.internalServerError)
}
}
XCTAssertThrowsError(try runLambda(behavior: Behavior(), handler: EchoHandler())) { error in
XCTAssertEqual(error as? Lambda.RuntimeError, .badStatusCode(.internalServerError))
XCTAssertThrowsError(try runLambda(behavior: Behavior(), handlerType: EchoHandler.self)) {
XCTAssertEqual($0 as? Lambda.RuntimeError, .badStatusCode(.internalServerError))
}
}
@@ -166,8 +166,8 @@ class LambdaRuntimeClientTest: XCTestCase {
return .failure(.internalServerError)
}
}
XCTAssertThrowsError(try runLambda(behavior: Behavior(), handler: FailedHandler("boom"))) { error in
XCTAssertEqual(error as? Lambda.RuntimeError, .badStatusCode(.internalServerError))
XCTAssertThrowsError(try runLambda(behavior: Behavior(), handlerType: RuntimeErrorHandler.self)) {
XCTAssertEqual($0 as? Lambda.RuntimeError, .badStatusCode(.internalServerError))
}
}
@@ -192,8 +192,8 @@ class LambdaRuntimeClientTest: XCTestCase {
.failure(.internalServerError)
}
}
XCTAssertThrowsError(try runLambda(behavior: Behavior(), factory: { $0.eventLoop.makeFailedFuture(TestError("boom")) })) { error in
XCTAssertEqual(error as? TestError, TestError("boom"))
XCTAssertThrowsError(try runLambda(behavior: Behavior(), handlerType: StartupErrorHandler.self)) {
XCTAssert($0 is StartupError)
}
}
@@ -29,37 +29,16 @@ class LambdaRuntimeTest: XCTestCase {
let eventLoop = eventLoopGroup.next()
let logger = Logger(label: "TestLogger")
let testError = TestError("kaboom")
let runtime = LambdaRuntime(eventLoop: eventLoop, logger: logger, factory: {
$0.eventLoop.makeFailedFuture(testError)
})
let runtime = LambdaRuntime<StartupErrorHandler>(eventLoop: eventLoop, logger: logger)
// eventLoop.submit in this case returns an EventLoopFuture<EventLoopFuture<ByteBufferHandler>>
// which is why we need `wait().wait()`
XCTAssertThrowsError(try eventLoop.flatSubmit { runtime.start() }.wait()) { error in
XCTAssertEqual(testError, error as? TestError)
XCTAssertThrowsError(try eventLoop.flatSubmit { runtime.start() }.wait()) {
XCTAssert($0 is StartupError)
}
XCTAssertThrowsError(_ = try runtime.shutdownFuture.wait()) { error in
XCTAssertEqual(testError, error as? TestError)
}
}
struct CallbackLambdaHandler: ByteBufferLambdaHandler {
let handler: (LambdaContext, ByteBuffer) -> (EventLoopFuture<ByteBuffer?>)
let shutdown: (Lambda.ShutdownContext) -> EventLoopFuture<Void>
init(_ handler: @escaping (LambdaContext, ByteBuffer) -> (EventLoopFuture<ByteBuffer?>), shutdown: @escaping (Lambda.ShutdownContext) -> EventLoopFuture<Void>) {
self.handler = handler
self.shutdown = shutdown
}
func handle(_ event: ByteBuffer, context: LambdaContext) -> EventLoopFuture<ByteBuffer?> {
self.handler(context, event)
}
func shutdown(context: Lambda.ShutdownContext) -> EventLoopFuture<Void> {
self.shutdown(context)
XCTAssertThrowsError(_ = try runtime.shutdownFuture.wait()) {
XCTAssert($0 is StartupError)
}
}
@@ -70,23 +49,14 @@ class LambdaRuntimeTest: XCTestCase {
let eventLoopGroup = MultiThreadedEventLoopGroup(numberOfThreads: 1)
defer { XCTAssertNoThrow(try eventLoopGroup.syncShutdownGracefully()) }
var count = 0
let handler = CallbackLambdaHandler({ XCTFail("Should not be reached"); return $0.eventLoop.makeSucceededFuture($1) }) { context in
count += 1
return context.eventLoop.makeSucceededFuture(())
}
let eventLoop = eventLoopGroup.next()
let logger = Logger(label: "TestLogger")
let runtime = LambdaRuntime(eventLoop: eventLoop, logger: logger, factory: {
$0.eventLoop.makeSucceededFuture(handler)
})
let runtime = LambdaRuntime<EchoHandler>(eventLoop: eventLoop, logger: logger)
XCTAssertNoThrow(_ = try eventLoop.flatSubmit { runtime.start() }.wait())
XCTAssertThrowsError(_ = try runtime.shutdownFuture.wait()) { error in
XCTAssertEqual(.badStatusCode(HTTPResponseStatus.internalServerError), error as? Lambda.RuntimeError)
XCTAssertThrowsError(_ = try runtime.shutdownFuture.wait()) {
XCTAssertEqual(.badStatusCode(HTTPResponseStatus.internalServerError), $0 as? Lambda.RuntimeError)
}
XCTAssertEqual(count, 1)
}
func testLambdaResultIfShutsdownIsUnclean() {
@@ -96,28 +66,38 @@ class LambdaRuntimeTest: XCTestCase {
let eventLoopGroup = MultiThreadedEventLoopGroup(numberOfThreads: 1)
defer { XCTAssertNoThrow(try eventLoopGroup.syncShutdownGracefully()) }
var count = 0
let handler = CallbackLambdaHandler({ XCTFail("Should not be reached"); return $0.eventLoop.makeSucceededFuture($1) }) { context in
count += 1
return context.eventLoop.makeFailedFuture(TestError("kaboom"))
struct ShutdownError: Error {}
struct ShutdownErrorHandler: EventLoopLambdaHandler {
typealias Event = String
typealias Output = Void
static func makeHandler(context: Lambda.InitializationContext) -> EventLoopFuture<ShutdownErrorHandler> {
context.eventLoop.makeSucceededFuture(ShutdownErrorHandler())
}
func handle(_ event: String, context: LambdaContext) -> EventLoopFuture<Void> {
context.eventLoop.makeSucceededVoidFuture()
}
func shutdown(context: Lambda.ShutdownContext) -> EventLoopFuture<Void> {
context.eventLoop.makeFailedFuture(ShutdownError())
}
}
let eventLoop = eventLoopGroup.next()
let logger = Logger(label: "TestLogger")
let runtime = LambdaRuntime(eventLoop: eventLoop, logger: logger, factory: {
$0.eventLoop.makeSucceededFuture(handler)
})
let runtime = LambdaRuntime<ShutdownErrorHandler>(eventLoop: eventLoop, logger: logger)
XCTAssertNoThrow(_ = try eventLoop.flatSubmit { runtime.start() }.wait())
XCTAssertThrowsError(_ = try runtime.shutdownFuture.wait()) { error in
XCTAssertNoThrow(try eventLoop.flatSubmit { runtime.start() }.wait())
XCTAssertThrowsError(try runtime.shutdownFuture.wait()) { error in
guard case Lambda.RuntimeError.shutdownError(let shutdownError, .failure(let runtimeError)) = error else {
XCTFail("Unexpected error"); return
XCTFail("Unexpected error: \(error)"); return
}
XCTAssertEqual(shutdownError as? TestError, TestError("kaboom"))
XCTAssert(shutdownError is ShutdownError)
XCTAssertEqual(runtimeError as? Lambda.RuntimeError, .badStatusCode(.internalServerError))
}
XCTAssertEqual(count, 1)
}
}
@@ -26,22 +26,18 @@ class LambdaTest: XCTestCase {
let maxTimes = Int.random(in: 10 ... 20)
let configuration = Lambda.Configuration(lifecycle: .init(maxTimes: maxTimes))
let result = Lambda.run(configuration: configuration, factory: {
$0.eventLoop.makeSucceededFuture(EchoHandler())
})
let result = Lambda.run(configuration: configuration, handlerType: EchoHandler.self)
assertLambdaRuntimeResult(result, shoudHaveRun: maxTimes)
}
func testFailure() {
let server = MockLambdaServer(behavior: Behavior(result: .failure(TestError("boom"))))
let server = MockLambdaServer(behavior: Behavior(result: .failure(RuntimeError())))
XCTAssertNoThrow(try server.start().wait())
defer { XCTAssertNoThrow(try server.stop().wait()) }
let maxTimes = Int.random(in: 10 ... 20)
let configuration = Lambda.Configuration(lifecycle: .init(maxTimes: maxTimes))
let result = Lambda.run(configuration: configuration, factory: {
$0.eventLoop.makeSucceededFuture(FailedHandler("boom"))
})
let result = Lambda.run(configuration: configuration, handlerType: RuntimeErrorHandler.self)
assertLambdaRuntimeResult(result, shoudHaveRun: maxTimes)
}
@@ -50,8 +46,8 @@ class LambdaTest: XCTestCase {
XCTAssertNoThrow(try server.start().wait())
defer { XCTAssertNoThrow(try server.stop().wait()) }
let result = Lambda.run(factory: { $0.eventLoop.makeFailedFuture(TestError("kaboom")) })
assertLambdaRuntimeResult(result, shouldFailWithError: TestError("kaboom"))
let result = Lambda.run(configuration: .init(), handlerType: StartupErrorHandler.self)
assertLambdaRuntimeResult(result, shouldFailWithError: StartupError())
}
func testBootstrapFailureAndReportErrorFailure() {
@@ -80,8 +76,8 @@ class LambdaTest: XCTestCase {
XCTAssertNoThrow(try server.start().wait())
defer { XCTAssertNoThrow(try server.stop().wait()) }
let result = Lambda.run(factory: { $0.eventLoop.makeFailedFuture(TestError("kaboom")) })
assertLambdaRuntimeResult(result, shouldFailWithError: TestError("kaboom"))
let result = Lambda.run(configuration: .init(), handlerType: StartupErrorHandler.self)
assertLambdaRuntimeResult(result, shouldFailWithError: StartupError())
}
func testStartStopInDebugMode() {
@@ -99,7 +95,7 @@ class LambdaTest: XCTestCase {
usleep(100_000)
kill(getpid(), signal.rawValue)
}
let result = Lambda.run(configuration: configuration, factory: { $0.eventLoop.makeSucceededFuture(EchoHandler()) })
let result = Lambda.run(configuration: configuration, handlerType: EchoHandler.self)
switch result {
case .success(let invocationCount):
@@ -118,9 +114,7 @@ class LambdaTest: XCTestCase {
let configuration = Lambda.Configuration(lifecycle: .init(maxTimes: 1),
runtimeEngine: .init(requestTimeout: .milliseconds(timeout)))
let result = Lambda.run(configuration: configuration, factory: {
$0.eventLoop.makeSucceededFuture(EchoHandler())
})
let result = Lambda.run(configuration: configuration, handlerType: EchoHandler.self)
assertLambdaRuntimeResult(result, shouldFailWithError: Lambda.RuntimeError.upstreamError("timeout"))
}
@@ -130,9 +124,7 @@ class LambdaTest: XCTestCase {
defer { XCTAssertNoThrow(try server.stop().wait()) }
let configuration = Lambda.Configuration(lifecycle: .init(maxTimes: 1))
let result = Lambda.run(configuration: configuration, factory: {
$0.eventLoop.makeSucceededFuture(EchoHandler())
})
let result = Lambda.run(configuration: configuration, handlerType: EchoHandler.self)
assertLambdaRuntimeResult(result, shouldFailWithError: Lambda.RuntimeError.upstreamError("connectionResetByPeer"))
}
@@ -143,9 +135,7 @@ class LambdaTest: XCTestCase {
defer { XCTAssertNoThrow(try server.stop().wait()) }
let configuration = Lambda.Configuration(lifecycle: .init(maxTimes: 1))
let result = Lambda.run(configuration: configuration, factory: {
$0.eventLoop.makeSucceededFuture(EchoHandler())
})
let result = Lambda.run(configuration: configuration, handlerType: EchoHandler.self)
assertLambdaRuntimeResult(result, shoudHaveRun: 1)
}
@@ -156,9 +146,7 @@ class LambdaTest: XCTestCase {
let maxTimes = 10
let configuration = Lambda.Configuration(lifecycle: .init(maxTimes: maxTimes))
let result = Lambda.run(configuration: configuration, factory: {
$0.eventLoop.makeSucceededFuture(EchoHandler())
})
let result = Lambda.run(configuration: configuration, handlerType: EchoHandler.self)
assertLambdaRuntimeResult(result, shoudHaveRun: maxTimes)
}
@@ -169,9 +157,7 @@ class LambdaTest: XCTestCase {
let maxTimes = 10
let configuration = Lambda.Configuration(lifecycle: .init(maxTimes: maxTimes))
let result = Lambda.run(configuration: configuration, factory: {
$0.eventLoop.makeSucceededFuture(EchoHandler())
})
let result = Lambda.run(configuration: configuration, handlerType: EchoHandler.self)
assertLambdaRuntimeResult(result, shoudHaveRun: maxTimes)
}
@@ -199,9 +185,7 @@ class LambdaTest: XCTestCase {
}
}
let result = Lambda.run(configuration: .init(), factory: {
$0.eventLoop.makeSucceededFuture(EchoHandler())
})
let result = Lambda.run(configuration: .init(), handlerType: EchoHandler.self)
assertLambdaRuntimeResult(result, shouldFailWithError: Lambda.RuntimeError.badStatusCode(.internalServerError))
}
@@ -271,9 +255,9 @@ class LambdaTest: XCTestCase {
private struct Behavior: LambdaServerBehavior {
let requestId: String
let event: String
let result: Result<String?, TestError>
let result: Result<String?, RuntimeError>
init(requestId: String = UUID().uuidString, event: String = "hello", result: Result<String?, TestError> = .success("hello")) {
init(requestId: String = UUID().uuidString, event: String = "hello", result: Result<String?, RuntimeError> = .success("hello")) {
self.requestId = requestId
self.event = event
self.result = result
@@ -302,7 +286,7 @@ private struct Behavior: LambdaServerBehavior {
XCTFail("unexpected to succeed, but failed with: \(error)")
return .failure(.internalServerError)
case .failure(let expected):
XCTAssertEqual(expected.description, error.errorMessage, "expecting error to match")
XCTAssertEqual(String(describing: expected), error.errorMessage, "expecting error to match")
return .success(())
}
}
+2 -6
View File
@@ -18,11 +18,7 @@ import NIOCore
import NIOPosix
import XCTest
func runLambda(behavior: LambdaServerBehavior, handler: Lambda.Handler) throws {
try runLambda(behavior: behavior, factory: { $0.eventLoop.makeSucceededFuture(handler) })
}
func runLambda(behavior: LambdaServerBehavior, factory: @escaping Lambda.HandlerFactory) throws {
func runLambda<Handler: ByteBufferLambdaHandler>(behavior: LambdaServerBehavior, handlerType: Handler.Type) throws {
let eventLoopGroup = MultiThreadedEventLoopGroup(numberOfThreads: 1)
defer { XCTAssertNoThrow(try eventLoopGroup.syncShutdownGracefully()) }
let logger = Logger(label: "TestLogger")
@@ -30,7 +26,7 @@ func runLambda(behavior: LambdaServerBehavior, factory: @escaping Lambda.Handler
let runner = Lambda.Runner(eventLoop: eventLoopGroup.next(), configuration: configuration)
let server = try MockLambdaServer(behavior: behavior).start().wait()
defer { XCTAssertNoThrow(try server.stop().wait()) }
try runner.initialize(logger: logger, factory: factory).flatMap { handler in
try runner.initialize(logger: logger, handlerType: handlerType).flatMap { handler in
runner.run(logger: logger, handler: handler)
}.wait()
}
@@ -41,7 +41,11 @@ class CodableLambdaTest: XCTestCase {
typealias Event = Request
typealias Output = Void
let expected: Request
var expected: Request?
static func makeHandler(context: Lambda.InitializationContext) -> EventLoopFuture<Handler> {
context.eventLoop.makeSucceededFuture(Handler())
}
func handle(_ event: Request, context: LambdaContext) -> EventLoopFuture<Void> {
XCTAssertEqual(event, self.expected)
@@ -66,7 +70,11 @@ class CodableLambdaTest: XCTestCase {
typealias Event = Request
typealias Output = Response
let expected: Request
var expected: Request?
static func makeHandler(context: Lambda.InitializationContext) -> EventLoopFuture<Handler> {
context.eventLoop.makeSucceededFuture(Handler())
}
func handle(_ event: Request, context: LambdaContext) -> EventLoopFuture<Response> {
XCTAssertEqual(event, self.expected)