Skip to content

RFC: Added ServiceLifecycle dependency and started integration #141

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Closed
wants to merge 2 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions Package.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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: [
Expand All @@ -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"),
Expand Down
19 changes: 15 additions & 4 deletions Sources/AWSLambdaRuntimeCore/Lambda.swift
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ import Darwin.C

import Backtrace
import Logging
import Lifecycle
import NIO

public enum Lambda {
Expand Down Expand Up @@ -101,13 +102,15 @@ public enum Lambda {
// 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
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<Int, Error>!
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)")
Expand All @@ -121,11 +124,19 @@ public enum Lambda {
#if DEBUG
signalSource.cancel()
#endif
eventLoop.shutdownGracefully { error in

serviceLifecycle.shutdown { (error) in
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

why (error)?

if let error = error {
preconditionFailure("Failed to shutdown eventloop: \(error)")
preconditionFailure("Failed to shutdown service: \(error)")
}

eventLoop.shutdownGracefully { error in
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

shouldn't that also be triggered by the lifecycle shutdown as a lifecycle task?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think thats a good idea: register the event loop in L110 then just call serviceLifecycle.shutdown

if let error = error {
preconditionFailure("Failed to shutdown eventloop: \(error)")
}
}
}

result = lifecycleResult
}
}
Expand Down
7 changes: 6 additions & 1 deletion Sources/AWSLambdaRuntimeCore/LambdaContext.swift
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
//===----------------------------------------------------------------------===//

import Dispatch
import Lifecycle
import Logging
import NIO

Expand All @@ -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
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

maybe simplify name to "lifecycle"?

Copy link
Member Author

@fabianfett fabianfett Sep 1, 2020

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not sure if just lifecycle might add confusion because of the Lambda.Lifecycle. Your call.


/// `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
}
}
Expand Down
11 changes: 7 additions & 4 deletions Sources/AWSLambdaRuntimeCore/LambdaLifecycle.swift
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
//===----------------------------------------------------------------------===//

import Logging
import Lifecycle
import NIO
import NIOConcurrencyHelpers

Expand All @@ -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
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ditto on name

private let shutdownPromise: EventLoopPromise<Int>
private let logger: Logger
private let configuration: Configuration
Expand All @@ -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
Expand Down Expand Up @@ -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<Int, Error>)> in
// after the startup future has succeeded, we have a handler that we can use
// to `run` the lambda.
Expand Down
18 changes: 17 additions & 1 deletion Sources/AWSLambdaRuntimeCore/LambdaRunner.swift
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@

import Dispatch
import Logging
import Lifecycle
import NIO

extension Lambda {
Expand All @@ -34,19 +35,34 @@ 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(serviceLifecycle: ServiceLifecycle, logger: Logger, factory: @escaping HandlerFactory) -> 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,
serviceLifecycle: serviceLifecycle,
allocator: self.allocator)
return factory(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
// 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
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I wonder if we should add sugar in the lifecycle NIO compact module to make this kind of integration easier. wdyt?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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
Expand Down
26 changes: 22 additions & 4 deletions Tests/AWSLambdaRuntimeCoreTests/LambdaLifecycleTest.swift
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@

@testable import AWSLambdaRuntimeCore
import Logging
import Lifecycle
import NIO
import NIOHTTP1
import XCTest
Expand All @@ -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)
})

Expand Down Expand Up @@ -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()
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I wonder if we can now simplify the shutdown logic a bit by relying more on the users using lifecycle


var count = 0
let handler = CallbackLambdaHandler({ XCTFail("Should not be reached"); return $0.eventLoop.makeSucceededFuture($1) }) { context in
Expand All @@ -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)
})

Expand All @@ -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
Expand All @@ -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)
})

Expand Down
10 changes: 9 additions & 1 deletion Tests/AWSLambdaRuntimeCoreTests/Utils.swift
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@

@testable import AWSLambdaRuntimeCore
import Logging
import Lifecycle
import NIO
import XCTest

Expand All @@ -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()
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I guess we can register the server with lifecycle but maybe will make the test harder to reason about


try runner.initialize(serviceLifecycle: serviceLifecycle, logger: logger, factory: factory).flatMap { handler in
runner.run(logger: logger, handler: handler)
}.wait()
}
Expand Down