diff --git a/Package.swift b/Package.swift index 78b1c36e..d7272b13 100644 --- a/Package.swift +++ b/Package.swift @@ -49,7 +49,6 @@ let package = Package( .product(name: "NIOFoundationCompat", package: "swift-nio"), ]), .testTarget(name: "AWSLambdaRuntimeTests", dependencies: [ - .byName(name: "AWSLambdaRuntimeCore"), .byName(name: "AWSLambdaRuntime"), ]), // testing helper diff --git a/Sources/AWSLambdaRuntimeCore/HTTPClient.swift b/Sources/AWSLambdaRuntimeCore/HTTPClient.swift index 7e724485..46f00fb6 100644 --- a/Sources/AWSLambdaRuntimeCore/HTTPClient.swift +++ b/Sources/AWSLambdaRuntimeCore/HTTPClient.swift @@ -172,8 +172,6 @@ private final class LambdaChannelHandler: ChannelDuplexHandler { private var state: State = .idle private var lastError: Error? - init() {} - func write(context: ChannelHandlerContext, data: NIOAny, promise: EventLoopPromise?) { guard case .idle = self.state else { preconditionFailure("invalid state, outstanding request") diff --git a/Sources/AWSLambdaRuntimeCore/Lambda+LocalServer.swift b/Sources/AWSLambdaRuntimeCore/Lambda+LocalServer.swift index 1f8c9e0f..8b84fcf6 100644 --- a/Sources/AWSLambdaRuntimeCore/Lambda+LocalServer.swift +++ b/Sources/AWSLambdaRuntimeCore/Lambda+LocalServer.swift @@ -36,17 +36,16 @@ extension Lambda { /// - body: Code to run within the context of the mock server. Typically this would be a Lambda.run function call. /// /// - note: This API is designed strictly for local testing and is behind a DEBUG flag - internal static func withLocalServer(invocationEndpoint: String? = nil, _ body: @escaping () -> Value) throws -> Value { + internal static func startLocalServer(invocationEndpoint: String? = nil) throws -> LocalLambda.Server { let server = LocalLambda.Server(invocationEndpoint: invocationEndpoint) try server.start().wait() - defer { try! server.stop() } - return body() + return server } } // MARK: - Local Mock Server -private enum LocalLambda { +internal enum LocalLambda { struct Server { private let logger: Logger private let group: EventLoopGroup diff --git a/Sources/AWSLambdaRuntimeCore/Lambda+String.swift b/Sources/AWSLambdaRuntimeCore/Lambda+String.swift index 8e3da3e5..1dc5c239 100644 --- a/Sources/AWSLambdaRuntimeCore/Lambda+String.swift +++ b/Sources/AWSLambdaRuntimeCore/Lambda+String.swift @@ -16,7 +16,7 @@ import NIOCore extension EventLoopLambdaHandler where Event == String { /// Implementation of a `ByteBuffer` to `String` decoding. @inlinable - public func decode(buffer: ByteBuffer) throws -> String { + public func decode(buffer: ByteBuffer) throws -> Event { var buffer = buffer guard let string = buffer.readString(length: buffer.readableBytes) else { fatalError("buffer.readString(length: buffer.readableBytes) failed") @@ -28,7 +28,7 @@ extension EventLoopLambdaHandler where Event == String { extension EventLoopLambdaHandler where Output == String { /// Implementation of `String` to `ByteBuffer` encoding. @inlinable - public func encode(allocator: ByteBufferAllocator, value: String) throws -> ByteBuffer? { + public func encode(allocator: ByteBufferAllocator, value: Output) throws -> ByteBuffer? { // FIXME: reusable buffer var buffer = allocator.buffer(capacity: value.utf8.count) buffer.writeString(value) diff --git a/Sources/AWSLambdaRuntimeCore/Lambda.swift b/Sources/AWSLambdaRuntimeCore/Lambda.swift index 0f976c76..1a008efc 100644 --- a/Sources/AWSLambdaRuntimeCore/Lambda.swift +++ b/Sources/AWSLambdaRuntimeCore/Lambda.swift @@ -33,60 +33,61 @@ public enum Lambda { /// - 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. + @discardableResult internal static func run( configuration: LambdaConfiguration = .init(), handlerType: Handler.Type - ) -> Result { - let _run = { (configuration: LambdaConfiguration) -> Result in - Backtrace.install() - var logger = Logger(label: "Lambda") - logger.logLevel = configuration.general.logLevel + ) throws -> Int { + var result: Result = .success(0) + + // start local server for debugging in DEBUG mode only + #if DEBUG + var localServer: LocalLambda.Server? = nil + if Handler.isLocalServer { + localServer = try Lambda.startLocalServer() + } + #endif - var result: Result! - MultiThreadedEventLoopGroup.withCurrentThreadAsEventLoop { eventLoop in - let runtime = LambdaRuntime(eventLoop: eventLoop, logger: logger, configuration: configuration) + Backtrace.install() + var logger = Logger(label: "Lambda") + logger.logLevel = configuration.general.logLevel + + MultiThreadedEventLoopGroup.withCurrentThreadAsEventLoop { eventLoop in + let runtime = LambdaRuntime(eventLoop: eventLoop, logger: logger, configuration: configuration) + #if DEBUG + let signalSource = trap(signal: configuration.lifecycle.stopSignal) { signal in + logger.info("intercepted signal: \(signal)") + runtime.shutdown() + } + #endif + + runtime.start().flatMap { + runtime.shutdownFuture + }.whenComplete { lifecycleResult in #if DEBUG - let signalSource = trap(signal: configuration.lifecycle.stopSignal) { signal in - logger.info("intercepted signal: \(signal)") - runtime.shutdown() - } + signalSource.cancel() #endif - - runtime.start().flatMap { - runtime.shutdownFuture - }.whenComplete { lifecycleResult in - #if DEBUG - signalSource.cancel() - #endif - eventLoop.shutdownGracefully { error in - if let error = error { - preconditionFailure("Failed to shutdown eventloop: \(error)") - } + eventLoop.shutdownGracefully { error in + if let error = error { + preconditionFailure("Failed to shutdown eventloop: \(error)") } - result = lifecycleResult } + result = lifecycleResult } - - logger.info("shutdown completed") - return result } - // start local server for debugging in DEBUG mode only + logger.info("shutdown completed") + #if DEBUG - if Lambda.env("LOCAL_LAMBDA_SERVER_ENABLED").flatMap(Bool.init) ?? false { - do { - return try Lambda.withLocalServer { - _run(configuration) - } - } catch { - return .failure(error) - } - } else { - return _run(configuration) - } - #else - return _run(configuration) + try localServer?.stop() #endif + + switch result { + case .success(let count): + return count + case .failure(let error): + throw error + } } } diff --git a/Sources/AWSLambdaRuntimeCore/LambdaConfiguration.swift b/Sources/AWSLambdaRuntimeCore/LambdaConfiguration.swift index 33d056f8..4817869b 100644 --- a/Sources/AWSLambdaRuntimeCore/LambdaConfiguration.swift +++ b/Sources/AWSLambdaRuntimeCore/LambdaConfiguration.swift @@ -17,26 +17,12 @@ import Logging import NIOCore internal struct LambdaConfiguration: CustomStringConvertible { - let general: General - let lifecycle: Lifecycle - let runtimeEngine: RuntimeEngine - - init() { - self.init(general: .init(), lifecycle: .init(), runtimeEngine: .init()) - } - - init(general: General? = nil, lifecycle: Lifecycle? = nil, runtimeEngine: RuntimeEngine? = nil) { - self.general = general ?? General() - self.lifecycle = lifecycle ?? Lifecycle() - self.runtimeEngine = runtimeEngine ?? RuntimeEngine() - } + var general: General = .init() + var lifecycle: Lifecycle = .init() + var runtimeEngine: RuntimeEngine = .init() struct General: CustomStringConvertible { - let logLevel: Logger.Level - - init(logLevel: Logger.Level? = nil) { - self.logLevel = logLevel ?? Lambda.env("LOG_LEVEL").flatMap(Logger.Level.init) ?? .info - } + var logLevel = Lambda.env("LOG_LEVEL").flatMap(Logger.Level.init) ?? .info var description: String { "\(General.self)(logLevel: \(self.logLevel))" @@ -44,15 +30,10 @@ internal struct LambdaConfiguration: CustomStringConvertible { } struct Lifecycle: CustomStringConvertible { - let id: String - let maxTimes: Int - let stopSignal: Signal - - init(id: String? = nil, maxTimes: Int? = nil, stopSignal: Signal? = nil) { - self.id = id ?? "\(DispatchTime.now().uptimeNanoseconds)" - self.maxTimes = maxTimes ?? Lambda.env("MAX_REQUESTS").flatMap(Int.init) ?? 0 - self.stopSignal = stopSignal ?? Lambda.env("STOP_SIGNAL").flatMap(Int32.init).flatMap(Signal.init) ?? Signal.TERM - precondition(self.maxTimes >= 0, "maxTimes must be equal or larger than 0") + var id: String = "\(DispatchTime.now().uptimeNanoseconds)" + var stopSignal: Signal = Lambda.env("STOP_SIGNAL").flatMap(Int32.init).flatMap(Signal.init) ?? Signal.TERM + var maxTimes: Int = Lambda.env("MAX_REQUESTS").flatMap(Int.init) ?? 0 { + didSet { precondition(self.maxTimes >= 0, "maxTimes must be equal or larger than 0") } } var description: String { diff --git a/Sources/AWSLambdaRuntimeCore/LambdaContext.swift b/Sources/AWSLambdaRuntimeCore/LambdaContext.swift index 7118a5ac..684ebbce 100644 --- a/Sources/AWSLambdaRuntimeCore/LambdaContext.swift +++ b/Sources/AWSLambdaRuntimeCore/LambdaContext.swift @@ -14,13 +14,12 @@ #if compiler(>=5.6) @preconcurrency import Dispatch -@preconcurrency import Logging -@preconcurrency import NIOCore #else import Dispatch +#endif + import Logging import NIOCore -#endif // MARK: - InitializationContext @@ -45,14 +44,14 @@ public struct LambdaInitializationContext: _AWSLambdaSendable { /// ``LambdaTerminator`` to register shutdown operations. public let terminator: LambdaTerminator - + init(logger: Logger, eventLoop: EventLoop, allocator: ByteBufferAllocator, terminator: LambdaTerminator) { self.eventLoop = eventLoop self.logger = logger self.allocator = allocator self.terminator = terminator } - + /// This interface is not part of the public API and must not be used by adopters. This API is not part of semver versioning. public static func __forTestsOnly( logger: Logger, @@ -193,24 +192,4 @@ public struct LambdaContext: CustomDebugStringConvertible, _AWSLambdaSendable { public var debugDescription: String { "\(Self.self)(requestID: \(self.requestID), traceID: \(self.traceID), invokedFunctionARN: \(self.invokedFunctionARN), cognitoIdentity: \(self.cognitoIdentity ?? "nil"), clientContext: \(self.clientContext ?? "nil"), deadline: \(self.deadline))" } - - /// This interface is not part of the public API and must not be used by adopters. This API is not part of semver versioning. - public static func __forTestsOnly( - requestID: String, - traceID: String, - invokedFunctionARN: String, - timeout: DispatchTimeInterval, - logger: Logger, - eventLoop: EventLoop - ) -> LambdaContext { - LambdaContext( - requestID: requestID, - traceID: traceID, - invokedFunctionARN: invokedFunctionARN, - deadline: .now() + timeout, - logger: logger, - eventLoop: eventLoop, - allocator: ByteBufferAllocator() - ) - } } diff --git a/Sources/AWSLambdaRuntimeCore/LambdaHandler.swift b/Sources/AWSLambdaRuntimeCore/LambdaHandler.swift index 8f4b9f6e..a0053ee3 100644 --- a/Sources/AWSLambdaRuntimeCore/LambdaHandler.swift +++ b/Sources/AWSLambdaRuntimeCore/LambdaHandler.swift @@ -27,10 +27,24 @@ import NIOCore /// ``ByteBufferLambdaHandler``. @available(macOS 12, iOS 15, tvOS 15, watchOS 8, *) public protocol LambdaHandler: EventLoopLambdaHandler { - /// The Lambda initialization method. + /// 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 Request = Event + /// The lambda functions output. Can be `Void`. + associatedtype Response = Output + + /// The empty Lambda initialization method. /// Use this method to initialize resources that will be used in every request. /// /// Examples for this can be HTTP or database clients. + init() async throws + + /// The Lambda initialization method. + /// Use this method to initialize resources that will be used in every request. Defaults to + /// calling ``LambdaHandler/init()`` + /// + /// Examples for this can be HTTP or database clients. /// - parameters: /// - context: Runtime ``LambdaInitializationContext``. init(context: LambdaInitializationContext) async throws @@ -39,15 +53,23 @@ public protocol LambdaHandler: EventLoopLambdaHandler { /// Concrete Lambda handlers implement this method to provide the Lambda functionality. /// /// - parameters: - /// - event: Event of type `Event` representing the event or request. + /// - request: Event of type `Request` representing the event or request. /// - context: Runtime ``LambdaContext``. /// /// - Returns: A Lambda result ot type `Output`. - func handle(_ event: Event, context: LambdaContext) async throws -> Output + func handle(request: Request, context: LambdaContext) async throws -> Response } @available(macOS 12, iOS 15, tvOS 15, watchOS 8, *) extension LambdaHandler { + public init() async throws { + throw LambdaRuntimeError.upstreamError("You should not indirectly initialize LambdaHandler as an explicit type") + } + + public init(context: LambdaInitializationContext) async throws { + try await self.init() + } + public static func makeHandler(context: LambdaInitializationContext) -> EventLoopFuture { let promise = context.eventLoop.makePromise(of: Self.self) promise.completeWithTask { @@ -55,31 +77,36 @@ extension LambdaHandler { } return promise.futureResult } +} - public func handle(_ event: Event, context: LambdaContext) -> EventLoopFuture { +@available(macOS 12, iOS 15, tvOS 15, watchOS 8, *) +extension LambdaHandler where Request == Event, Response == Output { + public func handle(event: Event, context: LambdaContext) -> EventLoopFuture { let promise = context.eventLoop.makePromise(of: Output.self) // using an unchecked sendable wrapper for the handler // this is safe since lambda runtime is designed to calls the handler serially let handler = UncheckedSendableHandler(underlying: self) promise.completeWithTask { - try await handler.handle(event, context: context) + try await handler.handle(request: event, context: context) } return promise.futureResult } } + + /// unchecked sendable wrapper for the handler /// this is safe since lambda runtime is designed to calls the handler serially @available(macOS 12, iOS 15, tvOS 15, watchOS 8, *) -fileprivate struct UncheckedSendableHandler: @unchecked Sendable where Event == Underlying.Event, Output == Underlying.Output { +fileprivate struct UncheckedSendableHandler: @unchecked Sendable where Request == Underlying.Request, Response == Underlying.Response { let underlying: Underlying init(underlying: Underlying) { self.underlying = underlying } - func handle(_ event: Event, context: LambdaContext) async throws -> Output { - try await self.underlying.handle(event, context: context) + func handle(request: Request, context: LambdaContext) async throws -> Response { + try await self.underlying.handle(request: request, context: context) } } #endif @@ -106,18 +133,18 @@ public protocol EventLoopLambdaHandler: ByteBufferLambdaHandler { /// which provides a number of commonly used AWS Event implementations. associatedtype Event /// The lambda functions output. Can be `Void`. - associatedtype Output + associatedtype Output = Void /// The Lambda handling method. /// Concrete Lambda handlers implement this method to provide the Lambda functionality. /// /// - parameters: - /// - context: Runtime ``LambdaContext``. /// - event: Event of type `Event` representing the event or request. + /// - context: Runtime ``LambdaContext``. /// /// - Returns: An `EventLoopFuture` to report the result of the Lambda back to the runtime engine. /// The `EventLoopFuture` should be completed with either a response of type ``Output`` or an `Error`. - func handle(_ event: Event, context: LambdaContext) -> EventLoopFuture + func handle(event: Event, context: LambdaContext) -> EventLoopFuture /// Encode a response of type ``Output`` to `ByteBuffer`. /// Concrete Lambda handlers implement this method to provide coding functionality. @@ -141,15 +168,15 @@ public protocol EventLoopLambdaHandler: ByteBufferLambdaHandler { extension EventLoopLambdaHandler { /// Driver for `ByteBuffer` -> ``Event`` decoding and ``Output`` -> `ByteBuffer` encoding @inlinable - public func handle(_ event: ByteBuffer, context: LambdaContext) -> EventLoopFuture { + public func handle(buffer: ByteBuffer, context: LambdaContext) -> EventLoopFuture { let input: Event do { - input = try self.decode(buffer: event) + input = try self.decode(buffer: buffer) } catch { return context.eventLoop.makeFailedFuture(CodecError.requestDecoding(error)) } - return self.handle(input, context: context).flatMapThrowing { output in + return self.handle(event: input, context: context).flatMapThrowing { output in do { return try self.encode(allocator: context.allocator, value: output) } catch { @@ -162,7 +189,7 @@ extension EventLoopLambdaHandler { /// Implementation of `ByteBuffer` to `Void` decoding. extension EventLoopLambdaHandler where Output == Void { @inlinable - public func encode(allocator: ByteBufferAllocator, value: Void) throws -> ByteBuffer? { + public func encode(allocator: ByteBufferAllocator, value: Output) throws -> ByteBuffer? { nil } } @@ -176,6 +203,37 @@ extension EventLoopLambdaHandler where Output == Void { /// ``LambdaHandler`` based APIs. /// Most users are not expected to use this protocol. public protocol ByteBufferLambdaHandler { + #if DEBUG + /// Informs the Lambda whether or not it should run as a local server. + /// + /// If not implemented, this variable has a default value that follows this priority: + /// + /// 1. The value of the `LOCAL_LAMBDA_SERVER_ENABLED` environment variable. + /// 2. If the env variable isn't found, defaults to `true` if running directly in Xcode. If + /// running tests in Xcode, this defaults to `false`, instead. + /// 3. If not running in Xcode and the env variable is missing, defaults to `false`. + /// 4. No-op on `RELEASE` (production) builds. The AWSLambdaRuntime framework will not compile + /// any logic accessing this property. + /// + /// The following is an example of this variable within a simple ``LambdaHandler`` that uses + /// `Codable` types for its associated `Event` and `Output` types: + /// + /// ```swift + /// import AWSLambdaRuntime + /// import Foundation + /// + /// @main + /// struct EntryHandler: LambdaHandler { + /// static let isLocalServer = true + /// + /// func handle(_ event: Event, context: LambdaContext) async throws -> Output { + /// try await client.processResponse(for: event) + /// } + /// } + /// ``` + static var isLocalServer: Bool { get } + #endif + /// 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 @@ -188,15 +246,32 @@ public protocol ByteBufferLambdaHandler { /// Concrete Lambda handlers implement this method to provide the Lambda functionality. /// /// - parameters: + /// - buffer: The event or input payload encoded as `ByteBuffer`. /// - context: Runtime ``LambdaContext``. - /// - event: The event or input payload encoded as `ByteBuffer`. /// /// - Returns: An `EventLoopFuture` to report the result of the Lambda back to the runtime engine. /// The `EventLoopFuture` should be completed with either a response encoded as `ByteBuffer` or an `Error`. - func handle(_ event: ByteBuffer, context: LambdaContext) -> EventLoopFuture + func handle(buffer: ByteBuffer, context: LambdaContext) -> EventLoopFuture } extension ByteBufferLambdaHandler { + #if DEBUG + /// If running this Lambda in Xcode, this value defaults to `true` if the presence of the + /// `LOCAL_LAMBDA_SERVER_ENABLED` environment variable cannot be found. Otherwise, this value + /// defaults to `false`. + public static var isLocalServer: Bool { + var enabled = Lambda.env("LOCAL_LAMBDA_SERVER_ENABLED").flatMap(Bool.init) + + #if Xcode + if enabled == nil { + enabled = true + } + #endif + + return enabled ?? false + } + #endif + /// Initializes and runs the Lambda function. /// /// If you precede your ``ByteBufferLambdaHandler`` conformer's declaration with the @@ -205,8 +280,8 @@ extension ByteBufferLambdaHandler { /// /// 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) + public static func main() throws { + try Lambda.run(configuration: .init(), handlerType: Self.self) } } diff --git a/Sources/AWSLambdaRuntimeCore/LambdaRunner.swift b/Sources/AWSLambdaRuntimeCore/LambdaRunner.swift index 54e90f95..8f9040a8 100644 --- a/Sources/AWSLambdaRuntimeCore/LambdaRunner.swift +++ b/Sources/AWSLambdaRuntimeCore/LambdaRunner.swift @@ -74,7 +74,7 @@ internal final class LambdaRunner { invocation: invocation ) logger.debug("sending invocation to lambda handler \(handler)") - return handler.handle(bytes, context: context) + return handler.handle(buffer: bytes, context: context) // Hopping back to "our" EventLoop is important in case the handler returns a future that // originiated from a foreign EventLoop/EventLoopGroup. // This can happen if the handler uses a library (lets say a DB client) that manages its own threads/loops diff --git a/Sources/AWSLambdaRuntimeCore/LambdaRuntimeClient.swift b/Sources/AWSLambdaRuntimeCore/LambdaRuntimeClient.swift index 29d04b9d..bcc65736 100644 --- a/Sources/AWSLambdaRuntimeCore/LambdaRuntimeClient.swift +++ b/Sources/AWSLambdaRuntimeCore/LambdaRuntimeClient.swift @@ -13,13 +13,8 @@ //===----------------------------------------------------------------------===// import Logging -#if compiler(>=5.6) -@preconcurrency import NIOCore -@preconcurrency import NIOHTTP1 -#else import NIOCore import NIOHTTP1 -#endif /// An HTTP based client for AWS Runtime Engine. This encapsulates the RESTful methods exposed by the Runtime Engine: /// * /runtime/invocation/next diff --git a/Sources/AWSLambdaRuntimeCore/Terminator.swift b/Sources/AWSLambdaRuntimeCore/Terminator.swift index 6a0b65c0..2696cc90 100644 --- a/Sources/AWSLambdaRuntimeCore/Terminator.swift +++ b/Sources/AWSLambdaRuntimeCore/Terminator.swift @@ -20,11 +20,7 @@ import NIOCore public final class LambdaTerminator { fileprivate typealias Handler = (EventLoop) -> EventLoopFuture - private var storage: Storage - - init() { - self.storage = Storage() - } + private var storage: Storage = Storage() /// Register a shutdown handler with the terminator. /// diff --git a/Sources/AWSLambdaTesting/Lambda+Testing.swift b/Sources/AWSLambdaTesting/Lambda+Testing.swift index 11e2bf89..9a995cfd 100644 --- a/Sources/AWSLambdaTesting/Lambda+Testing.swift +++ b/Sources/AWSLambdaTesting/Lambda+Testing.swift @@ -17,11 +17,6 @@ // // func test() { // struct MyLambda: LambdaHandler { -// typealias Event = String -// typealias Output = String -// -// init(context: Lambda.InitializationContext) {} -// // func handle(_ event: String, context: LambdaContext) async throws -> String { // "echo" + event // } @@ -40,6 +35,8 @@ import Logging import NIOCore import NIOPosix +@testable import AWSLambdaRuntimeCore + @available(macOS 12, iOS 15, tvOS 15, watchOS 8, *) extension Lambda { public struct TestConfig { @@ -77,13 +74,14 @@ extension Lambda { eventLoop: eventLoop ) - let context = LambdaContext.__forTestsOnly( + let context = LambdaContext( requestID: config.requestID, traceID: config.traceID, invokedFunctionARN: config.invokedFunctionARN, - timeout: config.timeout, + deadline: .now() + config.timeout, logger: logger, - eventLoop: eventLoop + eventLoop: eventLoop, + allocator: ByteBufferAllocator() ) promise.completeWithTask { @@ -92,7 +90,7 @@ extension Lambda { let handler = try promise.futureResult.wait() return try eventLoop.flatSubmit { - handler.handle(event, context: context) + handler.handle(event: event, context: context) }.wait() } } diff --git a/Tests/AWSLambdaRuntimeCoreTests/LambdaHandlerTest.swift b/Tests/AWSLambdaRuntimeCoreTests/LambdaHandlerTest.swift index 98b49ca7..05c41257 100644 --- a/Tests/AWSLambdaRuntimeCoreTests/LambdaHandlerTest.swift +++ b/Tests/AWSLambdaRuntimeCoreTests/LambdaHandlerTest.swift @@ -18,6 +18,11 @@ import XCTest class LambdaHandlerTest: XCTestCase { #if compiler(>=5.5) && canImport(_Concurrency) + + override func setUp() { + super.setUp() + setenv("LOCAL_LAMBDA_SERVER_ENABLED", "false", 1) + } // MARK: - LambdaHandler @@ -28,9 +33,6 @@ class LambdaHandlerTest: XCTestCase { defer { XCTAssertNoThrow(try server.stop().wait()) } struct TestBootstrapHandler: LambdaHandler { - typealias Event = String - typealias Output = String - var initialized = false init(context: LambdaInitializationContext) async throws { @@ -39,15 +41,17 @@ class LambdaHandlerTest: XCTestCase { self.initialized = true } - func handle(_ event: String, context: LambdaContext) async throws -> String { - event + func handle(request: String, context: LambdaContext) async throws -> String { + request } } let maxTimes = Int.random(in: 10 ... 20) let configuration = LambdaConfiguration(lifecycle: .init(maxTimes: maxTimes)) - let result = Lambda.run(configuration: configuration, handlerType: TestBootstrapHandler.self) - assertLambdaRuntimeResult(result, shoudHaveRun: maxTimes) + assertLambdaRuntimeResult( + try Lambda.run(configuration: configuration, handlerType: TestBootstrapHandler.self), + shouldHaveRun: maxTimes + ) } @available(macOS 12, iOS 15, tvOS 15, watchOS 8, *) @@ -57,9 +61,6 @@ class LambdaHandlerTest: XCTestCase { defer { XCTAssertNoThrow(try server.stop().wait()) } struct TestBootstrapHandler: LambdaHandler { - typealias Event = String - typealias Output = Void - var initialized = false init(context: LambdaInitializationContext) async throws { @@ -68,15 +69,18 @@ class LambdaHandlerTest: XCTestCase { throw TestError("kaboom") } - func handle(_ event: String, context: LambdaContext) async throws { + func handle(request: String, context: LambdaContext) async throws { XCTFail("How can this be called if init failed") } } let maxTimes = Int.random(in: 10 ... 20) let configuration = LambdaConfiguration(lifecycle: .init(maxTimes: maxTimes)) - let result = Lambda.run(configuration: configuration, handlerType: TestBootstrapHandler.self) - assertLambdaRuntimeResult(result, shouldFailWithError: TestError("kaboom")) + + assertLambdaRuntimeResult( + try Lambda.run(configuration: configuration, handlerType: TestBootstrapHandler.self), + shouldFailWithError: TestError("kaboom") + ) } @available(macOS 12, iOS 15, tvOS 15, watchOS 8, *) @@ -86,20 +90,17 @@ class LambdaHandlerTest: XCTestCase { defer { XCTAssertNoThrow(try server.stop().wait()) } struct Handler: LambdaHandler { - typealias Event = String - typealias Output = String - - init(context: LambdaInitializationContext) {} - - func handle(_ event: String, context: LambdaContext) async throws -> String { - event + func handle(request: String, context: LambdaContext) async throws -> String { + request } } let maxTimes = Int.random(in: 1 ... 10) let configuration = LambdaConfiguration(lifecycle: .init(maxTimes: maxTimes)) - let result = Lambda.run(configuration: configuration, handlerType: Handler.self) - assertLambdaRuntimeResult(result, shoudHaveRun: maxTimes) + assertLambdaRuntimeResult( + try Lambda.run(configuration: configuration, handlerType: Handler.self), + shouldHaveRun: maxTimes + ) } @available(macOS 12, iOS 15, tvOS 15, watchOS 8, *) @@ -109,19 +110,16 @@ class LambdaHandlerTest: XCTestCase { defer { XCTAssertNoThrow(try server.stop().wait()) } struct Handler: LambdaHandler { - typealias Event = String - typealias Output = Void - - init(context: LambdaInitializationContext) {} - - func handle(_ event: String, context: LambdaContext) async throws {} + func handle(request: String, context: LambdaContext) async throws {} } let maxTimes = Int.random(in: 1 ... 10) let configuration = LambdaConfiguration(lifecycle: .init(maxTimes: maxTimes)) - let result = Lambda.run(configuration: configuration, handlerType: Handler.self) - assertLambdaRuntimeResult(result, shoudHaveRun: maxTimes) + assertLambdaRuntimeResult( + try Lambda.run(configuration: configuration, handlerType: Handler.self), + shouldHaveRun: maxTimes + ) } @available(macOS 12, iOS 15, tvOS 15, watchOS 8, *) @@ -131,20 +129,17 @@ class LambdaHandlerTest: XCTestCase { defer { XCTAssertNoThrow(try server.stop().wait()) } struct Handler: LambdaHandler { - typealias Event = String - typealias Output = String - - init(context: LambdaInitializationContext) {} - - func handle(_ event: String, context: LambdaContext) async throws -> String { + func handle(request: String, context: LambdaContext) async throws -> String { throw TestError("boom") } } let maxTimes = Int.random(in: 1 ... 10) let configuration = LambdaConfiguration(lifecycle: .init(maxTimes: maxTimes)) - let result = Lambda.run(configuration: configuration, handlerType: Handler.self) - assertLambdaRuntimeResult(result, shoudHaveRun: maxTimes) + assertLambdaRuntimeResult( + try Lambda.run(configuration: configuration, handlerType: Handler.self), + shouldHaveRun: maxTimes + ) } #endif @@ -156,22 +151,21 @@ class LambdaHandlerTest: XCTestCase { defer { XCTAssertNoThrow(try server.stop().wait()) } struct Handler: EventLoopLambdaHandler { - typealias Event = String - typealias Output = String - static func makeHandler(context: LambdaInitializationContext) -> EventLoopFuture { context.eventLoop.makeSucceededFuture(Handler()) } - func handle(_ event: String, context: LambdaContext) -> EventLoopFuture { + func handle(event: String, context: LambdaContext) -> EventLoopFuture { context.eventLoop.makeSucceededFuture(event) } } let maxTimes = Int.random(in: 1 ... 10) let configuration = LambdaConfiguration(lifecycle: .init(maxTimes: maxTimes)) - let result = Lambda.run(configuration: configuration, handlerType: Handler.self) - assertLambdaRuntimeResult(result, shoudHaveRun: maxTimes) + assertLambdaRuntimeResult( + try Lambda.run(configuration: configuration, handlerType: Handler.self), + shouldHaveRun: maxTimes + ) } func testVoidEventLoopSuccess() { @@ -180,22 +174,21 @@ class LambdaHandlerTest: XCTestCase { defer { XCTAssertNoThrow(try server.stop().wait()) } struct Handler: EventLoopLambdaHandler { - typealias Event = String - typealias Output = Void - static func makeHandler(context: LambdaInitializationContext) -> EventLoopFuture { context.eventLoop.makeSucceededFuture(Handler()) } - func handle(_ event: String, context: LambdaContext) -> EventLoopFuture { + func handle(event: String, context: LambdaContext) -> EventLoopFuture { context.eventLoop.makeSucceededFuture(()) } } let maxTimes = Int.random(in: 1 ... 10) let configuration = LambdaConfiguration(lifecycle: .init(maxTimes: maxTimes)) - let result = Lambda.run(configuration: configuration, handlerType: Handler.self) - assertLambdaRuntimeResult(result, shoudHaveRun: maxTimes) + assertLambdaRuntimeResult( + try Lambda.run(configuration: configuration, handlerType: Handler.self), + shouldHaveRun: maxTimes + ) } func testEventLoopFailure() { @@ -204,22 +197,21 @@ class LambdaHandlerTest: XCTestCase { defer { XCTAssertNoThrow(try server.stop().wait()) } struct Handler: EventLoopLambdaHandler { - typealias Event = String - typealias Output = String - static func makeHandler(context: LambdaInitializationContext) -> EventLoopFuture { context.eventLoop.makeSucceededFuture(Handler()) } - func handle(_ event: String, context: LambdaContext) -> EventLoopFuture { + func handle(event: String, context: LambdaContext) -> EventLoopFuture { context.eventLoop.makeFailedFuture(TestError("boom")) } } let maxTimes = Int.random(in: 1 ... 10) let configuration = LambdaConfiguration(lifecycle: .init(maxTimes: maxTimes)) - let result = Lambda.run(configuration: configuration, handlerType: Handler.self) - assertLambdaRuntimeResult(result, shoudHaveRun: maxTimes) + assertLambdaRuntimeResult( + try Lambda.run(configuration: configuration, handlerType: Handler.self), + shouldHaveRun: maxTimes + ) } func testEventLoopBootstrapFailure() { @@ -228,21 +220,20 @@ class LambdaHandlerTest: XCTestCase { defer { XCTAssertNoThrow(try server.stop().wait()) } struct Handler: EventLoopLambdaHandler { - typealias Event = String - typealias Output = String - static func makeHandler(context: LambdaInitializationContext) -> EventLoopFuture { context.eventLoop.makeFailedFuture(TestError("kaboom")) } - func handle(_ event: String, context: LambdaContext) -> EventLoopFuture { + func handle(event: String, context: LambdaContext) -> EventLoopFuture { 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")) + assertLambdaRuntimeResult( + try Lambda.run(configuration: .init(), handlerType: Handler.self), + shouldFailWithError: TestError("kaboom") + ) } } diff --git a/Tests/AWSLambdaRuntimeCoreTests/LambdaHandlers.swift b/Tests/AWSLambdaRuntimeCoreTests/LambdaHandlers.swift index c2f3fc9e..ff10a142 100644 --- a/Tests/AWSLambdaRuntimeCoreTests/LambdaHandlers.swift +++ b/Tests/AWSLambdaRuntimeCoreTests/LambdaHandlers.swift @@ -17,14 +17,11 @@ import NIOCore import XCTest struct EchoHandler: EventLoopLambdaHandler { - typealias Event = String - typealias Output = String - static func makeHandler(context: LambdaInitializationContext) -> EventLoopFuture { context.eventLoop.makeSucceededFuture(EchoHandler()) } - func handle(_ event: String, context: LambdaContext) -> EventLoopFuture { + func handle(event: String, context: LambdaContext) -> EventLoopFuture { context.eventLoop.makeSucceededFuture(event) } } @@ -32,14 +29,11 @@ struct EchoHandler: EventLoopLambdaHandler { struct StartupError: Error {} struct StartupErrorHandler: EventLoopLambdaHandler { - typealias Event = String - typealias Output = String - static func makeHandler(context: LambdaInitializationContext) -> EventLoopFuture { context.eventLoop.makeFailedFuture(StartupError()) } - func handle(_ event: String, context: LambdaContext) -> EventLoopFuture { + func handle(event: String, context: LambdaContext) -> EventLoopFuture { XCTFail("Must never be called") return context.eventLoop.makeSucceededFuture(event) } @@ -48,14 +42,11 @@ struct StartupErrorHandler: EventLoopLambdaHandler { struct RuntimeError: Error {} struct RuntimeErrorHandler: EventLoopLambdaHandler { - typealias Event = String - typealias Output = Void - static func makeHandler(context: LambdaInitializationContext) -> EventLoopFuture { context.eventLoop.makeSucceededFuture(RuntimeErrorHandler()) } - func handle(_ event: String, context: LambdaContext) -> EventLoopFuture { + func handle(event: String, context: LambdaContext) -> EventLoopFuture { context.eventLoop.makeFailedFuture(RuntimeError()) } } diff --git a/Tests/AWSLambdaRuntimeCoreTests/LambdaRuntimeTest.swift b/Tests/AWSLambdaRuntimeCoreTests/LambdaRuntimeTest.swift index 64bc4384..1d28e7e1 100644 --- a/Tests/AWSLambdaRuntimeCoreTests/LambdaRuntimeTest.swift +++ b/Tests/AWSLambdaRuntimeCoreTests/LambdaRuntimeTest.swift @@ -71,9 +71,6 @@ class LambdaRuntimeTest: XCTestCase { } struct ShutdownErrorHandler: EventLoopLambdaHandler { - typealias Event = String - typealias Output = Void - static func makeHandler(context: LambdaInitializationContext) -> EventLoopFuture { // register shutdown operation context.terminator.register(name: "test 1", handler: { eventLoop in @@ -94,7 +91,7 @@ class LambdaRuntimeTest: XCTestCase { return context.eventLoop.makeSucceededFuture(ShutdownErrorHandler()) } - func handle(_ event: String, context: LambdaContext) -> EventLoopFuture { + func handle(event: String, context: LambdaContext) -> EventLoopFuture { context.eventLoop.makeSucceededVoidFuture() } } diff --git a/Tests/AWSLambdaRuntimeCoreTests/LambdaTest.swift b/Tests/AWSLambdaRuntimeCoreTests/LambdaTest.swift index 1cf6aa1a..fc869e46 100644 --- a/Tests/AWSLambdaRuntimeCoreTests/LambdaTest.swift +++ b/Tests/AWSLambdaRuntimeCoreTests/LambdaTest.swift @@ -13,49 +13,50 @@ //===----------------------------------------------------------------------===// @testable import AWSLambdaRuntimeCore -#if compiler(>=5.6) -@preconcurrency import Logging -@preconcurrency import NIOPosix -#else import Logging import NIOPosix -#endif import NIOCore import XCTest class LambdaTest: XCTestCase { - func testSuccess() { + func testSuccess() throws { let server = MockLambdaServer(behavior: Behavior()) XCTAssertNoThrow(try server.start().wait()) defer { XCTAssertNoThrow(try server.stop().wait()) } let maxTimes = Int.random(in: 10 ... 20) let configuration = LambdaConfiguration(lifecycle: .init(maxTimes: maxTimes)) - let result = Lambda.run(configuration: configuration, handlerType: EchoHandler.self) - assertLambdaRuntimeResult(result, shoudHaveRun: maxTimes) + assertLambdaRuntimeResult( + try Lambda.run(configuration: configuration, handlerType: EchoHandler.self), + shouldHaveRun: maxTimes + ) } - func testFailure() { + func testFailure() throws { 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 = LambdaConfiguration(lifecycle: .init(maxTimes: maxTimes)) - let result = Lambda.run(configuration: configuration, handlerType: RuntimeErrorHandler.self) - assertLambdaRuntimeResult(result, shoudHaveRun: maxTimes) + assertLambdaRuntimeResult( + try Lambda.run(configuration: configuration, handlerType: RuntimeErrorHandler.self), + shouldHaveRun: maxTimes + ) } - func testBootstrapFailure() { + func testBootstrapFailure() throws { let server = MockLambdaServer(behavior: FailedBootstrapBehavior()) XCTAssertNoThrow(try server.start().wait()) defer { XCTAssertNoThrow(try server.stop().wait()) } - let result = Lambda.run(configuration: .init(), handlerType: StartupErrorHandler.self) - assertLambdaRuntimeResult(result, shouldFailWithError: StartupError()) + assertLambdaRuntimeResult( + try Lambda.run(configuration: .init(), handlerType: StartupErrorHandler.self), + shouldFailWithError: StartupError() + ) } - func testBootstrapFailureAndReportErrorFailure() { + func testBootstrapFailureAndReportErrorFailure() throws { struct Behavior: LambdaServerBehavior { func getInvocation() -> GetInvocationResult { XCTFail("should not get invocation") @@ -81,18 +82,20 @@ class LambdaTest: XCTestCase { XCTAssertNoThrow(try server.start().wait()) defer { XCTAssertNoThrow(try server.stop().wait()) } - let result = Lambda.run(configuration: .init(), handlerType: StartupErrorHandler.self) - assertLambdaRuntimeResult(result, shouldFailWithError: StartupError()) + assertLambdaRuntimeResult( + try Lambda.run(configuration: .init(), handlerType: StartupErrorHandler.self), + shouldFailWithError: StartupError() + ) } - func testStartStopInDebugMode() { + func testStartStopInDebugMode() throws { let server = MockLambdaServer(behavior: Behavior()) XCTAssertNoThrow(try server.start().wait()) defer { XCTAssertNoThrow(try server.stop().wait()) } let signal = Signal.ALRM let maxTimes = 1000 - let configuration = LambdaConfiguration(lifecycle: .init(maxTimes: maxTimes, stopSignal: signal)) + let configuration = LambdaConfiguration(lifecycle: .init(stopSignal: signal, maxTimes: maxTimes)) DispatchQueue(label: "test").async { // we need to schedule the signal before we start the long running `Lambda.run`, since @@ -100,15 +103,9 @@ class LambdaTest: XCTestCase { usleep(100_000) kill(getpid(), signal.rawValue) } - let result = Lambda.run(configuration: configuration, handlerType: EchoHandler.self) - - switch result { - case .success(let invocationCount): - XCTAssertGreaterThan(invocationCount, 0, "should have stopped before any request made") - XCTAssertLessThan(invocationCount, maxTimes, "should have stopped before \(maxTimes)") - case .failure(let error): - XCTFail("Unexpected error: \(error)") - } + let result = try Lambda.run(configuration: configuration, handlerType: EchoHandler.self) + XCTAssertGreaterThan(result, 0, "should have stopped before any request made") + XCTAssertLessThan(result, maxTimes, "should have stopped before \(maxTimes)") } func testTimeout() { @@ -119,8 +116,10 @@ class LambdaTest: XCTestCase { let configuration = LambdaConfiguration(lifecycle: .init(maxTimes: 1), runtimeEngine: .init(requestTimeout: .milliseconds(timeout))) - let result = Lambda.run(configuration: configuration, handlerType: EchoHandler.self) - assertLambdaRuntimeResult(result, shouldFailWithError: LambdaRuntimeError.upstreamError("timeout")) + assertLambdaRuntimeResult( + try Lambda.run(configuration: configuration, handlerType: EchoHandler.self), + shouldFailWithError: LambdaRuntimeError.upstreamError("timeout") + ) } func testDisconnect() { @@ -129,8 +128,10 @@ class LambdaTest: XCTestCase { defer { XCTAssertNoThrow(try server.stop().wait()) } let configuration = LambdaConfiguration(lifecycle: .init(maxTimes: 1)) - let result = Lambda.run(configuration: configuration, handlerType: EchoHandler.self) - assertLambdaRuntimeResult(result, shouldFailWithError: LambdaRuntimeError.upstreamError("connectionResetByPeer")) + assertLambdaRuntimeResult( + try Lambda.run(configuration: configuration, handlerType: EchoHandler.self), + shouldFailWithError: LambdaRuntimeError.upstreamError("connectionResetByPeer") + ) } func testBigEvent() { @@ -140,8 +141,10 @@ class LambdaTest: XCTestCase { defer { XCTAssertNoThrow(try server.stop().wait()) } let configuration = LambdaConfiguration(lifecycle: .init(maxTimes: 1)) - let result = Lambda.run(configuration: configuration, handlerType: EchoHandler.self) - assertLambdaRuntimeResult(result, shoudHaveRun: 1) + assertLambdaRuntimeResult( + try Lambda.run(configuration: configuration, handlerType: EchoHandler.self), + shouldHaveRun: 1 + ) } func testKeepAliveServer() { @@ -151,8 +154,10 @@ class LambdaTest: XCTestCase { let maxTimes = 10 let configuration = LambdaConfiguration(lifecycle: .init(maxTimes: maxTimes)) - let result = Lambda.run(configuration: configuration, handlerType: EchoHandler.self) - assertLambdaRuntimeResult(result, shoudHaveRun: maxTimes) + assertLambdaRuntimeResult( + try Lambda.run(configuration: configuration, handlerType: EchoHandler.self), + shouldHaveRun: maxTimes + ) } func testNoKeepAliveServer() { @@ -162,8 +167,10 @@ class LambdaTest: XCTestCase { let maxTimes = 10 let configuration = LambdaConfiguration(lifecycle: .init(maxTimes: maxTimes)) - let result = Lambda.run(configuration: configuration, handlerType: EchoHandler.self) - assertLambdaRuntimeResult(result, shoudHaveRun: maxTimes) + assertLambdaRuntimeResult( + try Lambda.run(configuration: configuration, handlerType: EchoHandler.self), + shouldHaveRun: maxTimes + ) } func testServerFailure() { @@ -190,8 +197,10 @@ class LambdaTest: XCTestCase { } } - let result = Lambda.run(configuration: .init(), handlerType: EchoHandler.self) - assertLambdaRuntimeResult(result, shouldFailWithError: LambdaRuntimeError.badStatusCode(.internalServerError)) + assertLambdaRuntimeResult( + try Lambda.run(configuration: .init(), handlerType: EchoHandler.self), + shouldFailWithError: LambdaRuntimeError.badStatusCode(.internalServerError) + ) } func testDeadline() { @@ -259,14 +268,11 @@ class LambdaTest: XCTestCase { #if compiler(>=5.6) func testSendable() async throws { struct Handler: EventLoopLambdaHandler { - typealias Event = String - typealias Output = String - static func makeHandler(context: LambdaInitializationContext) -> EventLoopFuture { context.eventLoop.makeSucceededFuture(Handler()) } - func handle(_ event: String, context: LambdaContext) -> EventLoopFuture { + func handle(event: String, context: LambdaContext) -> EventLoopFuture { context.eventLoop.makeSucceededFuture("hello") } } diff --git a/Tests/AWSLambdaRuntimeCoreTests/Utils.swift b/Tests/AWSLambdaRuntimeCoreTests/Utils.swift index 49cd7708..62ef1f73 100644 --- a/Tests/AWSLambdaRuntimeCoreTests/Utils.swift +++ b/Tests/AWSLambdaRuntimeCoreTests/Utils.swift @@ -32,18 +32,20 @@ func runLambda(behavior: LambdaServerBehavior, }.wait() } -func assertLambdaRuntimeResult(_ result: Result, shoudHaveRun: Int = 0, shouldFailWithError: Error? = nil, file: StaticString = #file, line: UInt = #line) { - switch result { - case .success where shouldFailWithError != nil: - XCTFail("should fail with \(shouldFailWithError!)", file: file, line: line) - case .success(let count) where shouldFailWithError == nil: - XCTAssertEqual(shoudHaveRun, count, "should have run \(shoudHaveRun) times", file: file, line: line) - case .failure(let error) where shouldFailWithError == nil: - XCTFail("should succeed, but failed with \(error)", file: file, line: line) - case .failure(let error) where shouldFailWithError != nil: - XCTAssertEqual(String(describing: shouldFailWithError!), String(describing: error), "expected error to mactch", file: file, line: line) - default: - XCTFail("invalid state") +func assertLambdaRuntimeResult(_ result: @autoclosure () throws -> Int, shouldHaveRun: Int = 0, shouldFailWithError: Error? = nil, file: StaticString = #file, line: UInt = #line) { + do { + let count = try result() + if let shouldFailWithError = shouldFailWithError { + XCTFail("should fail with \(shouldFailWithError)", file: file, line: line) + } else { + XCTAssertEqual(shouldHaveRun, count, "should have run \(shouldHaveRun) times", file: file, line: line) + } + } catch { + if let shouldFailWithError = shouldFailWithError { + XCTAssertEqual(String(describing: shouldFailWithError), String(describing: error), "expected error to match", file: file, line: line) + } else { + XCTFail("should succeed, but failed with \(error)", file: file, line: line) + } } } diff --git a/Tests/AWSLambdaRuntimeTests/Lambda+CodableTest.swift b/Tests/AWSLambdaRuntimeTests/Lambda+CodableTest.swift index eceaa2d8..6608e920 100644 --- a/Tests/AWSLambdaRuntimeTests/Lambda+CodableTest.swift +++ b/Tests/AWSLambdaRuntimeTests/Lambda+CodableTest.swift @@ -38,16 +38,13 @@ class CodableLambdaTest: XCTestCase { var outputBuffer: ByteBuffer? struct Handler: EventLoopLambdaHandler { - typealias Event = Request - typealias Output = Void - var expected: Request? static func makeHandler(context: LambdaInitializationContext) -> EventLoopFuture { context.eventLoop.makeSucceededFuture(Handler()) } - func handle(_ event: Request, context: LambdaContext) -> EventLoopFuture { + func handle(event: Request, context: LambdaContext) -> EventLoopFuture { XCTAssertEqual(event, self.expected) return context.eventLoop.makeSucceededVoidFuture() } @@ -56,7 +53,7 @@ class CodableLambdaTest: XCTestCase { let handler = Handler(expected: request) XCTAssertNoThrow(inputBuffer = try JSONEncoder().encode(request, using: self.allocator)) - XCTAssertNoThrow(outputBuffer = try handler.handle(XCTUnwrap(inputBuffer), context: self.newContext()).wait()) + XCTAssertNoThrow(outputBuffer = try handler.handle(buffer: XCTUnwrap(inputBuffer), context: self.newContext()).wait()) XCTAssertNil(outputBuffer) } @@ -67,16 +64,13 @@ class CodableLambdaTest: XCTestCase { var response: Response? struct Handler: EventLoopLambdaHandler { - typealias Event = Request - typealias Output = Response - var expected: Request? static func makeHandler(context: LambdaInitializationContext) -> EventLoopFuture { context.eventLoop.makeSucceededFuture(Handler()) } - func handle(_ event: Request, context: LambdaContext) -> EventLoopFuture { + func handle(event: Request, context: LambdaContext) -> EventLoopFuture { XCTAssertEqual(event, self.expected) return context.eventLoop.makeSucceededFuture(Response(requestId: event.requestId)) } @@ -85,7 +79,7 @@ class CodableLambdaTest: XCTestCase { let handler = Handler(expected: request) XCTAssertNoThrow(inputBuffer = try JSONEncoder().encode(request, using: self.allocator)) - XCTAssertNoThrow(outputBuffer = try handler.handle(XCTUnwrap(inputBuffer), context: self.newContext()).wait()) + XCTAssertNoThrow(outputBuffer = try handler.handle(buffer: XCTUnwrap(inputBuffer), context: self.newContext()).wait()) XCTAssertNoThrow(response = try JSONDecoder().decode(Response.self, from: XCTUnwrap(outputBuffer))) XCTAssertEqual(response?.requestId, request.requestId) } @@ -94,15 +88,10 @@ class CodableLambdaTest: XCTestCase { @available(macOS 12, iOS 15, tvOS 15, watchOS 8, *) func testCodableVoidHandler() { struct Handler: LambdaHandler { - typealias Event = Request - typealias Output = Void - var expected: Request? - init(context: LambdaInitializationContext) async throws {} - - func handle(_ event: Request, context: LambdaContext) async throws { - XCTAssertEqual(event, self.expected) + func handle(request: Request, context: LambdaContext) async throws { + XCTAssertEqual(request, self.expected) } } @@ -115,7 +104,7 @@ class CodableLambdaTest: XCTestCase { handler.expected = request XCTAssertNoThrow(inputBuffer = try JSONEncoder().encode(request, using: self.allocator)) - XCTAssertNoThrow(outputBuffer = try handler.handle(XCTUnwrap(inputBuffer), context: self.newContext()).wait()) + XCTAssertNoThrow(outputBuffer = try handler.handle(buffer: XCTUnwrap(inputBuffer), context: self.newContext()).wait()) XCTAssertNil(outputBuffer) } } @@ -123,16 +112,11 @@ class CodableLambdaTest: XCTestCase { @available(macOS 12, iOS 15, tvOS 15, watchOS 8, *) func testCodableHandler() { struct Handler: LambdaHandler { - typealias Event = Request - typealias Output = Response - var expected: Request? - init(context: LambdaInitializationContext) async throws {} - - func handle(_ event: Request, context: LambdaContext) async throws -> Response { - XCTAssertEqual(event, self.expected) - return Response(requestId: event.requestId) + func handle(request: Request, context: LambdaContext) async throws -> Response { + XCTAssertEqual(request, self.expected) + return Response(requestId: request.requestId) } } @@ -146,7 +130,7 @@ class CodableLambdaTest: XCTestCase { handler.expected = request XCTAssertNoThrow(inputBuffer = try JSONEncoder().encode(request, using: self.allocator)) - XCTAssertNoThrow(outputBuffer = try handler.handle(XCTUnwrap(inputBuffer), context: self.newContext()).wait()) + XCTAssertNoThrow(outputBuffer = try handler.handle(buffer: XCTUnwrap(inputBuffer), context: self.newContext()).wait()) XCTAssertNoThrow(response = try JSONDecoder().decode(Response.self, from: XCTUnwrap(outputBuffer))) XCTAssertEqual(response?.requestId, request.requestId) } diff --git a/Tests/AWSLambdaTestingTests/Tests.swift b/Tests/AWSLambdaTestingTests/Tests.swift index f4520aa2..7b3c6380 100644 --- a/Tests/AWSLambdaTestingTests/Tests.swift +++ b/Tests/AWSLambdaTestingTests/Tests.swift @@ -30,13 +30,8 @@ class LambdaTestingTests: XCTestCase { } struct MyLambda: LambdaHandler { - typealias Event = Request - typealias Output = Response - - init(context: LambdaInitializationContext) {} - - func handle(_ event: Request, context: LambdaContext) async throws -> Response { - Response(message: "echo" + event.name) + func handle(request: Request, context: LambdaContext) async throws -> Response { + Response(message: "echo" + request.name) } } @@ -54,12 +49,7 @@ class LambdaTestingTests: XCTestCase { } struct MyLambda: LambdaHandler { - typealias Event = Request - typealias Output = Void - - init(context: LambdaInitializationContext) {} - - func handle(_ event: Request, context: LambdaContext) async throws { + func handle(request: Request, context: LambdaContext) async throws { LambdaTestingTests.VoidLambdaHandlerInvokeCount += 1 } } @@ -74,12 +64,7 @@ class LambdaTestingTests: XCTestCase { struct MyError: Error {} struct MyLambda: LambdaHandler { - typealias Event = String - typealias Output = Void - - init(context: LambdaInitializationContext) {} - - func handle(_ event: String, context: LambdaContext) async throws { + func handle(request: String, context: LambdaContext) async throws { throw MyError() } } @@ -91,14 +76,9 @@ class LambdaTestingTests: XCTestCase { func testAsyncLongRunning() { struct MyLambda: LambdaHandler { - typealias Event = String - typealias Output = String - - init(context: LambdaInitializationContext) {} - - func handle(_ event: String, context: LambdaContext) async throws -> String { + func handle(request: String, context: LambdaContext) async throws -> String { try await Task.sleep(nanoseconds: 500 * 1000 * 1000) - return event + return request } } diff --git a/readme.md b/readme.md index 8ffcff85..89b8f1ce 100644 --- a/readme.md +++ b/readme.md @@ -14,134 +14,142 @@ Swift AWS Lambda Runtime was designed to make building Lambda functions in Swift If you have never used AWS Lambda or Docker before, check out this [getting started guide](https://fabianfett.de/getting-started-with-swift-aws-lambda-runtime) which helps you with every step from zero to a running Lambda. -First, create a SwiftPM project and pull Swift AWS Lambda Runtime as dependency into your project - - ```swift - // swift-tools-version:5.6 - - import PackageDescription - - let package = Package( - name: "my-lambda", - products: [ - .executable(name: "MyLambda", targets: ["MyLambda"]), - ], - dependencies: [ - .package(url: "https://github.com/swift-server/swift-aws-lambda-runtime.git", from: "0.1.0"), - ], - targets: [ - .executableTarget(name: "MyLambda", dependencies: [ - .product(name: "AWSLambdaRuntime", package: "swift-aws-lambda-runtime"), - ]), - ] - ) - ``` - -Next, create a `main.swift` and implement your Lambda. - - ### Using Closures - - The simplest way to use `AWSLambdaRuntime` is to pass in a closure, for example: - - ```swift - // Import the module - import AWSLambdaRuntime - - // in this example we are receiving and responding with strings - Lambda.run { (context, name: String, callback: @escaping (Result) -> Void) in - callback(.success("Hello, \(name)")) - } - ``` - - More commonly, the event would be a JSON, which is modeled using `Codable`, for example: - - ```swift - // Import the module - import AWSLambdaRuntime - - // Request, uses Codable for transparent JSON encoding - private struct Request: Codable { - let name: String - } - - // Response, uses Codable for transparent JSON encoding - private struct Response: Codable { - let message: String - } - - // In this example we are receiving and responding with `Codable`. - Lambda.run { (context, request: Request, callback: @escaping (Result) -> Void) in - callback(.success(Response(message: "Hello, \(request.name)"))) - } - ``` - - Since most Lambda functions are triggered by events originating in the AWS platform like `SNS`, `SQS` or `APIGateway`, the [Swift AWS Lambda Events](http://github.com/swift-server/swift-aws-lambda-events) package includes an `AWSLambdaEvents` module that provides implementations for most common AWS event types further simplifying writing Lambda functions. For example, handling an `SQS` message: - -First, add a dependency on the event packages: - - ```swift - // swift-tools-version:5.6 - - import PackageDescription - - let package = Package( - name: "my-lambda", - products: [ - .executable(name: "MyLambda", targets: ["MyLambda"]), - ], - dependencies: [ - .package(url: "https://github.com/swift-server/swift-aws-lambda-runtime.git", from: "0.1.0"), - ], - targets: [ - .executableTarget(name: "MyLambda", dependencies: [ - .product(name: "AWSLambdaRuntime", package: "swift-aws-lambda-runtime"), - .product(name: "AWSLambdaEvents", package: "swift-aws-lambda-runtime"), - ]), - ] - ) - ``` - - - ```swift - // Import the modules - import AWSLambdaRuntime - import AWSLambdaEvents - - // In this example we are receiving an SQS Event, with no response (Void). - Lambda.run { (context, message: SQS.Event, callback: @escaping (Result) -> Void) in - ... - callback(.success(Void())) - } - ``` - - Modeling Lambda functions as Closures is both simple and safe. Swift AWS Lambda Runtime will ensure that the user-provided code is offloaded from the network processing thread such that even if the code becomes slow to respond or gets hang, the underlying process can continue to function. This safety comes at a small performance penalty from context switching between threads. In many cases, the simplicity and safety of using the Closure based API is often preferred over the complexity of the performance-oriented API. +First, create a SwiftPM project with an executable target and pull Swift AWS Lambda Runtime as dependency into your project: -### Using EventLoopLambdaHandler +```swift +// swift-tools-version:5.6 +import PackageDescription +let package = Package( + name: "my-lambda", + products: [ + .executable(name: "MyLambda", targets: ["MyLambda"]), + ], + dependencies: [ + .package(url: "https://github.com/swift-server/swift-aws-lambda-runtime.git", from: "0.1.0"), + ], + targets: [ + .executableTarget(name: "MyLambda", dependencies: [ + .product(name: "AWSLambdaRuntime", package: "swift-aws-lambda-runtime"), + ]), + ] +) +``` + +Now you can start implementing your Lambda! + +### Creating an Entry Point for your Lambda + +The simplest way to use `AWSLambdaRuntime` is to create a type conforming to the `LambdaHandler` +protocol: + +```swift +// Import the module +import AWSLambdaRuntime + +@main +struct Handler: LambdaHandler { + // in this example we are receiving and responding with strings + func handle(request: String, context: LambdaContext) -> String { + "Hello, \(request)!" + } +} +``` + +> Using the `@main` attribute provides a signal to `AWSLambdaRuntime` that this is your application's entry point. The framework will handle everything else from there. This means that you don't have to create a main.swift file. + +### Using JSON as your Lambda's Input/Output + +More commonly, events coming into your Lambda will most likely be JSON objects, which is modeled using `Codable`, for example: + +```swift +// Import the module +import AWSLambdaRuntime +import Foundation + +// Request, uses `Decodable` for transparent JSON encoding +private struct Request: Decodable { + let name: String +} + +// Response, uses `Encodable` for transparent JSON encoding +private struct Response: Encodable { + let message: String +} + +@main +struct Handler: LambdaHandler { + // In this example we are receiving a `Decodable` and responding with an `Encodable`. + func handle(request: Request, context: LambdaContext) -> Response { + Response(message: Hello, \(request.name)!") + } +} +``` + +### Using Popular AWS Events + +Since most Lambda functions are triggered by events originating in the AWS platform like `SNS`, `SQS` or `APIGateway`, the [Swift AWS Lambda Events](http://github.com/swift-server/swift-aws-lambda-events) package includes an `AWSLambdaEvents` module that provides implementations for most common AWS event types further simplifying writing Lambda functions. For example, handling an `SQS` message: + +First, add a dependency on the event package: - Performance sensitive Lambda functions may choose to use a more complex API which allows user code to run on the same thread as the networking handlers. Swift AWS Lambda Runtime uses [SwiftNIO](https://github.com/apple/swift-nio) as its underlying networking engine which means the APIs are based on [SwiftNIO](https://github.com/apple/swift-nio) concurrency primitives like the `EventLoop` and `EventLoopFuture`. For example: +```swift +// swift-tools-version:5.6 + +import PackageDescription + +let package = Package( + name: "my-lambda", + products: [ + .executable(name: "MyLambda", targets: ["MyLambda"]), + ], + dependencies: [ + .package(url: "https://github.com/swift-server/swift-aws-lambda-runtime.git", from: "0.1.0"), + ], + targets: [ + .executableTarget(name: "MyLambda", dependencies: [ + .product(name: "AWSLambdaRuntime", package: "swift-aws-lambda-runtime"), + .product(name: "AWSLambdaEvents", package: "swift-aws-lambda-runtime"), + ]), + ] +) +``` - ```swift - // Import the modules - import AWSLambdaRuntime - import AWSLambdaEvents - import NIO - // Our Lambda handler, conforms to EventLoopLambdaHandler - struct Handler: EventLoopLambdaHandler { - typealias In = SNS.Message // Request type - typealias Out = Void // Response type +```swift +// Import the modules +import AWSLambdaRuntime +import AWSLambdaEvents + +@main +struct Handler: LambdaHandler { + // In this example we are receiving a Decodable, with no response (Void). + func handle(request: SQS.Event, context: LambdaContext) { + ... + } +} +``` - // In this example we are receiving an SNS Message, with no response (Void). - func handle(context: Lambda.Context, event: In) -> EventLoopFuture { - ... - context.eventLoop.makeSucceededFuture(Void()) - } - } +### Using EventLoopLambdaHandler - Lambda.run(Handler()) - ``` +Performance sensitive Lambda functions may choose to use a more complex API which allows user code to run on the same thread as the networking handlers. Swift AWS Lambda Runtime uses [SwiftNIO](https://github.com/apple/swift-nio) as its underlying networking engine which means the APIs are based on [SwiftNIO](https://github.com/apple/swift-nio) concurrency primitives like the `EventLoop` and `EventLoopFuture`. For example: - Beyond the small cognitive complexity of using the `EventLoopFuture` based APIs, note these APIs should be used with extra care. An `EventLoopLambdaHandler` will execute the user code on the same `EventLoop` (thread) as the library, making processing faster but requiring the user code to never call blocking APIs as it might prevent the underlying process from functioning. +```swift +// Import the modules +import AWSLambdaRuntime +import AWSLambdaEvents +import NIO + +// Our Lambda handler which conforms to `EventLoopLambdaHandler` +@main +struct Handler: EventLoopLambdaHandler { + // In this example we are receiving an SNS Message, with no response (Void). + func handle(event: SNS.Message, context: LambdaContext) -> EventLoopFuture { + ... + context.eventLoop.makeSucceededFuture(Void()) + } +} +``` + +Beyond the small cognitive complexity of using the `EventLoopFuture` based APIs, note these APIs should be used with extra care. An `EventLoopLambdaHandler` will execute the user code on the same `EventLoop` (thread) as the library, making processing faster but requiring the user code to never call blocking APIs as it might prevent the underlying process from functioning. ## Deploying to AWS Lambda @@ -169,100 +177,79 @@ public protocol ByteBufferLambdaHandler { /// Concrete Lambda handlers implement this method to provide the Lambda functionality. /// /// - parameters: + /// - buffer: The event or request payload encoded as a `ByteBuffer`. /// - context: Runtime `Context`. - /// - event: The event or request payload encoded as `ByteBuffer`. /// /// - Returns: An `EventLoopFuture` to report the result of the Lambda back to the runtime engine. /// The `EventLoopFuture` should be completed with either a response encoded as `ByteBuffer` or an `Error` - func handle(context: Lambda.Context, event: ByteBuffer) -> EventLoopFuture + func handle(buffer: ByteBuffer, context: LambdaContext) -> EventLoopFuture } ``` ### EventLoopLambdaHandler -`EventLoopLambdaHandler` is a strongly typed, `EventLoopFuture` based asynchronous processing protocol for a Lambda that takes a user defined `In` and returns a user defined `Out`. +`EventLoopLambdaHandler` is a strongly typed, `EventLoopFuture` based asynchronous processing protocol for a Lambda that takes a user defined `Event` type and returns a user defined `Output` type. -`EventLoopLambdaHandler` extends `ByteBufferLambdaHandler`, providing `ByteBuffer` -> `In` decoding and `Out` -> `ByteBuffer?` encoding for `Codable` and `String`. +`EventLoopLambdaHandler` extends `ByteBufferLambdaHandler`, providing `ByteBuffer` -> `Event` decoding and `Output` -> `ByteBuffer?` encoding for `Codable` and `String`. -`EventLoopLambdaHandler` executes the user provided Lambda on the same `EventLoop` as the core runtime engine, making the processing fast but requires more care from the implementation to never block the `EventLoop`. It it designed for performance sensitive applications that use `Codable` or `String` based Lambda functions. +`EventLoopLambdaHandler` executes the user provided Lambda on the same `EventLoop` as the core runtime engine, making the processing fast but requires more care from the implementation to never block the `EventLoop`. It is designed for performance sensitive applications that use `Codable` or `String` based Lambda functions. ```swift public protocol EventLoopLambdaHandler: ByteBufferLambdaHandler { - associatedtype In - associatedtype Out + associatedtype Event + associatedtype Output /// The Lambda handling method /// Concrete Lambda handlers implement this method to provide the Lambda functionality. /// /// - parameters: + /// - event: Event of type `Event` representing the event or request. /// - context: Runtime `Context`. - /// - event: Event of type `In` representing the event or request. /// /// - Returns: An `EventLoopFuture` to report the result of the Lambda back to the runtime engine. - /// The `EventLoopFuture` should be completed with either a response of type `Out` or an `Error` - func handle(context: Lambda.Context, event: In) -> EventLoopFuture + /// The `EventLoopFuture` should be completed with either a response of type `Output` or an `Error` + func handle(event: Event, context: LambdaContext) -> EventLoopFuture - /// Encode a response of type `Out` to `ByteBuffer` + /// Encode a response of type `Output` to `ByteBuffer` /// Concrete Lambda handlers implement this method to provide coding functionality. /// - parameters: /// - allocator: A `ByteBufferAllocator` to help allocate the `ByteBuffer`. - /// - value: Response of type `Out`. + /// - value: Response of type `Output`. /// /// - Returns: A `ByteBuffer` with the encoded version of the `value`. - func encode(allocator: ByteBufferAllocator, value: Out) throws -> ByteBuffer? + func encode(allocator: ByteBufferAllocator, value: Output) throws -> ByteBuffer? - /// Decode a`ByteBuffer` to a request or event of type `In` + /// Decode a `ByteBuffer` to a request or event of type `Event` /// Concrete Lambda handlers implement this method to provide coding functionality. /// /// - parameters: /// - buffer: The `ByteBuffer` to decode. /// - /// - Returns: A request or event of type `In`. - func decode(buffer: ByteBuffer) throws -> In + /// - Returns: A request or event of type `Event`. + func decode(buffer: ByteBuffer) throws -> Event } ``` ### LambdaHandler -`LambdaHandler` is a strongly typed, completion handler based asynchronous processing protocol for a Lambda that takes a user defined `In` and returns a user defined `Out`. +`LambdaHandler` is a strongly-typed processing protocol that uses Swift concurrency primitives for a Lambda that takes a user defined `Request` and returns a user defined `Response`. -`LambdaHandler` extends `ByteBufferLambdaHandler`, performing `ByteBuffer` -> `In` decoding and `Out` -> `ByteBuffer` encoding for `Codable` and `String`. - -`LambdaHandler` offloads the user provided Lambda execution to a `DispatchQueue` making processing safer but slower. +`LambdaHandler` extends `EventLoopLambdaHandler`, allowing the user to write Swift async logic in the body of their `handle(request:context:)` function. ```swift public protocol LambdaHandler: EventLoopLambdaHandler { - /// Defines to which `DispatchQueue` the Lambda execution is offloaded to. - var offloadQueue: DispatchQueue { get } - - /// The Lambda handling method + /// The Lambda handling method. /// Concrete Lambda handlers implement this method to provide the Lambda functionality. /// /// - parameters: - /// - context: Runtime `Context`. - /// - event: Event of type `In` representing the event or request. - /// - callback: Completion handler to report the result of the Lambda back to the runtime engine. - /// The completion handler expects a `Result` with either a response of type `Out` or an `Error` - func handle(context: Lambda.Context, event: In, callback: @escaping (Result) -> Void) + /// - request: Event of type `Request` representing the event or request. + /// - context: Runtime ``LambdaContext``. + /// + /// - Returns: A Lambda result ot type `Output`. + func handle(request: Request, context: LambdaContext) async throws -> Response } ``` -### Closures - -In addition to protocol-based Lambda, the library provides support for Closure-based ones, as demonstrated in the overview section above. Closure-based Lambdas are based on the `LambdaHandler` protocol which mean they are safer. For most use cases, Closure-based Lambda is a great fit and users are encouraged to use them. - -The library includes implementations for `Codable` and `String` based Lambda. Since AWS Lambda is primarily JSON based, this covers the most common use cases. - -```swift -public typealias CodableClosure = (Lambda.Context, In, @escaping (Result) -> Void) -> Void -``` - -```swift -public typealias StringClosure = (Lambda.Context, String, @escaping (Result) -> Void) -> Void -``` - -This design allows for additional event types as well, and such Lambda implementation can extend one of the above protocols and provided their own `ByteBuffer` -> `In` decoding and `Out` -> `ByteBuffer` encoding. - ### Context When calling the user provided Lambda function, the library provides a `Context` class that provides metadata about the execution context, as well as utilities for logging and allocating buffers. @@ -307,7 +294,7 @@ public final class Context { ### Configuration -The library’s behavior can be fine tuned using environment variables based configuration. The library supported the following environment variables: +This library’s behavior can be fine tuned using environment variables based configuration. This library supports the following environment variables: * `LOG_LEVEL`: Define the logging level as defined by [SwiftLog](https://github.com/apple/swift-log). Set to INFO by default. * `MAX_REQUESTS`: Max cycles the library should handle before exiting. Set to none by default. @@ -327,11 +314,11 @@ A single Lambda execution workflow is made of the following steps: 1. The library calls AWS Lambda Runtime Engine `/next` endpoint to retrieve the next invocation request. 2. The library parses the response HTTP headers and populate the `Context` object. -3. The library reads the `/next` response body and attempt to decode it. Typically it decodes to user provided `In` type which extends `Decodable`, but users may choose to write Lambda functions that receive the input as `String` or `ByteBuffer` which require less, or no decoding. -4. The library hands off the `Context` and `In` event to the user provided handler. In the case of `LambdaHandler` based handler this is done on a dedicated `DispatchQueue`, providing isolation between user's and the library's code. -5. User provided handler processes the request asynchronously, invoking a callback or returning a future upon completion, which returns a `Result` type with the `Out` or `Error` populated. +3. The library reads the `/next` response body and attempt to decode it. Typically it decodes to user provided `Event` type which extends `Decodable`, but users may choose to write Lambda functions that receive the input as `String` or `ByteBuffer` which require less, or no decoding. +4. The library hands off the `Context` and `Event` event to the user provided handler. +5. The user-provided handler processes the request asynchronously, returning a future or the result itself upon completion, which returns a `Result` type with the `Output` or `Error` populated. 6. In case of error, the library posts to AWS Lambda Runtime Engine `/error` endpoint to provide the error details, which will show up on AWS Lambda logs. -7. In case of success, the library will attempt to encode the response. Typically it encodes from user provided `Out` type which extends `Encodable`, but users may choose to write Lambda functions that return a `String` or `ByteBuffer`, which require less, or no encoding. The library then posts the response to AWS Lambda Runtime Engine `/response` endpoint to provide the response to the callee. +7. In case of success, the library will attempt to encode the response. Typically it encodes from user provided `Output` type which extends `Encodable`, but users may choose to write Lambda functions that return a `String` or `ByteBuffer`, which require less, or no encoding. The library then posts the response to AWS Lambda Runtime Engine `/response` endpoint to provide the response to the callee. The library encapsulates the workflow via the internal `LambdaRuntimeClient` and `LambdaRunner` structs respectively.