Skip to content

Commit 8f1c4ff

Browse files
committed
terminator handler
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 8f1c4ff

File tree

9 files changed

+195
-63
lines changed

9 files changed

+195
-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: Terminator
43+
44+
init(logger: Logger, eventLoop: EventLoop, allocator: ByteBufferAllocator, terminator: Terminator) {
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: Lambda.Terminator()
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: Terminator, 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 = Lambda.Terminator()
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: 133 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,133 @@
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+
extension Lambda {
19+
/// Lambda terminator.
20+
/// Utility to manage the lambda shutdown sequence.
21+
public final class Terminator {
22+
public typealias Handler = (EventLoop) -> EventLoopFuture<Void>
23+
public typealias RegistrationKey = String
24+
25+
private var storage: Storage
26+
27+
public init() {
28+
self.storage = Storage()
29+
}
30+
31+
/// Register a shutdown handler with the terminator
32+
///
33+
/// - parameters:
34+
/// - name: Display name for logging purposes
35+
/// - handler: The shutdown handler to call when terminating the Lambda.
36+
/// Shutdown handlers are called in the reverse order of being registered.
37+
///
38+
/// - Returns: A `RegistrationKey` that can be used to de-register the handler when its no longer needed.
39+
@discardableResult
40+
public func register(name: String, handler: @escaping Handler) -> RegistrationKey {
41+
let key = LambdaRequestID().uuidString // UUID basically
42+
self.storage.add(key: key, name: name, handler: handler)
43+
return key
44+
}
45+
46+
/// De-register a shutdown handler with the terminator
47+
///
48+
/// - parameters:
49+
/// - key: A `RegistrationKey` obtained from calling the register API.
50+
public func deregister(_ key: RegistrationKey) {
51+
self.storage.remove(key)
52+
}
53+
54+
/// Begin the termination cycle
55+
/// Shutdown handlers are called in the reverse order of being registered.
56+
///
57+
/// - parameters:
58+
/// - eventLoop: The `EventLoop` to run the termination on.
59+
///
60+
/// - Returns: An`EventLoopFuture` with the result of the termination cycle.
61+
internal func terminate(eventLoop: EventLoop) -> EventLoopFuture<Void> {
62+
func terminate(_ iterator: IndexingIterator<[(name: String, handler: Handler)]>, errors: [Error], promise: EventLoopPromise<Void>) {
63+
var iterator = iterator
64+
guard let handler = iterator.next()?.handler else {
65+
if errors.isEmpty {
66+
return promise.succeed(())
67+
} else {
68+
return promise.fail(TerminationError(underlying: errors))
69+
}
70+
}
71+
handler(eventLoop).whenComplete { result in
72+
var errors = errors
73+
if case .failure(let error) = result {
74+
errors.append(error)
75+
}
76+
return terminate(iterator, errors: errors, promise: promise)
77+
}
78+
}
79+
80+
// terminate in cascading, reverse order
81+
let promise = eventLoop.makePromise(of: Void.self)
82+
terminate(self.storage.handlers.reversed().makeIterator(), errors: [], promise: promise)
83+
return promise.futureResult
84+
}
85+
}
86+
87+
private final class Storage {
88+
private let lock: Lock
89+
private var index: [String]
90+
private var map: [String: (name: String, handler: Terminator.Handler)]
91+
92+
public init() {
93+
self.lock = .init()
94+
self.index = []
95+
self.map = [:]
96+
}
97+
98+
func add(key: String, name: String, handler: @escaping Terminator.Handler) {
99+
self.lock.withLock {
100+
self.index.append(key)
101+
self.map[key] = (name: name, handler: handler)
102+
}
103+
}
104+
105+
func remove(_ key: String) {
106+
self.lock.withLock {
107+
self.index = self.index.filter { $0 != key }
108+
self.map[key] = nil
109+
}
110+
}
111+
112+
var handlers: [(name: String, handler: Terminator.Handler)] {
113+
self.lock.withLock {
114+
self.index.compactMap { self.map[$0] }
115+
}
116+
}
117+
}
118+
119+
struct TerminationError: Error {
120+
let underlying: [Error]
121+
}
122+
}
123+
124+
extension Result {
125+
fileprivate var error: Error? {
126+
switch self {
127+
case .failure(let error):
128+
return error
129+
case .success:
130+
return .none
131+
}
132+
}
133+
}

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 3", handler: { eventLoop in
89+
eventLoop.makeSucceededVoidFuture()
90+
})
91+
context.terminator.register(name: "test 4", 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? Lambda.TerminationError, Lambda.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 = Lambda.Terminator()
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 Lambda.TerminationError: Equatable {
72+
public static func == (lhs: Lambda.TerminationError, rhs: Lambda.TerminationError) -> 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: Lambda.Terminator()
176177
)
177178
}
178179
}

0 commit comments

Comments
 (0)