Skip to content

Commit d06d22c

Browse files
authored
Lambda factory as a protocol requirement. (#244)
1 parent 5d235c0 commit d06d22c

15 files changed

+236
-207
lines changed

Examples/Benchmark/main.swift renamed to Examples/Benchmark/BenchmarkHandler.swift

+6-3
Original file line numberDiff line numberDiff line change
@@ -20,13 +20,16 @@ import NIOCore
2020
// `EventLoopLambdaHandler` does not offload the Lambda processing to a separate thread
2121
// while the closure-based handlers do.
2222

23-
struct MyLambda: EventLoopLambdaHandler {
23+
@main
24+
struct BenchmarkHandler: EventLoopLambdaHandler {
2425
typealias Event = String
2526
typealias Output = String
2627

28+
static func makeHandler(context: Lambda.InitializationContext) -> EventLoopFuture<Self> {
29+
context.eventLoop.makeSucceededFuture(BenchmarkHandler())
30+
}
31+
2732
func handle(_ event: String, context: LambdaContext) -> EventLoopFuture<String> {
2833
context.eventLoop.makeSucceededFuture("hello, world!")
2934
}
3035
}
31-
32-
Lambda.run { $0.eventLoop.makeSucceededFuture(MyLambda()) }

Examples/Deployment/Sources/Benchmark/main.swift renamed to Examples/Deployment/Sources/Benchmark/BenchmarkHandler.swift

+5-2
Original file line numberDiff line numberDiff line change
@@ -20,13 +20,16 @@ import NIO
2020
// `EventLoopLambdaHandler` does not offload the Lambda processing to a separate thread
2121
// while the closure-based handlers do.
2222

23+
@main
2324
struct BenchmarkHandler: EventLoopLambdaHandler {
2425
typealias Event = String
2526
typealias Output = String
2627

28+
static func makeHandler(context: Lambda.InitializationContext) -> EventLoopFuture<Self> {
29+
context.eventLoop.makeSucceededFuture(BenchmarkHandler())
30+
}
31+
2732
func handle(_ event: String, context: LambdaContext) -> EventLoopFuture<String> {
2833
context.eventLoop.makeSucceededFuture("hello, world!")
2934
}
3035
}
31-
32-
Lambda.run { $0.eventLoop.makeSucceededFuture(BenchmarkHandler()) }

Sources/AWSLambdaRuntimeCore/Lambda.swift

+18-42
Original file line numberDiff line numberDiff line change
@@ -24,27 +24,6 @@ import NIOCore
2424
import NIOPosix
2525

2626
public enum Lambda {
27-
public typealias Handler = ByteBufferLambdaHandler
28-
29-
/// `ByteBufferLambdaHandler` factory.
30-
///
31-
/// A function that takes a `InitializationContext` and returns an `EventLoopFuture` of a `ByteBufferLambdaHandler`
32-
public typealias HandlerFactory = (InitializationContext) -> EventLoopFuture<Handler>
33-
34-
/// Run a Lambda defined by implementing the `LambdaHandler` protocol provided via a `LambdaHandlerFactory`.
35-
/// Use this to initialize all your resources that you want to cache between invocations. This could be database connections and HTTP clients for example.
36-
/// It is encouraged to use the given `EventLoop`'s conformance to `EventLoopGroup` when initializing NIO dependencies. This will improve overall performance.
37-
///
38-
/// - parameters:
39-
/// - factory: A `ByteBufferLambdaHandler` factory.
40-
///
41-
/// - note: This is a blocking operation that will run forever, as its lifecycle is managed by the AWS Lambda Runtime Engine.
42-
public static func run(_ factory: @escaping HandlerFactory) {
43-
if case .failure(let error) = self.run(factory: factory) {
44-
fatalError("\(error)")
45-
}
46-
}
47-
4827
/// Utility to access/read environment variables
4928
public static func env(_ name: String) -> String? {
5029
guard let value = getenv(name) else {
@@ -53,30 +32,27 @@ public enum Lambda {
5332
return String(cString: value)
5433
}
5534

56-
#if compiler(>=5.5) && canImport(_Concurrency)
57-
// for testing and internal use
58-
@available(macOS 12, iOS 15, tvOS 15, watchOS 8, *)
59-
internal static func run<Handler: LambdaHandler>(configuration: Configuration = .init(), handlerType: Handler.Type) -> Result<Int, Error> {
60-
self.run(configuration: configuration, factory: { context -> EventLoopFuture<ByteBufferLambdaHandler> in
61-
let promise = context.eventLoop.makePromise(of: ByteBufferLambdaHandler.self)
62-
promise.completeWithTask {
63-
try await Handler(context: context)
64-
}
65-
return promise.futureResult
66-
})
67-
}
68-
#endif
69-
70-
// for testing and internal use
71-
internal static func run(configuration: Configuration = .init(), factory: @escaping HandlerFactory) -> Result<Int, Error> {
72-
let _run = { (configuration: Configuration, factory: @escaping HandlerFactory) -> Result<Int, Error> in
35+
/// Run a Lambda defined by implementing the ``ByteBufferLambdaHandler`` protocol.
36+
/// The Runtime will manage the Lambdas application lifecycle automatically. It will invoke the
37+
/// ``ByteBufferLambdaHandler/makeHandler(context:)`` to create a new Handler.
38+
///
39+
/// - parameters:
40+
/// - configuration: A Lambda runtime configuration object
41+
/// - handlerType: The Handler to create and invoke.
42+
///
43+
/// - note: This is a blocking operation that will run forever, as its lifecycle is managed by the AWS Lambda Runtime Engine.
44+
internal static func run<Handler: ByteBufferLambdaHandler>(
45+
configuration: Configuration = .init(),
46+
handlerType: Handler.Type
47+
) -> Result<Int, Error> {
48+
let _run = { (configuration: Configuration) -> Result<Int, Error> in
7349
Backtrace.install()
7450
var logger = Logger(label: "Lambda")
7551
logger.logLevel = configuration.general.logLevel
7652

7753
var result: Result<Int, Error>!
7854
MultiThreadedEventLoopGroup.withCurrentThreadAsEventLoop { eventLoop in
79-
let runtime = LambdaRuntime(eventLoop: eventLoop, logger: logger, configuration: configuration, factory: factory)
55+
let runtime = LambdaRuntime<Handler>(eventLoop: eventLoop, logger: logger, configuration: configuration)
8056
#if DEBUG
8157
let signalSource = trap(signal: configuration.lifecycle.stopSignal) { signal in
8258
logger.info("intercepted signal: \(signal)")
@@ -108,16 +84,16 @@ public enum Lambda {
10884
if Lambda.env("LOCAL_LAMBDA_SERVER_ENABLED").flatMap(Bool.init) ?? false {
10985
do {
11086
return try Lambda.withLocalServer {
111-
_run(configuration, factory)
87+
_run(configuration)
11288
}
11389
} catch {
11490
return .failure(error)
11591
}
11692
} else {
117-
return _run(configuration, factory)
93+
return _run(configuration)
11894
}
11995
#else
120-
return _run(configuration, factory)
96+
return _run(configuration)
12197
#endif
12298
}
12399
}

Sources/AWSLambdaRuntimeCore/LambdaContext.swift

+3-1
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,9 @@ import NIOCore
2020

2121
extension Lambda {
2222
/// Lambda runtime initialization context.
23-
/// The Lambda runtime generates and passes the `InitializationContext` to the Lambda factory as an argument.
23+
/// The Lambda runtime generates and passes the `InitializationContext` to the Handlers
24+
/// ``ByteBufferLambdaHandler/makeHandler(context:)`` or ``LambdaHandler/init(context:)``
25+
/// as an argument.
2426
public struct InitializationContext {
2527
/// `Logger` to log with
2628
///

Sources/AWSLambdaRuntimeCore/LambdaHandler.swift

+56-14
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,13 @@ import NIOCore
1818
// MARK: - LambdaHandler
1919

2020
#if compiler(>=5.5) && canImport(_Concurrency)
21-
/// Strongly typed, processing protocol for a Lambda that takes a user defined `Event` and returns a user defined `Output` async.
21+
/// Strongly typed, processing protocol for a Lambda that takes a user defined
22+
/// ``EventLoopLambdaHandler/Event`` and returns a user defined
23+
/// ``EventLoopLambdaHandler/Output`` asynchronously.
24+
///
25+
/// - note: Most users should implement this protocol instead of the lower
26+
/// level protocols ``EventLoopLambdaHandler`` and
27+
/// ``ByteBufferLambdaHandler``.
2228
@available(macOS 12, iOS 15, tvOS 15, watchOS 8, *)
2329
public protocol LambdaHandler: EventLoopLambdaHandler {
2430
/// The Lambda initialization method
@@ -42,6 +48,14 @@ public protocol LambdaHandler: EventLoopLambdaHandler {
4248

4349
@available(macOS 12, iOS 15, tvOS 15, watchOS 8, *)
4450
extension LambdaHandler {
51+
public static func makeHandler(context: Lambda.InitializationContext) -> EventLoopFuture<Self> {
52+
let promise = context.eventLoop.makePromise(of: Self.self)
53+
promise.completeWithTask {
54+
try await Self(context: context)
55+
}
56+
return promise.futureResult
57+
}
58+
4559
public func handle(_ event: Event, context: LambdaContext) -> EventLoopFuture<Output> {
4660
let promise = context.eventLoop.makePromise(of: Output.self)
4761
promise.completeWithTask {
@@ -51,25 +65,30 @@ extension LambdaHandler {
5165
}
5266
}
5367

54-
@available(macOS 12, iOS 15, tvOS 15, watchOS 8, *)
55-
extension LambdaHandler {
56-
public static func main() {
57-
_ = Lambda.run(handlerType: Self.self)
58-
}
59-
}
6068
#endif
6169

6270
// MARK: - EventLoopLambdaHandler
6371

64-
/// Strongly typed, `EventLoopFuture` based processing protocol for a Lambda that takes a user defined `Event` and returns a user defined `Output` asynchronously.
65-
/// `EventLoopLambdaHandler` extends `ByteBufferLambdaHandler`, performing `ByteBuffer` -> `Event` decoding and `Output` -> `ByteBuffer` encoding.
72+
/// Strongly typed, `EventLoopFuture` based processing protocol for a Lambda that takes a user
73+
/// defined ``Event`` and returns a user defined ``Output`` asynchronously.
6674
///
67-
/// - note: To implement a Lambda, implement either `LambdaHandler` or the `EventLoopLambdaHandler` protocol.
68-
/// The `LambdaHandler` will offload the Lambda execution to a `DispatchQueue` making processing safer but slower
69-
/// The `EventLoopLambdaHandler` will execute the Lambda on the same `EventLoop` as the core runtime engine, making the processing faster but requires
70-
/// more care from the implementation to never block the `EventLoop`.
75+
/// ``EventLoopLambdaHandler`` extends ``ByteBufferLambdaHandler``, performing
76+
/// `ByteBuffer` -> ``Event`` decoding and ``Output`` -> `ByteBuffer` encoding.
77+
///
78+
/// - note: To implement a Lambda, implement either ``LambdaHandler`` or the
79+
/// ``EventLoopLambdaHandler`` protocol. The ``LambdaHandler`` will offload
80+
/// the Lambda execution to an async Task making processing safer but slower (due to
81+
/// fewer thread hops).
82+
/// The ``EventLoopLambdaHandler`` will execute the Lambda on the same `EventLoop`
83+
/// as the core runtime engine, making the processing faster but requires more care from the
84+
/// implementation to never block the `EventLoop`. Implement this protocol only in performance
85+
/// critical situations and implement ``LambdaHandler`` in all other circumstances.
7186
public protocol EventLoopLambdaHandler: ByteBufferLambdaHandler {
87+
/// The lambda functions input. In most cases this should be Codable. If your event originates from an
88+
/// AWS service, have a look at [AWSLambdaEvents](https://github.com/swift-server/swift-aws-lambda-events),
89+
/// which provides a number of commonly used AWS Event implementations.
7290
associatedtype Event
91+
/// The lambda functions output. Can be `Void`.
7392
associatedtype Output
7493

7594
/// The Lambda handling method
@@ -135,9 +154,18 @@ extension EventLoopLambdaHandler where Output == Void {
135154

136155
/// An `EventLoopFuture` based processing protocol for a Lambda that takes a `ByteBuffer` and returns a `ByteBuffer?` asynchronously.
137156
///
138-
/// - note: This is a low level protocol designed to power the higher level `EventLoopLambdaHandler` and `LambdaHandler` based APIs.
157+
/// - note: This is a low level protocol designed to power the higher level ``EventLoopLambdaHandler`` and
158+
/// ``LambdaHandler`` based APIs.
139159
/// Most users are not expected to use this protocol.
140160
public protocol ByteBufferLambdaHandler {
161+
/// Create your Lambda handler for the runtime.
162+
///
163+
/// Use this to initialize all your resources that you want to cache between invocations. This could be database
164+
/// connections and HTTP clients for example. It is encouraged to use the given `EventLoop`'s conformance
165+
/// to `EventLoopGroup` when initializing NIO dependencies. This will improve overall performance, as it
166+
/// minimizes thread hopping.
167+
static func makeHandler(context: Lambda.InitializationContext) -> EventLoopFuture<Self>
168+
141169
/// The Lambda handling method
142170
/// Concrete Lambda handlers implement this method to provide the Lambda functionality.
143171
///
@@ -163,6 +191,20 @@ extension ByteBufferLambdaHandler {
163191
}
164192
}
165193

194+
extension ByteBufferLambdaHandler {
195+
/// Initializes and runs the lambda function.
196+
///
197+
/// If you precede your ``ByteBufferLambdaHandler`` conformer's declaration with the
198+
/// [@main](https://docs.swift.org/swift-book/ReferenceManual/Attributes.html#ID626)
199+
/// attribute, the system calls the conformer's `main()` method to launch the lambda function.
200+
///
201+
/// The lambda runtime provides a default implementation of the method that manages the launch
202+
/// process.
203+
public static func main() {
204+
_ = Lambda.run(configuration: .init(), handlerType: Self.self)
205+
}
206+
}
207+
166208
@usableFromInline
167209
enum CodecError: Error {
168210
case requestDecoding(Error)

Sources/AWSLambdaRuntimeCore/LambdaRunner.swift

+3-3
Original file line numberDiff line numberDiff line change
@@ -34,14 +34,14 @@ 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(logger: Logger, factory: @escaping HandlerFactory) -> EventLoopFuture<Handler> {
37+
func initialize<Handler: ByteBufferLambdaHandler>(logger: Logger, handlerType: Handler.Type) -> EventLoopFuture<Handler> {
3838
logger.debug("initializing lambda")
3939
// 1. create the handler from the factory
4040
// 2. report initialization error if one occured
4141
let context = InitializationContext(logger: logger,
4242
eventLoop: self.eventLoop,
4343
allocator: self.allocator)
44-
return factory(context)
44+
return Handler.makeHandler(context: context)
4545
// Hopping back to "our" EventLoop is important in case the factory returns a future
4646
// that originated from a foreign EventLoop/EventLoopGroup.
4747
// This can happen if the factory uses a library (let's say a database client) that manages its own threads/loops
@@ -56,7 +56,7 @@ extension Lambda {
5656
}
5757
}
5858

59-
func run(logger: Logger, handler: Handler) -> EventLoopFuture<Void> {
59+
func run<Handler: ByteBufferLambdaHandler>(logger: Logger, handler: Handler) -> EventLoopFuture<Void> {
6060
logger.debug("lambda invocation sequence starting")
6161
// 1. request invocation from lambda runtime engine
6262
self.isGettingNextInvocation = true

Sources/AWSLambdaRuntimeCore/LambdaRuntime.swift

+9-12
Original file line numberDiff line numberDiff line change
@@ -19,12 +19,11 @@ import NIOCore
1919
/// `LambdaRuntime` manages the Lambda process lifecycle.
2020
///
2121
/// - note: It is intended to be used within a single `EventLoop`. For this reason this class is not thread safe.
22-
public final class LambdaRuntime {
22+
public final class LambdaRuntime<Handler: ByteBufferLambdaHandler> {
2323
private let eventLoop: EventLoop
2424
private let shutdownPromise: EventLoopPromise<Int>
2525
private let logger: Logger
2626
private let configuration: Lambda.Configuration
27-
private let factory: Lambda.HandlerFactory
2827

2928
private var state = State.idle {
3029
willSet {
@@ -38,17 +37,15 @@ public final class LambdaRuntime {
3837
/// - parameters:
3938
/// - eventLoop: An `EventLoop` to run the Lambda on.
4039
/// - logger: A `Logger` to log the Lambda events.
41-
/// - factory: A `LambdaHandlerFactory` to create the concrete Lambda handler.
42-
public convenience init(eventLoop: EventLoop, logger: Logger, factory: @escaping Lambda.HandlerFactory) {
43-
self.init(eventLoop: eventLoop, logger: logger, configuration: .init(), factory: factory)
40+
public convenience init(eventLoop: EventLoop, logger: Logger) {
41+
self.init(eventLoop: eventLoop, logger: logger, configuration: .init())
4442
}
4543

46-
init(eventLoop: EventLoop, logger: Logger, configuration: Lambda.Configuration, factory: @escaping Lambda.HandlerFactory) {
44+
init(eventLoop: EventLoop, logger: Logger, configuration: Lambda.Configuration) {
4745
self.eventLoop = eventLoop
4846
self.shutdownPromise = eventLoop.makePromise(of: Int.self)
4947
self.logger = logger
5048
self.configuration = configuration
51-
self.factory = factory
5249
}
5350

5451
deinit {
@@ -79,16 +76,16 @@ public final class LambdaRuntime {
7976
logger[metadataKey: "lifecycleId"] = .string(self.configuration.lifecycle.id)
8077
let runner = Lambda.Runner(eventLoop: self.eventLoop, configuration: self.configuration)
8178

82-
let startupFuture = runner.initialize(logger: logger, factory: self.factory)
83-
startupFuture.flatMap { handler -> EventLoopFuture<(ByteBufferLambdaHandler, Result<Int, Error>)> in
79+
let startupFuture = runner.initialize(logger: logger, handlerType: Handler.self)
80+
startupFuture.flatMap { handler -> EventLoopFuture<(Handler, Result<Int, Error>)> in
8481
// after the startup future has succeeded, we have a handler that we can use
8582
// to `run` the lambda.
8683
let finishedPromise = self.eventLoop.makePromise(of: Int.self)
8784
self.state = .active(runner, handler)
8885
self.run(promise: finishedPromise)
8986
return finishedPromise.futureResult.mapResult { (handler, $0) }
9087
}
91-
.flatMap { (handler, runnerResult) -> EventLoopFuture<Int> in
88+
.flatMap { handler, runnerResult -> EventLoopFuture<Int> in
9289
// after the lambda finishPromise has succeeded or failed we need to
9390
// shutdown the handler
9491
let shutdownContext = Lambda.ShutdownContext(logger: logger, eventLoop: self.eventLoop)
@@ -97,7 +94,7 @@ public final class LambdaRuntime {
9794
// the runner result
9895
logger.error("Error shutting down handler: \(error)")
9996
throw Lambda.RuntimeError.shutdownError(shutdownError: error, runnerResult: runnerResult)
100-
}.flatMapResult { (_) -> Result<Int, Error> in
97+
}.flatMapResult { _ -> Result<Int, Error> in
10198
// we had no error shutting down the lambda. let's return the runner's result
10299
runnerResult
103100
}
@@ -173,7 +170,7 @@ public final class LambdaRuntime {
173170
private enum State {
174171
case idle
175172
case initializing
176-
case active(Lambda.Runner, Lambda.Handler)
173+
case active(Lambda.Runner, Handler)
177174
case shuttingdown
178175
case shutdown
179176

0 commit comments

Comments
 (0)