Skip to content

Commit 4d0bba4

Browse files
authored
termination handler (#251)
motivation: make it simpler to register shutdown hooks changes: * introduce Terminator helper that allow registering and de-registaring shutdown handlers * expose the new terminator hanler on the InitializationContext and deprecate ShutdownContext * deprecate the Handler::shutdown protocol requirment * update the runtime code to use the new terminator instead of calling shutdown on the handler * add and adjust tests
1 parent f2a0ef5 commit 4d0bba4

File tree

9 files changed

+201
-63
lines changed

9 files changed

+201
-63
lines changed

Sources/AWSLambdaRuntimeCore/LambdaContext.swift

Lines changed: 7 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -38,10 +38,14 @@ extension Lambda {
3838
/// `ByteBufferAllocator` to allocate `ByteBuffer`
3939
public let allocator: ByteBufferAllocator
4040

41-
init(logger: Logger, eventLoop: EventLoop, allocator: ByteBufferAllocator) {
41+
/// `Terminator` to register shutdown operations
42+
public let terminator: LambdaTerminator
43+
44+
init(logger: Logger, eventLoop: EventLoop, allocator: ByteBufferAllocator, terminator: LambdaTerminator) {
4245
self.eventLoop = eventLoop
4346
self.logger = logger
4447
self.allocator = allocator
48+
self.terminator = terminator
4549
}
4650

4751
/// This interface is not part of the public API and must not be used by adopters. This API is not part of semver versioning.
@@ -52,7 +56,8 @@ extension Lambda {
5256
InitializationContext(
5357
logger: logger,
5458
eventLoop: eventLoop,
55-
allocator: ByteBufferAllocator()
59+
allocator: ByteBufferAllocator(),
60+
terminator: LambdaTerminator()
5661
)
5762
}
5863
}
@@ -205,27 +210,3 @@ public struct LambdaContext: CustomDebugStringConvertible {
205210
)
206211
}
207212
}
208-
209-
// MARK: - ShutdownContext
210-
211-
extension Lambda {
212-
/// Lambda runtime shutdown context.
213-
/// The Lambda runtime generates and passes the `ShutdownContext` to the Lambda handler as an argument.
214-
public final class ShutdownContext {
215-
/// `Logger` to log with
216-
///
217-
/// - note: The `LogLevel` can be configured using the `LOG_LEVEL` environment variable.
218-
public let logger: Logger
219-
220-
/// The `EventLoop` the Lambda is executed on. Use this to schedule work with.
221-
///
222-
/// - note: The `EventLoop` is shared with the Lambda runtime engine and should be handled with extra care.
223-
/// Most importantly the `EventLoop` must never be blocked.
224-
public let eventLoop: EventLoop
225-
226-
internal init(logger: Logger, eventLoop: EventLoop) {
227-
self.eventLoop = eventLoop
228-
self.logger = logger
229-
}
230-
}
231-
}

Sources/AWSLambdaRuntimeCore/LambdaHandler.swift

Lines changed: 0 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -176,19 +176,6 @@ public protocol ByteBufferLambdaHandler {
176176
/// - Returns: An `EventLoopFuture` to report the result of the Lambda back to the runtime engine.
177177
/// The `EventLoopFuture` should be completed with either a response encoded as `ByteBuffer` or an `Error`
178178
func handle(_ event: ByteBuffer, context: LambdaContext) -> EventLoopFuture<ByteBuffer?>
179-
180-
/// Clean up the Lambda resources asynchronously.
181-
/// Concrete Lambda handlers implement this method to shutdown resources like `HTTPClient`s and database connections.
182-
///
183-
/// - Note: In case your Lambda fails while creating your LambdaHandler in the `HandlerFactory`, this method
184-
/// **is not invoked**. In this case you must cleanup the created resources immediately in the `HandlerFactory`.
185-
func shutdown(context: Lambda.ShutdownContext) -> EventLoopFuture<Void>
186-
}
187-
188-
extension ByteBufferLambdaHandler {
189-
public func shutdown(context: Lambda.ShutdownContext) -> EventLoopFuture<Void> {
190-
context.eventLoop.makeSucceededFuture(())
191-
}
192179
}
193180

194181
extension ByteBufferLambdaHandler {

Sources/AWSLambdaRuntimeCore/LambdaRunner.swift

Lines changed: 8 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -34,13 +34,16 @@ extension Lambda {
3434
/// Run the user provided initializer. This *must* only be called once.
3535
///
3636
/// - Returns: An `EventLoopFuture<LambdaHandler>` fulfilled with the outcome of the initialization.
37-
func initialize<Handler: ByteBufferLambdaHandler>(logger: Logger, handlerType: Handler.Type) -> EventLoopFuture<Handler> {
37+
func initialize<Handler: ByteBufferLambdaHandler>(logger: Logger, terminator: LambdaTerminator, handlerType: Handler.Type) -> EventLoopFuture<Handler> {
3838
logger.debug("initializing lambda")
3939
// 1. create the handler from the factory
40-
// 2. report initialization error if one occured
41-
let context = InitializationContext(logger: logger,
42-
eventLoop: self.eventLoop,
43-
allocator: self.allocator)
40+
// 2. report initialization error if one occurred
41+
let context = InitializationContext(
42+
logger: logger,
43+
eventLoop: self.eventLoop,
44+
allocator: self.allocator,
45+
terminator: terminator
46+
)
4447
return Handler.makeHandler(context: context)
4548
// Hopping back to "our" EventLoop is important in case the factory returns a future
4649
// that originated from a foreign EventLoop/EventLoopGroup.

Sources/AWSLambdaRuntimeCore/LambdaRuntime.swift

Lines changed: 7 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -74,23 +74,22 @@ public final class LambdaRuntime<Handler: ByteBufferLambdaHandler> {
7474

7575
var logger = self.logger
7676
logger[metadataKey: "lifecycleId"] = .string(self.configuration.lifecycle.id)
77+
let terminator = LambdaTerminator()
7778
let runner = Lambda.Runner(eventLoop: self.eventLoop, configuration: self.configuration)
7879

79-
let startupFuture = runner.initialize(logger: logger, handlerType: Handler.self)
80-
startupFuture.flatMap { handler -> EventLoopFuture<(Handler, Result<Int, Error>)> in
80+
let startupFuture = runner.initialize(logger: logger, terminator: terminator, handlerType: Handler.self)
81+
startupFuture.flatMap { handler -> EventLoopFuture<Result<Int, Error>> in
8182
// after the startup future has succeeded, we have a handler that we can use
8283
// to `run` the lambda.
8384
let finishedPromise = self.eventLoop.makePromise(of: Int.self)
8485
self.state = .active(runner, handler)
8586
self.run(promise: finishedPromise)
86-
return finishedPromise.futureResult.mapResult { (handler, $0) }
87-
}
88-
.flatMap { handler, runnerResult -> EventLoopFuture<Int> in
87+
return finishedPromise.futureResult.mapResult { $0 }
88+
}.flatMap { runnerResult -> EventLoopFuture<Int> in
8989
// after the lambda finishPromise has succeeded or failed we need to
9090
// shutdown the handler
91-
let shutdownContext = Lambda.ShutdownContext(logger: logger, eventLoop: self.eventLoop)
92-
return handler.shutdown(context: shutdownContext).flatMapErrorThrowing { error in
93-
// if, we had an error shuting down the lambda, we want to concatenate it with
91+
terminator.terminate(eventLoop: self.eventLoop).flatMapErrorThrowing { error in
92+
// if, we had an error shutting down the handler, we want to concatenate it with
9493
// the runner result
9594
logger.error("Error shutting down handler: \(error)")
9695
throw Lambda.RuntimeError.shutdownError(shutdownError: error, runnerResult: runnerResult)
Lines changed: 139 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,139 @@
1+
//===----------------------------------------------------------------------===//
2+
//
3+
// This source file is part of the SwiftAWSLambdaRuntime open source project
4+
//
5+
// Copyright (c) 2022 Apple Inc. and the SwiftAWSLambdaRuntime project authors
6+
// Licensed under Apache License v2.0
7+
//
8+
// See LICENSE.txt for license information
9+
// See CONTRIBUTORS.txt for the list of SwiftAWSLambdaRuntime project authors
10+
//
11+
// SPDX-License-Identifier: Apache-2.0
12+
//
13+
//===----------------------------------------------------------------------===//
14+
15+
import NIOConcurrencyHelpers
16+
import NIOCore
17+
18+
/// Lambda terminator.
19+
/// Utility to manage the lambda shutdown sequence.
20+
public final class LambdaTerminator {
21+
private typealias Handler = (EventLoop) -> EventLoopFuture<Void>
22+
23+
private var storage: Storage
24+
25+
init() {
26+
self.storage = Storage()
27+
}
28+
29+
/// Register a shutdown handler with the terminator
30+
///
31+
/// - parameters:
32+
/// - name: Display name for logging purposes
33+
/// - handler: The shutdown handler to call when terminating the Lambda.
34+
/// Shutdown handlers are called in the reverse order of being registered.
35+
///
36+
/// - Returns: A `RegistrationKey` that can be used to de-register the handler when its no longer needed.
37+
@discardableResult
38+
public func register(name: String, handler: @escaping (EventLoop) -> EventLoopFuture<Void>) -> RegistrationKey {
39+
let key = RegistrationKey()
40+
self.storage.add(key: key, name: name, handler: handler)
41+
return key
42+
}
43+
44+
/// De-register a shutdown handler with the terminator
45+
///
46+
/// - parameters:
47+
/// - key: A `RegistrationKey` obtained from calling the register API.
48+
public func deregister(_ key: RegistrationKey) {
49+
self.storage.remove(key)
50+
}
51+
52+
/// Begin the termination cycle
53+
/// Shutdown handlers are called in the reverse order of being registered.
54+
///
55+
/// - parameters:
56+
/// - eventLoop: The `EventLoop` to run the termination on.
57+
///
58+
/// - Returns: An `EventLoopFuture` with the result of the termination cycle.
59+
internal func terminate(eventLoop: EventLoop) -> EventLoopFuture<Void> {
60+
func terminate(_ iterator: IndexingIterator<[(name: String, handler: Handler)]>, errors: [Error], promise: EventLoopPromise<Void>) {
61+
var iterator = iterator
62+
guard let handler = iterator.next()?.handler else {
63+
if errors.isEmpty {
64+
return promise.succeed(())
65+
} else {
66+
return promise.fail(TerminationError(underlying: errors))
67+
}
68+
}
69+
handler(eventLoop).whenComplete { result in
70+
var errors = errors
71+
if case .failure(let error) = result {
72+
errors.append(error)
73+
}
74+
return terminate(iterator, errors: errors, promise: promise)
75+
}
76+
}
77+
78+
// terminate in cascading, reverse order
79+
let promise = eventLoop.makePromise(of: Void.self)
80+
terminate(self.storage.handlers.reversed().makeIterator(), errors: [], promise: promise)
81+
return promise.futureResult
82+
}
83+
}
84+
85+
extension LambdaTerminator {
86+
/// Lambda terminator registration key.
87+
public struct RegistrationKey: Hashable, CustomStringConvertible {
88+
var value: String
89+
90+
init() {
91+
// UUID basically
92+
self.value = LambdaRequestID().uuidString
93+
}
94+
95+
public var description: String {
96+
self.value
97+
}
98+
}
99+
}
100+
101+
extension LambdaTerminator {
102+
private final class Storage {
103+
private let lock: Lock
104+
private var index: [RegistrationKey]
105+
private var map: [RegistrationKey: (name: String, handler: Handler)]
106+
107+
init() {
108+
self.lock = .init()
109+
self.index = []
110+
self.map = [:]
111+
}
112+
113+
func add(key: RegistrationKey, name: String, handler: @escaping Handler) {
114+
self.lock.withLock {
115+
self.index.append(key)
116+
self.map[key] = (name: name, handler: handler)
117+
}
118+
}
119+
120+
func remove(_ key: RegistrationKey) {
121+
self.lock.withLock {
122+
self.index = self.index.filter { $0 != key }
123+
self.map[key] = nil
124+
}
125+
}
126+
127+
var handlers: [(name: String, handler: Handler)] {
128+
self.lock.withLock {
129+
self.index.compactMap { self.map[$0] }
130+
}
131+
}
132+
}
133+
}
134+
135+
extension LambdaTerminator {
136+
struct TerminationError: Error {
137+
let underlying: [Error]
138+
}
139+
}

Sources/AWSLambdaTesting/Lambda+Testing.swift

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -51,8 +51,7 @@ extension Lambda {
5151
public init(requestID: String = "\(DispatchTime.now().uptimeNanoseconds)",
5252
traceID: String = "Root=\(DispatchTime.now().uptimeNanoseconds);Parent=\(DispatchTime.now().uptimeNanoseconds);Sampled=1",
5353
invokedFunctionARN: String = "arn:aws:lambda:us-west-1:\(DispatchTime.now().uptimeNanoseconds):function:custom-runtime",
54-
timeout: DispatchTimeInterval = .seconds(5))
55-
{
54+
timeout: DispatchTimeInterval = .seconds(5)) {
5655
self.requestID = requestID
5756
self.traceID = traceID
5857
self.invokedFunctionARN = invokedFunctionARN

Tests/AWSLambdaRuntimeCoreTests/LambdaRuntimeTest.swift

Lines changed: 25 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -66,23 +66,37 @@ class LambdaRuntimeTest: XCTestCase {
6666
let eventLoopGroup = MultiThreadedEventLoopGroup(numberOfThreads: 1)
6767
defer { XCTAssertNoThrow(try eventLoopGroup.syncShutdownGracefully()) }
6868

69-
struct ShutdownError: Error {}
69+
struct ShutdownError: Error {
70+
let description: String
71+
}
7072

7173
struct ShutdownErrorHandler: EventLoopLambdaHandler {
7274
typealias Event = String
7375
typealias Output = Void
7476

7577
static func makeHandler(context: Lambda.InitializationContext) -> EventLoopFuture<ShutdownErrorHandler> {
76-
context.eventLoop.makeSucceededFuture(ShutdownErrorHandler())
78+
// register shutdown operation
79+
context.terminator.register(name: "test 1", handler: { eventLoop in
80+
eventLoop.makeFailedFuture(ShutdownError(description: "error 1"))
81+
})
82+
context.terminator.register(name: "test 2", handler: { eventLoop in
83+
eventLoop.makeSucceededVoidFuture()
84+
})
85+
context.terminator.register(name: "test 3", handler: { eventLoop in
86+
eventLoop.makeFailedFuture(ShutdownError(description: "error 2"))
87+
})
88+
context.terminator.register(name: "test 4", handler: { eventLoop in
89+
eventLoop.makeSucceededVoidFuture()
90+
})
91+
context.terminator.register(name: "test 5", handler: { eventLoop in
92+
eventLoop.makeFailedFuture(ShutdownError(description: "error 3"))
93+
})
94+
return context.eventLoop.makeSucceededFuture(ShutdownErrorHandler())
7795
}
7896

7997
func handle(_ event: String, context: LambdaContext) -> EventLoopFuture<Void> {
8098
context.eventLoop.makeSucceededVoidFuture()
8199
}
82-
83-
func shutdown(context: Lambda.ShutdownContext) -> EventLoopFuture<Void> {
84-
context.eventLoop.makeFailedFuture(ShutdownError())
85-
}
86100
}
87101

88102
let eventLoop = eventLoopGroup.next()
@@ -95,7 +109,11 @@ class LambdaRuntimeTest: XCTestCase {
95109
XCTFail("Unexpected error: \(error)"); return
96110
}
97111

98-
XCTAssert(shutdownError is ShutdownError)
112+
XCTAssertEqual(shutdownError as? LambdaTerminator.TerminationError, LambdaTerminator.TerminationError(underlying: [
113+
ShutdownError(description: "error 3"),
114+
ShutdownError(description: "error 2"),
115+
ShutdownError(description: "error 1"),
116+
]))
99117
XCTAssertEqual(runtimeError as? Lambda.RuntimeError, .badStatusCode(.internalServerError))
100118
}
101119
}

Tests/AWSLambdaRuntimeCoreTests/Utils.swift

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,10 +23,11 @@ func runLambda<Handler: ByteBufferLambdaHandler>(behavior: LambdaServerBehavior,
2323
defer { XCTAssertNoThrow(try eventLoopGroup.syncShutdownGracefully()) }
2424
let logger = Logger(label: "TestLogger")
2525
let configuration = Lambda.Configuration(runtimeEngine: .init(requestTimeout: .milliseconds(100)))
26+
let terminator = LambdaTerminator()
2627
let runner = Lambda.Runner(eventLoop: eventLoopGroup.next(), configuration: configuration)
2728
let server = try MockLambdaServer(behavior: behavior).start().wait()
2829
defer { XCTAssertNoThrow(try server.stop().wait()) }
29-
try runner.initialize(logger: logger, handlerType: handlerType).flatMap { handler in
30+
try runner.initialize(logger: logger, terminator: terminator, handlerType: handlerType).flatMap { handler in
3031
runner.run(logger: logger, handler: handler)
3132
}.wait()
3233
}
@@ -66,3 +67,13 @@ extension Lambda.RuntimeError: Equatable {
6667
String(describing: lhs) == String(describing: rhs)
6768
}
6869
}
70+
71+
extension LambdaTerminator.TerminationError: Equatable {
72+
public static func == (lhs: Self, rhs: Self) -> Bool {
73+
guard lhs.underlying.count == rhs.underlying.count else {
74+
return false
75+
}
76+
// technically incorrect, but good enough for our tests
77+
return String(describing: lhs) == String(describing: rhs)
78+
}
79+
}

Tests/AWSLambdaRuntimeTests/Lambda+CodableTest.swift

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -172,7 +172,8 @@ class CodableLambdaTest: XCTestCase {
172172
Lambda.InitializationContext(
173173
logger: Logger(label: "test"),
174174
eventLoop: self.eventLoopGroup.next(),
175-
allocator: ByteBufferAllocator()
175+
allocator: ByteBufferAllocator(),
176+
terminator: LambdaTerminator()
176177
)
177178
}
178179
}

0 commit comments

Comments
 (0)