diff --git a/Package.swift b/Package.swift index 0e5823f6..42cf2c02 100644 --- a/Package.swift +++ b/Package.swift @@ -18,6 +18,8 @@ let package = Package( .package(url: "https://github.com/apple/swift-nio.git", .upToNextMajor(from: "2.17.0")), .package(url: "https://github.com/apple/swift-log.git", .upToNextMajor(from: "1.0.0")), .package(url: "https://github.com/swift-server/swift-backtrace.git", .upToNextMajor(from: "1.1.0")), +// .package(url: "https://github.com/swift-server/swift-service-lifecycle.git", .upToNextMajor(from: "1.0.0-alpha")), + .package(url: "https://github.com/swift-server/swift-service-lifecycle.git", .branch("main")), ], targets: [ .target(name: "AWSLambdaRuntime", dependencies: [ @@ -29,6 +31,7 @@ let package = Package( .product(name: "Logging", package: "swift-log"), .product(name: "Backtrace", package: "swift-backtrace"), .product(name: "NIOHTTP1", package: "swift-nio"), + .product(name: "Lifecycle", package: "swift-service-lifecycle"), ]), .testTarget(name: "AWSLambdaRuntimeCoreTests", dependencies: [ .byName(name: "AWSLambdaRuntimeCore"), diff --git a/Sources/AWSLambdaRuntimeCore/Lambda.swift b/Sources/AWSLambdaRuntimeCore/Lambda.swift index 5dc27648..2473a316 100644 --- a/Sources/AWSLambdaRuntimeCore/Lambda.swift +++ b/Sources/AWSLambdaRuntimeCore/Lambda.swift @@ -20,6 +20,7 @@ import Darwin.C import Backtrace import Logging +import Lifecycle import NIO public enum Lambda { @@ -101,13 +102,15 @@ public enum Lambda { // for testing and internal use internal static func run(configuration: Configuration = .init(), factory: @escaping HandlerFactory) -> Result { let _run = { (configuration: Configuration, factory: @escaping HandlerFactory) -> Result in - Backtrace.install() var logger = Logger(label: "Lambda") logger.logLevel = configuration.general.logLevel + + // we don't intercept the shutdown signal here yet. + let serviceLifecycle = ServiceLifecycle(configuration: .init(logger: logger, shutdownSignal: [], installBacktrace: true)) var result: Result! MultiThreadedEventLoopGroup.withCurrentThreadAsEventLoop { eventLoop in - let lifecycle = Lifecycle(eventLoop: eventLoop, logger: logger, configuration: configuration, factory: factory) + let lifecycle = Lifecycle(eventLoop: eventLoop, serviceLifecycle: serviceLifecycle, logger: logger, configuration: configuration, factory: factory) #if DEBUG let signalSource = trap(signal: configuration.lifecycle.stopSignal) { signal in logger.info("intercepted signal: \(signal)") @@ -121,11 +124,19 @@ public enum Lambda { #if DEBUG signalSource.cancel() #endif - eventLoop.shutdownGracefully { error in + + serviceLifecycle.shutdown { (error) in if let error = error { - preconditionFailure("Failed to shutdown eventloop: \(error)") + preconditionFailure("Failed to shutdown service: \(error)") + } + + eventLoop.shutdownGracefully { error in + if let error = error { + preconditionFailure("Failed to shutdown eventloop: \(error)") + } } } + result = lifecycleResult } } diff --git a/Sources/AWSLambdaRuntimeCore/LambdaContext.swift b/Sources/AWSLambdaRuntimeCore/LambdaContext.swift index ab30dd7b..1d429485 100644 --- a/Sources/AWSLambdaRuntimeCore/LambdaContext.swift +++ b/Sources/AWSLambdaRuntimeCore/LambdaContext.swift @@ -13,6 +13,7 @@ //===----------------------------------------------------------------------===// import Dispatch +import Lifecycle import Logging import NIO @@ -32,13 +33,17 @@ extension Lambda { /// - note: The `EventLoop` is shared with the Lambda runtime engine and should be handled with extra care. /// Most importantly the `EventLoop` must never be blocked. public let eventLoop: EventLoop + + /// `ServiceLifecycle` to register services with + public let serviceLifecycle: ServiceLifecycle /// `ByteBufferAllocator` to allocate `ByteBuffer` public let allocator: ByteBufferAllocator - internal init(logger: Logger, eventLoop: EventLoop, allocator: ByteBufferAllocator) { + internal init(logger: Logger, eventLoop: EventLoop, serviceLifecycle: ServiceLifecycle, allocator: ByteBufferAllocator) { self.eventLoop = eventLoop self.logger = logger + self.serviceLifecycle = serviceLifecycle self.allocator = allocator } } diff --git a/Sources/AWSLambdaRuntimeCore/LambdaLifecycle.swift b/Sources/AWSLambdaRuntimeCore/LambdaLifecycle.swift index ec609901..324dc66b 100644 --- a/Sources/AWSLambdaRuntimeCore/LambdaLifecycle.swift +++ b/Sources/AWSLambdaRuntimeCore/LambdaLifecycle.swift @@ -13,6 +13,7 @@ //===----------------------------------------------------------------------===// import Logging +import Lifecycle import NIO import NIOConcurrencyHelpers @@ -22,6 +23,7 @@ extension Lambda { /// - note: It is intended to be used within a single `EventLoop`. For this reason this class is not thread safe. public final class Lifecycle { private let eventLoop: EventLoop + private let serviceLifecycle: ServiceLifecycle private let shutdownPromise: EventLoopPromise private let logger: Logger private let configuration: Configuration @@ -40,12 +42,13 @@ extension Lambda { /// - 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 HandlerFactory) { - self.init(eventLoop: eventLoop, logger: logger, configuration: .init(), factory: factory) + public convenience init(eventLoop: EventLoop, serviceLifecycle: ServiceLifecycle, logger: Logger, factory: @escaping HandlerFactory) { + self.init(eventLoop: eventLoop, serviceLifecycle: serviceLifecycle, logger: logger, configuration: .init(), factory: factory) } - init(eventLoop: EventLoop, logger: Logger, configuration: Configuration, factory: @escaping HandlerFactory) { + init(eventLoop: EventLoop, serviceLifecycle: ServiceLifecycle, logger: Logger, configuration: Configuration, factory: @escaping HandlerFactory) { self.eventLoop = eventLoop + self.serviceLifecycle = serviceLifecycle self.shutdownPromise = eventLoop.makePromise(of: Int.self) self.logger = logger self.configuration = configuration @@ -80,7 +83,7 @@ extension Lambda { logger[metadataKey: "lifecycleId"] = .string(self.configuration.lifecycle.id) let runner = Runner(eventLoop: self.eventLoop, configuration: self.configuration) - let startupFuture = runner.initialize(logger: logger, factory: self.factory) + let startupFuture = runner.initialize(serviceLifecycle: self.serviceLifecycle, logger: logger, factory: self.factory) startupFuture.flatMap { handler -> EventLoopFuture<(ByteBufferLambdaHandler, Result)> in // after the startup future has succeeded, we have a handler that we can use // to `run` the lambda. diff --git a/Sources/AWSLambdaRuntimeCore/LambdaRunner.swift b/Sources/AWSLambdaRuntimeCore/LambdaRunner.swift index 96bae6ed..f002df24 100644 --- a/Sources/AWSLambdaRuntimeCore/LambdaRunner.swift +++ b/Sources/AWSLambdaRuntimeCore/LambdaRunner.swift @@ -14,6 +14,7 @@ import Dispatch import Logging +import Lifecycle import NIO extension Lambda { @@ -34,12 +35,13 @@ extension Lambda { /// Run the user provided initializer. This *must* only be called once. /// /// - Returns: An `EventLoopFuture` fulfilled with the outcome of the initialization. - func initialize(logger: Logger, factory: @escaping HandlerFactory) -> EventLoopFuture { + func initialize(serviceLifecycle: ServiceLifecycle, logger: Logger, factory: @escaping HandlerFactory) -> EventLoopFuture { 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, + serviceLifecycle: serviceLifecycle, allocator: self.allocator) return factory(context) // Hopping back to "our" EventLoop is important in case the factory returns a future @@ -47,6 +49,20 @@ extension Lambda { // This can happen if the factory uses a library (let's say a database client) that manages its own threads/loops // for whatever reason and returns a future that originated from that foreign EventLoop. .hop(to: self.eventLoop) + .flatMap { (handler) in + let promise = self.eventLoop.makePromise(of: ByteBufferLambdaHandler.self) + // after we have created the LambdaHandler we must now start the services. + // in order to not have to map once our success case returns the handler. + serviceLifecycle.start { (error) in + if let error = error { + promise.fail(error) + } + else { + promise.succeed(handler) + } + } + return promise.futureResult + } .peekError { error in self.runtimeClient.reportInitializationError(logger: logger, error: error).peekError { reportingError in // We're going to bail out because the init failed, so there's not a lot we can do other than log diff --git a/Tests/AWSLambdaRuntimeCoreTests/LambdaLifecycleTest.swift b/Tests/AWSLambdaRuntimeCoreTests/LambdaLifecycleTest.swift index a485530d..4dddc3f8 100644 --- a/Tests/AWSLambdaRuntimeCoreTests/LambdaLifecycleTest.swift +++ b/Tests/AWSLambdaRuntimeCoreTests/LambdaLifecycleTest.swift @@ -14,6 +14,7 @@ @testable import AWSLambdaRuntimeCore import Logging +import Lifecycle import NIO import NIOHTTP1 import XCTest @@ -25,11 +26,16 @@ class LambdaLifecycleTest: XCTestCase { defer { XCTAssertNoThrow(try server.stop().wait()) } let eventLoopGroup = MultiThreadedEventLoopGroup(numberOfThreads: 1) defer { XCTAssertNoThrow(try eventLoopGroup.syncShutdownGracefully()) } - + + let serviceLifecycle = ServiceLifecycle(configuration: .init(shutdownSignal: [], installBacktrace: false)) + defer { + serviceLifecycle.shutdown() + serviceLifecycle.wait() + } let eventLoop = eventLoopGroup.next() let logger = Logger(label: "TestLogger") let testError = TestError("kaboom") - let lifecycle = Lambda.Lifecycle(eventLoop: eventLoop, logger: logger, factory: { + let lifecycle = Lambda.Lifecycle(eventLoop: eventLoop, serviceLifecycle: serviceLifecycle, logger: logger, factory: { $0.eventLoop.makeFailedFuture(testError) }) @@ -68,6 +74,12 @@ class LambdaLifecycleTest: XCTestCase { defer { XCTAssertNoThrow(try server.stop().wait()) } let eventLoopGroup = MultiThreadedEventLoopGroup(numberOfThreads: 1) defer { XCTAssertNoThrow(try eventLoopGroup.syncShutdownGracefully()) } + + let serviceLifecycle = ServiceLifecycle(configuration: .init(shutdownSignal: [], installBacktrace: false)) + defer { + serviceLifecycle.shutdown() + serviceLifecycle.wait() + } var count = 0 let handler = CallbackLambdaHandler({ XCTFail("Should not be reached"); return $0.eventLoop.makeSucceededFuture($1) }) { context in @@ -77,7 +89,7 @@ class LambdaLifecycleTest: XCTestCase { let eventLoop = eventLoopGroup.next() let logger = Logger(label: "TestLogger") - let lifecycle = Lambda.Lifecycle(eventLoop: eventLoop, logger: logger, factory: { + let lifecycle = Lambda.Lifecycle(eventLoop: eventLoop, serviceLifecycle: serviceLifecycle, logger: logger, factory: { $0.eventLoop.makeSucceededFuture(handler) }) @@ -94,6 +106,12 @@ class LambdaLifecycleTest: XCTestCase { defer { XCTAssertNoThrow(try server.stop().wait()) } let eventLoopGroup = MultiThreadedEventLoopGroup(numberOfThreads: 1) defer { XCTAssertNoThrow(try eventLoopGroup.syncShutdownGracefully()) } + + let serviceLifecycle = ServiceLifecycle(configuration: .init(shutdownSignal: [], installBacktrace: false)) + defer { + serviceLifecycle.shutdown() + serviceLifecycle.wait() + } var count = 0 let handler = CallbackLambdaHandler({ XCTFail("Should not be reached"); return $0.eventLoop.makeSucceededFuture($1) }) { context in @@ -103,7 +121,7 @@ class LambdaLifecycleTest: XCTestCase { let eventLoop = eventLoopGroup.next() let logger = Logger(label: "TestLogger") - let lifecycle = Lambda.Lifecycle(eventLoop: eventLoop, logger: logger, factory: { + let lifecycle = Lambda.Lifecycle(eventLoop: eventLoop, serviceLifecycle: serviceLifecycle, logger: logger, factory: { $0.eventLoop.makeSucceededFuture(handler) }) diff --git a/Tests/AWSLambdaRuntimeCoreTests/Utils.swift b/Tests/AWSLambdaRuntimeCoreTests/Utils.swift index e7160307..fffc88e8 100644 --- a/Tests/AWSLambdaRuntimeCoreTests/Utils.swift +++ b/Tests/AWSLambdaRuntimeCoreTests/Utils.swift @@ -14,6 +14,7 @@ @testable import AWSLambdaRuntimeCore import Logging +import Lifecycle import NIO import XCTest @@ -29,7 +30,14 @@ 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 + + let serviceLifecycle = ServiceLifecycle(configuration: .init(shutdownSignal: [], installBacktrace: false)) + defer { + serviceLifecycle.shutdown() + serviceLifecycle.wait() + } + + try runner.initialize(serviceLifecycle: serviceLifecycle, logger: logger, factory: factory).flatMap { handler in runner.run(logger: logger, handler: handler) }.wait() }