From c19ae239279464ff79b8ff449166fd2fb5570647 Mon Sep 17 00:00:00 2001 From: Aryan Shah Date: Wed, 7 Aug 2024 16:57:58 +0100 Subject: [PATCH 01/10] v2 API Proposal Document --- .../Proposals/0001-v2-api.md | 556 ++++++++++++++++++ 1 file changed, 556 insertions(+) create mode 100644 Sources/AWSLambdaRuntimeCore/Documentation.docc/Proposals/0001-v2-api.md diff --git a/Sources/AWSLambdaRuntimeCore/Documentation.docc/Proposals/0001-v2-api.md b/Sources/AWSLambdaRuntimeCore/Documentation.docc/Proposals/0001-v2-api.md new file mode 100644 index 00000000..8a438a2b --- /dev/null +++ b/Sources/AWSLambdaRuntimeCore/Documentation.docc/Proposals/0001-v2-api.md @@ -0,0 +1,556 @@ +# v2 API proposal for swift-aws-lambda-runtime + +`swift-aws-lambda-runtime` is an important library for the Swift on Server ecosystem. The initial API was written before async/await was introduced to Swift. When async/await was introduced, shims were added to bridge between the underlying SwiftNIO `EventLoop` interfaces and async/await. However, just like ``gRPC-swift`` and `postgres-nio`, we now want to shift to solely using async/await instead of `EventLoop` interfaces. For this, large parts of the current API have to be reconsidered. + +## Motivation + +### Current Limitations + +#### EventLoop interfaces + +The current API extensively uses the `EventLoop` family of interfaces from SwiftNIO in many areas. To use these interfaces correctly though, it requires developers to exercise great care and be aware of certain details such as never running blocking code on the same `EventLoop` the library uses. Developers also need to understand the various transform methods that are used to work with `EventLoop`s and `EventLoopFuture`s. This results in a lot of cognitive complexity and makes the code in the current API hard to reason about and maintain. For these reasons, the overarching trend in the Swift on Server ecosystem is to shift to newer, more readable, Swift concurrency constructs and de-couple from SwiftNIO’s `EventLoop` interfaces. + +#### No ownership of the main() function + +A Lambda function can currently be implemented through conformance to the various handler protocols defined in `AWSLambdaRuntimeCore/LambdaHandler`. Each of these protocols have an extension which implements a `static func main()`. This allows users to annotate their `LambdaHandler` conforming object with `@main`. The `static func main()` calls the internal `Lambda.run()` function, which starts the Lambda function. Since the `Lambda.run()` method is internal, users cannot override the default implementation. This has proven challenging for users who want to [set up global properties before the Lambda starts-up](https://github.com/swift-server/swift-aws-lambda-runtime/issues/265). Setting up global properties is required to customize the Swift Logging, Metric and Tracing backend. + +#### Non-trivial transition from SimpleLambdaHandler to LambdaHandler + +The `SimpleLambdaHandler` protocol provides a quick and easy way to implement a basic Lambda function. It only requires an implementation of the `handle` function where the business logic of the Lambda function can be written. `SimpleLambdaHandler` is perfectly sufficient for small use-cases as the user does not need to spend much time looking into the library. + +However, `SimpleLambdaHandler` cannot be used when services such as a database client need to be initalized before the Lambda runtime starts and then also gracefully shutdown prior to the runtime terminating. This is because the only way to register termination logic is through the `LambdaInitializationContext` (containing a field `terminator: LambdaTerminator`) which is created and used *internally* within `LambdaRuntime` and never exposed through `SimpleLambdaHandler`. For such use-cases, other handler protocols like `LambdaHandler` must be used. `LambdaHandler` exposes a `context` argument of type `LambdaInitializationContext` through its initializer. Within the initializer, required services can be initalized and their graceful shutdown logic can be registered with the `context.terminator.register` function. + +Yet, `LambdaHandler` is quite cumbersome to use in such use-cases as users have to deviate from the established norms of the Swift on Server ecosystem in order to cleanly manage the lifecycle of the services intended to be used. This is because the convenient `swift-service-lifecycle` v2 library — which is commonly used for cleanly managing the lifecycles of required services and widely supported by many libraries — cannot be used in a structured concurrency manner. + +#### Does not integrate well with swift-service-lifecycle in a structured concurrency manner + +The Lambda runtime can only be started using the **internal** `Lambda.run()` function. This function is called by the `main()` function defined by the `LambdaHandler` protocol, preventing users from injecting initialized services into the runtime *prior* to it starting. As shown below, this forces users to use an **unstructured concurrency** approach and manually initialize services, leading to the issue of the user then perhaps forgetting to gracefully shutdown the initalized services: + +```swift +struct MyLambda: LambdaHandler { + let pgClient: PostgresClient + + init(context: AWSLambdaRuntimeCore.LambdaInitializationContext) async throws { + /// Instantiate service + let client = PostgresClient(configuration: ...) + + /// Unstructured concurrency to initialize the service + let pgTask = Task { + await client.run() + } + + /// Store the client in `self` so that it can be used in `handle(...)` + self.pgClient = client + + /// !!! Must remember to explicitly register termination logic for PostgresClient !!! + context.terminator.register( + name: "PostgreSQL Client", + handler: { eventLoop in + pgTask.cancel() + return eventLoop.makeFutureWithTask { + await pgTask.value + } + } + ) + } + + func handle(_ event: Event, context: LambdaContext) async throws -> Output { + /// Use the initalized service stored in `self.pgClient` + try await self.pgClient.query(...) + } +} +``` + +#### Verbose Codable support + +In the current API, there are extensions and Codable wrapper classes for decoding events and encoding computed responses for *each* different handler protocol and for both `String` and `JSON` formats. This has resulted in a lot of boilerplate code which can very easily be made generic and simplified in v2. + +### New features + +#### Support response streaming + +In April 2023 [AWS introduced support for response streaming](https://aws.amazon.com/blogs/compute/introducing-aws-lambda-response-streaming/) in Lambda. The current API does not support streaming. For v2 we want to change this. + +#### Scheduling background work + +In May [AWS described in a blog post that you can run background tasks in Lambda](https://aws.amazon.com/blogs/compute/running-code-after-returning-a-response-from-an-aws-lambda-function/) until the runtime asks for more work from the control plane. We want to support this by adding new API that allows background processing, even after the response has been returned. + +## Proposed Solution + +#### async/await-first API + +Large parts of `Lambda`, `LambdaHandler`, and `LambdaRuntime` will be re-written to use async/await constructs in place of the `EventLoop` family of interfaces. + +#### Providing ownership of main() and support for swift-service-lifecycle + +* Instead of conforming to a handler protocol, users can now create a `LambdaRuntime` by passing in a handler closure. +* `LambdaRuntime` conforms to `ServiceLifecycle.Service` by implementing a `run()` method that contains initialization and graceful shutdown logic. +* This allows the lifecycle of the `LambdaRuntime` to be managed with `swift-service-lifecycle` *alongside* and in the same way the lifecycles of the required services are managed, e.g. `try await ServiceGroup(services: [postgresClient, ..., lambdaRuntime], ...).run()`. +* Dependencies can now be injected into `LambdaRuntime` — `swift-service-lifecycle` guarantees that the services will be initialized *before* the `LambdaRuntime`’s `run()` function is called. +* The required services can then be used within the handler in a structured concurrency manner. `swift-service-lifecycle` takes care of listening for termination signals and terminating the services as well as the `LambdaRuntime` in correct order. +* `LambdaTerminator` can now be eliminated because its role is replaced with `swift-service-lifecycle`. The termination logic of the Lambda function will be implemented in the conforming `run()` function of `LambdaRuntime`. + +With this, the earlier code snippet can be replaced with something much easier to read, maintain, and debug: + +```swift +/// Instantiate services +let postgresClient = PostgresClient() + +/// Instantiate LambdaRuntime with a closure handler implementing the business logic of the Lambda function +let runtime = LambdaRuntime { (event: Input, context: LambdaContext) in + /// Use initialized service within the handler + try await postgresClient.query(...) +} + +/// Use ServiceLifecycle to manage the initialization and termination +/// of the services as well as the LambdaRuntime +let serviceGroup = ServiceGroup( + services: [postgresClient, runtime], + configuration: .init(gracefulShutdownSignals: [.sigterm]), + logger: logger +) +try await serviceGroup.run() +``` + +#### Simplifying Codable support + +A detailed explanation is provided in the Codable Support section. In short, much of the boilerplate code defined for each handler protocol in `Lambda+Codable` and `Lambda+String` will be replaced with a single `LambdaCodableAdapter` class. + +This adapter class is generic over (1) any handler conforming to `LambdaHandler`, (2) the user-specified input and output types, and (3) any decoder and encoder conforming to `LambdaEventDecoder` and `LambdaOutputDecoder`. The adapter will wrap the underlying handler with encoding/decoding logic. + +#### Simplifying the handler protocols + +There are four different handler protocols in the current API (`SimpleLambdaHandler`, `LambdaHandler`, `EventLoopLambdaHandler`, `ByteBufferLambdaHandler`). As noted in the **Current Limitations** section, the ease-of-use varies for each handler protocol and users may not be able to easily determine which protocol best serves their use-case without spending time digging into the library. To reduce this problem and provide users with clear-cut options, we propose replacing all of the existing handler protocols with just two: `LambdaHandler` and `StreamingLambdaHandler`. Both will be explained in the **Detailed Solution** section. + +## Detailed Solution + +Below you’ll find explanation’s for all types that we want to use in AWS Lambda Runtime v2. + +### LambdaResponseWriter + +We will introduce a new `LambdaResponseWriter` protocol. It is used in the new `StreamingLambdaHandler` (defined below), which is the new base protocol for the `LambdaRuntime` (defined below as well). + +```swift +/// A writer object to write the Lambda response stream into +public protocol LambdaResponseWriter: ~Copyable { + /// Write a response part into the stream + func write(_ buffer: ByteBuffer) async throws + /// End the response stream + consuming func finish() + /// Write a response part into the stream and end the response stream + consuming func writeAndFinish(_ buffer: ByteBuffer) async throws + /// Report an error in the response stream + consuming func reportError(_ error: any Error) async throws +} +``` + +### LambdaContext + +`LambdaContext` will be largely unchanged, but the `eventLoop` property as well as the `allocator` property (of type `ByteBuffer`) will be removed. + +A new function `backgroundTask()` will also be added. This will allow tasks to be run in the background while and after the response is/has been sent. Please note that `LambdaContext` will not be Sendable anymore. + +```swift +/// A context object passed as part of an invocation in LambdaHandler handle functions. +public struct LambdaContext { + /// The request ID, which identifies the request that triggered the function invocation. + public var requestID: String { get } + + /// The AWS X-Ray tracing header. + public var traceID: String { get } + + /// The ARN of the Lambda function, version, or alias that's specified in the invocation. + public var invokedFunctionARN: String { get } + + /// The timestamp that the function times out. + public var deadline: DispatchWallTime { get } + + /// For invocations from the AWS Mobile SDK, data about the Amazon Cognito identity provider. + public var cognitoIdentity: String? { get } + + /// For invocations from the AWS Mobile SDK, data about the client application and device. + public var clientContext: String? { get } + + /// `Logger` to log with. + /// + /// - note: The `LogLevel` can be configured using the `LOG_LEVEL` environment variable. + public var logger: Logger { get } + + /// Schedule background work in lambda. This work must be completed before + /// the runtime can ask for further events. Note that AWS will continue to + /// charge you after the response has been returned, but work is still + /// processing in the background. + public func addBackgroundTask(_ body: sending @escaping () async -> ()) +} +``` + +### StreamingLambdaHandler + +The new `StreamingLambdaHandler` protocol is the base protocol to implement a Lambda function. Most users will not use this protocol and instead use the `LambdaHandler` protocol defined in the `Codable` Support section. + +```swift +/// The base LambdaHandler protocol +public protocol StreamingLambdaHandler: ~Copyable { + /// The business logic of the Lambda function + /// - Parameters: + /// - event: The invocation's input data + /// - responseWriter: A ``LambdaResponseWriter`` to write the invocations response to. + /// If no response or error is written to the `responseWriter` it will + /// report an error to the invoker. + /// - context: The LambdaContext containing the invocation's metadata + mutating func handle(_ event: ByteBuffer, responseWriter: consuming some LambdaResponseWriter, context: LambdaContext) async +} +``` + +The protocol is defined in a way that supports a broad range of use-cases. The handle method is marked as `mutating` to allow handlers to be implemented with a `struct`. Users can opt to even mark the handler as non-copyable. + +An implementation that sends the number 1 to 10 every 500ms could look like this: + +```swift +struct SendNumbersWithPause: StreamingLambdaHandler { + func handle( + _ event: ByteBuffer, + responseWriter: consuming some LambdaResponseWriter, + context: LambdaContext + ) async { + for i in 1...10 { + responseWriter.write(ByteBuffer(string: #"\#(i)\n\r"#)) + try? await Task.sleep(for: .milliseconds(500)) + } + responseWriter.finish() + } +} +``` + +The method is not marked as `throws` as we want the users of this API to handle errors themselves and call `LambdaResponseWriter.reportError(_:)` if needed. + +### LambdaRuntime + +`LambdaRuntime` is the class that communicates with the Lambda control plane as defined in [Building a custom runtime for AWS Lambda](https://docs.aws.amazon.com/lambda/latest/dg/runtimes-custom.html) and forward the invocations to the provided `StreamingLambdaHandler`. It will conform to `ServiceLifecycle.Service` to provide support for `swift-service-lifecycle`. + +```swift +/// The LambdaRuntime object. This object communicates with the Lambda control plane +/// to fetch work and report errors. +public final class LambdaRuntime: ServiceLifecycle.Service, Sendable + where Handler: StreamingLambdaHandler, Handler: ~Copyable +{ + + /// Create a LambdaRuntime by passing a handler, an eventLoop and a logger. + /// - Parameter handler: A ``StreamingLambdaHandler`` that will be invoked + /// - Parameter eventLoop: An ``EventLoop`` on which the LambdaRuntime will be + /// executed. Defaults to an EventLoop from + /// ``NIOSingletons.posixEventLoopGroup``. + /// - Parameter logger: A logger + public init( + handler: consuming sending Handler, + eventLoop: EventLoop = Lambda.defaultEventLoop, + logger: Logger = Logger(label: "Lambda") + ) + + /// Create a LambdaRuntime by passing a ``StreamingLambdaHandler``. + public convenience init(handler: consuming sending Handler) + + /// Starts the LambdaRuntime by connecting to the Lambda control plane to ask + /// for events to process. If the environment variable AWS_LAMBDA_RUNTIME_API is + /// set, the LambdaRuntime will connect to the Lambda control plane. Otherwise + /// it will start a mock server that can be used for testing at port 8080 + /// locally. + /// Cancel the task that runs this function to close the communication with + /// the Lambda control plane or close the local mock server. This function + /// only returns once cancelled. + public func run() async throws +} +``` + +The current API allows for a Lambda function to be tested locally through a mock server by requiring an environment variable named `LOCAL_LAMBDA_SERVER_ENABLED` to be set to `true`. If this environment variable is not set, the program immediately crashes as the user will not have the `AWS_LAMBDA_RUNTIME_API` environment variable on their local machine (set automatically when deployed to AWS Lambda). However, making the user set the `LOCAL_LAMBDA_SERVER_ENABLED` environment variable is an unnecessary step that can be avoided. In the v2 API, the `run()` function will automatically start the mock server when the `AWS_LAMBDA_RUNTIME_API` environment variable cannot be found. + +### Lambda + +We also add an enum to store a static function and a property on. We put this on the static `Lambda` because `LambdaRuntime` is generic and thus has bad ergonomics for static properties and functions. + +```swift +enum Lambda { + /// This returns the default EventLoop that a LambdaRuntime is scheduled on. + /// It uses `NIOSingletons.posixEventLoopGroup.next()` under the hood. + public static var defaultEventLoop: any EventLoop { get } + + /// Report a startup error to the Lambda Control Plane API + public static func reportStartupError(any Error) async +} +``` + +Since the library now provides ownership of the `main()` function and allows users to initialize services before the `LambdaRuntime` is initialized, the library cannot implicitly report [errors that occur during initialization to the dedicated endpoint AWS exposes](https://docs.aws.amazon.com/lambda/latest/dg/runtimes-api.html#runtimes-api-initerror) like it currently does through the `initialize()` function of `LambdaRunner` which wraps the handler’s `init(...)` and handles any errors thrown by reporting it to the dedicated AWS endpoint. + +To retain support for initialization error reporting, the `Lambda.reportStartupError(any Error)` function gives users the option to manually report initialization errors in their closure handler. Although this should ideally happen implicitly like it currently does in v1, we believe this is a small compromise in comparison to the benefits gained in now being able to cleanly manage the lifecycles of required services in a structured concurrency manner. + + +>Use-case: +> +>Assume we want to load a secret for the Lambda function from a secret vault first. +>If this fails, we want to report the error to the control plane: +>```swift +>let secretVault = SecretVault() +> +>do { +> /// !!! Error thrown: secret "foo" does not exist !!! +> let secret = try await secretVault.getSecret("foo") +> +> let runtime = LambdaRuntime { (event: Input, context: LambdaContext) in +> /// Lambda business logic +> } +> +> let serviceGroup = ServiceGroup( +> services: [postgresClient, runtime], +> configuration: .init(gracefulShutdownSignals: [.sigterm]), +> logger: logger +> ) +> try await serviceGroup.run() +>} catch { +> /// Report startup error straight away to the dedicated initialization error endpoint +> try await Lambda.reportStartupError(error) +>} +>``` + +### Codable support + +For v2 we want to introduce a `LambdaCodableAdapter` that: + +1. Accepts any generic underlying handler conforming to a new protocol `LambdaHandler` specifying `associatedtype`s `Event` and `Output`, and a `handle(_ event: Event, context: LambdaContext) async throws -> Output` function. + +#### LambdaHandler + +```swift +public protocol LambdaHandler { + /// Generic input type + /// The body of the request sent to Lambda will be decoded into this type for the handler to consume + associatedtype Event + /// Generic output type + /// This is the return type of the handle() function. + associatedtype Output + + /// The business logic of the Lambda function. Receives a generic input type and returns a generic output type. + /// Agnostic to JSON encoding/decoding + mutating func handle(_ event: Event, context: LambdaContext) async throws -> Output +} +``` + +2. Accepts *any* encoder and decoder object conforming to the `LambdaEventDecoder` and `LambdaOutputEncoder` protocols: + +#### LambdaEventDecoder and LambdaOutputEncoder protocols + +```swift +public protocol LambdaEventDecoder { + /// Decode the ByteBuffer representing the received event into the generic type T + /// the handler will receive + func decode(_ type: T.Type, from buffer: ByteBuffer) throws -> T +} + +public protocol LambdaOutputEncoder { + /// Encode the generic type T the handler has produced into a ByteBuffer + func encode(_ value: T, into buffer: inout ByteBuffer) throws +} +``` + +We provide conformances for Foundation’s `JSONDecoder` to `LambdaEventDecoder` and `JSONEncoder` to `LambdaOutputEncoder`. + +3. Implements its `handle()` method by: + 1. Decoding the `ByteBuffer` event into the generic `Event` type. + 2. Passing the generic `Event` instance to the underlying handler's `handle()` method. + 3. Encoding the generic `Output` returned from the underlying `handle()` into JSON and returning it. + +#### LambdaCodableAdapter + +`LambdaCodableAdapter` can implement encoding/decoding for *any* handler conforming to `LambdaHandler` if `Event` is `Decodable` and the `Output` is `Encodable`, meaning that the encoding/decoding stubs do not need to be implemented by the user. + +```swift +/// Wraps an underlying handler conforming to `CodableLambdaHandler` +/// with encoding/decoding logic +public struct LambdaCodableAdapter< + Handler: LambdaHandler, + Event: Decodable, + Output, + Decoder: LambdaEventDecoder, + Encoder: LambdaOutputEncoder +>: StreamingLambdaHandler where Handler.Output == Output, Handler.Event == Event { + + /// Register the concrete handler, encoder, and decoder. + public init( + handler: Handler, + encoder: Encoder, + decoder: Decoder + ) where Output: Encodable + + /// For handler with a void output -- the user doesn't specify an encoder. + public init( + handler: Handler, + decoder: Decoder + ) where Output == Void, Encoder == VoidEncoder + + /// 1. Decode the invocation event using `self.decoder` + /// 2. Call the underlying `self.handler.handle()` + /// method with the decoded event data. + /// 3. Return the encoded output of (2) with `self.encoder` + public mutating func handle( + _ request: ByteBuffer, + context: LambdaContext + ) async throws -> LambdaResponse +} +``` + +### Handler as a Closure + +To create a Lambda function using the current API, a user first has to create an object and conform it to one of the handler protocols by implementing the initializer and the `handle(...)` function. Now that `LambdaRuntime` is public, this verbosity can very easily be simplified. + +**ClosureHandler** + +This handler is generic over any `Event` type conforming to `Decodable` and any `Output` type conforming to `Encodable` or `Void`. + +```swift +public struct ClosureHandler: LambdaHandler { + /// Intialize with a closure handler over generic Input and Output types + public init(body: @escaping (Event, LambdaContext) async throws -> Output) where Output: Encoda + /// Intialize with a closure handler over a generic Input type (Void Output). + public init(body: @escaping (Event, LambdaContext) async throws -> Void) where Output == Void + /// The business logic of the Lambda function. + public func handle(_ event: Event, context: LambdaContext) async throws -> Output +} +``` + +Given that `ClosureHandler` conforms to `LambdaHandler`: + +1. We can extend the `LambdaRuntime` initializer such that it accepts a closure as an argument. +2. Within the initializer, the closure handler is wrapped with `LambdaCodableAdapter`. + +```swift +extension LambdaRuntime { + /// Initialize a LambdaRuntime with a closure handler over generic Event and Output types. + /// This initializer bolts on encoding/decoding logic by wrapping the closure handler with + /// LambdaCodableAdapter. + public init( + body: @escaping (Event, LambdaContext) async throws -> Output + ) where Handler == LambdaCodableAdapter, Event, Output, JSONDecoder, JSONEncoder> + + /// Same as above but for handlers with a void output + public init( + body: @escaping (Event, LambdaContext) async throws -> Void + ) where Handler == LambdaCodableAdapter, Event, Void, JSONDecoder, VoidEncoder> +} +``` + +We can now significantly reduce the verbosity and leverage Swift’s trailing closure syntax to cleanly create and run a Lambda function, abstracting away the decoding and encoding logic from the user: + +```swift +/// The type the handler will use as input +struct Input: Decodable { + var message: String +} + +/// The type the handler will output +struct Greeting: Encodable { + var echoedMessage: String +} + +/// A simple Lambda function that echoes the input +let runtime = LambdaRuntime { (event: Input, context: LambdaContext) in + Greeting(echoedMessage: event.message) +} + +try await runtime.run() +``` + +We also add a `StreamingClosureHandler` conforming to `StreamingLambdaHandler` for use-cases where the user wants to handle encoding/decoding themselves: + +```swift +public struct StreamingClosureHandler: StreamingLambdaHandler { + + public init( + body: @escaping sending (ByteBuffer, consuming LambdaResponseWriter, LambdaContext) async -> () + ) + + public func handle( + _ request: ByteBuffer, + responseWriter: consuming LambdaResponseWriter, + context: LambdaContext + ) async +} + +extension LambdaRuntime { + public init( + body: @escaping sending (ByteBuffer, consuming LambdaResponseWriter, LambdaContext) async -> () + ) +} +``` + +## Alternatives considered + +### [UInt8] instead of ByteBuffer + +We considered using `[UInt8]` instead of `ByteBuffer` in the base `LambdaHandler` API. We decided to use `ByteBuffer` for two reasons. + +1. 99% of use-cases will use the JSON codable API and will not directly get in touch with ByteBuffer anyway. For those users it does not matter if the base API uses `ByteBuffer` or `[UInt8]`. +2. The incoming and outgoing data must be in the `ByteBuffer` format anyway, as Lambda uses SwiftNIO under the hood and SwiftNIO uses `ByteBuffer` in its APIs. By using `ByteBuffer` we can save a copies to and from `[UInt8]`. This will reduce the invocation time for all users. +3. The base `LambdaHandler` API is most likely mainly being used by developers that want to integrate their web framework with Lambda (examples: Vapor, Hummingbird, ...). Those developers will most likely prefer to get the data in the `ByteBuffer` format anyway, as their lower level networking stack also depends on SwiftNIO. + +### Users create a LambdaResponse, that supports streaming instead of being passed a LambdaResponseWriter + +Instead of passing the `LambdaResponseWriter` in the invocation we considered a new type `LambdaResponse`, that users must return in the `StreamingLambdaHandler`. + +Its API would look like this: + +```swift +/// A response returned from a ``LambdaHandler``. +/// The response can be empty, a single ByteBuffer or a response stream. +public struct LambdaResponse { + /// A writer to be used when creating a streamed response. + public struct Writer: ~Copyable { + /// Writes data to the response stream + public func write(_ byteBuffer: ByteBuffer) async throws + /// Closes off the response stream + public func finish() async throws + /// Writes the `byteBuffer` to the response stream and subsequently closes the stream + public func writeAndFinish(_ byteBuffer: ByteBuffer) async throws + } + + /// Creates an empty lambda response + public init() + + /// Creates a LambdaResponse with a fixed ByteBuffer. + public init(_ byteBuffer: ByteBuffer) + + /// Creates a streamed lambda response. Use the ``Writer`` to send + /// response chunks on the stream. + public init(_ stream: @escaping sending (Writer) async throws -> ()) +} +``` + +The `StreamingLambdaHandler` would look like this: + +```swift +/// The base LambdaHandler protocol +public protocol StreamingLambdaHandler: ~Copyable { + /// The business logic of the Lambda function + /// - Parameters: + /// - event: The invocation's input data + /// - context: The LambdaContext containing the invocation's metadata + /// - Returns: A LambdaResponse, that can be streamed + mutating func handle( + _ event: ByteBuffer, + context: LambdaContext + ) async throws -> LambdaResponse +} +``` + +There are pros and cons for the API that returns the `LambdaResponses` and there are pros and cons for the API that receives a `LambdaResponseWriter` as a parameter. + +Concerning following structured concurrency principles the approach that receives a `LambdaResponseWriter` as a parameter has benefits as the lifetime of the handle function is tied to the invocation runtime. +The approach that returns a `LambdaResponse` splits the invocation into two separate function calls. First the handle method is invoked, second the `LambdaResponse` writer closure is invoked. This means that it is impossible to use Swift APIs that use `with` style lifecycle management patterns from before creating the response until sending the full response stream off. This means for example: That users instrumenting their lambdas with Swift tracing likely can not use the `withSpan` API for the full lifetime of the request, if they return a streamed response. + +However, if it comes to consistency with the larger Swift on server ecosystem, the API that returns a `LambdaResponse` is likely the better choice. Hummingbird v2, OpenAPI and the new Swift gRPC v2 implementation all use this approach. This might be due to the fact that writing middleware becomes easier, if a Response is explicitly returned. + +We decided to implement the approach in which a `LambdaResponseWriter` is passed to the function, since the approach in which a `LambdaResponse` is returned can trivially be built on top of it. This is not true vice versa. + +We welcome the discussion on this topic and are open to change our minds and API here. + +## A word about versioning + +We are aware that AWS Lambda Runtime has not reached a proper 1.0. We intend to keep the current implementation around at 1.0-alpha. We don’t want to change the current API without releasing a new major. We think there are lots of adopters out there that depend on the API in v1. Because of this we intend to release the proposed API here as AWS Lambda Runtime v2. From 250f94b9bda77181df59df79252db0239c806bf7 Mon Sep 17 00:00:00 2001 From: Aryan Shah Date: Thu, 8 Aug 2024 13:47:54 +0100 Subject: [PATCH 02/10] Fix headings and run formatter --- .../Proposals/0001-v2-api.md | 60 +++++++++---------- 1 file changed, 30 insertions(+), 30 deletions(-) diff --git a/Sources/AWSLambdaRuntimeCore/Documentation.docc/Proposals/0001-v2-api.md b/Sources/AWSLambdaRuntimeCore/Documentation.docc/Proposals/0001-v2-api.md index 8a438a2b..80ce2b77 100644 --- a/Sources/AWSLambdaRuntimeCore/Documentation.docc/Proposals/0001-v2-api.md +++ b/Sources/AWSLambdaRuntimeCore/Documentation.docc/Proposals/0001-v2-api.md @@ -1,6 +1,6 @@ # v2 API proposal for swift-aws-lambda-runtime -`swift-aws-lambda-runtime` is an important library for the Swift on Server ecosystem. The initial API was written before async/await was introduced to Swift. When async/await was introduced, shims were added to bridge between the underlying SwiftNIO `EventLoop` interfaces and async/await. However, just like ``gRPC-swift`` and `postgres-nio`, we now want to shift to solely using async/await instead of `EventLoop` interfaces. For this, large parts of the current API have to be reconsidered. +`swift-aws-lambda-runtime` is an important library for the Swift on Server ecosystem. The initial API was written before async/await was introduced to Swift. When async/await was introduced, shims were added to bridge between the underlying SwiftNIO `EventLoop` interfaces and async/await. However, just like `gRPC-swift` and `postgres-nio`, we now want to shift to solely using async/await instead of `EventLoop` interfaces. For this, large parts of the current API have to be reconsidered. ## Motivation @@ -18,13 +18,13 @@ A Lambda function can currently be implemented through conformance to the variou The `SimpleLambdaHandler` protocol provides a quick and easy way to implement a basic Lambda function. It only requires an implementation of the `handle` function where the business logic of the Lambda function can be written. `SimpleLambdaHandler` is perfectly sufficient for small use-cases as the user does not need to spend much time looking into the library. -However, `SimpleLambdaHandler` cannot be used when services such as a database client need to be initalized before the Lambda runtime starts and then also gracefully shutdown prior to the runtime terminating. This is because the only way to register termination logic is through the `LambdaInitializationContext` (containing a field `terminator: LambdaTerminator`) which is created and used *internally* within `LambdaRuntime` and never exposed through `SimpleLambdaHandler`. For such use-cases, other handler protocols like `LambdaHandler` must be used. `LambdaHandler` exposes a `context` argument of type `LambdaInitializationContext` through its initializer. Within the initializer, required services can be initalized and their graceful shutdown logic can be registered with the `context.terminator.register` function. +However, `SimpleLambdaHandler` cannot be used when services such as a database client need to be initalized before the Lambda runtime starts and then also gracefully shutdown prior to the runtime terminating. This is because the only way to register termination logic is through the `LambdaInitializationContext` (containing a field `terminator: LambdaTerminator`) which is created and used _internally_ within `LambdaRuntime` and never exposed through `SimpleLambdaHandler`. For such use-cases, other handler protocols like `LambdaHandler` must be used. `LambdaHandler` exposes a `context` argument of type `LambdaInitializationContext` through its initializer. Within the initializer, required services can be initalized and their graceful shutdown logic can be registered with the `context.terminator.register` function. Yet, `LambdaHandler` is quite cumbersome to use in such use-cases as users have to deviate from the established norms of the Swift on Server ecosystem in order to cleanly manage the lifecycle of the services intended to be used. This is because the convenient `swift-service-lifecycle` v2 library — which is commonly used for cleanly managing the lifecycles of required services and widely supported by many libraries — cannot be used in a structured concurrency manner. #### Does not integrate well with swift-service-lifecycle in a structured concurrency manner -The Lambda runtime can only be started using the **internal** `Lambda.run()` function. This function is called by the `main()` function defined by the `LambdaHandler` protocol, preventing users from injecting initialized services into the runtime *prior* to it starting. As shown below, this forces users to use an **unstructured concurrency** approach and manually initialize services, leading to the issue of the user then perhaps forgetting to gracefully shutdown the initalized services: +The Lambda runtime can only be started using the **internal** `Lambda.run()` function. This function is called by the `main()` function defined by the `LambdaHandler` protocol, preventing users from injecting initialized services into the runtime _prior_ to it starting. As shown below, this forces users to use an **unstructured concurrency** approach and manually initialize services, leading to the issue of the user then perhaps forgetting to gracefully shutdown the initalized services: ```swift struct MyLambda: LambdaHandler { @@ -63,7 +63,7 @@ struct MyLambda: LambdaHandler { #### Verbose Codable support -In the current API, there are extensions and Codable wrapper classes for decoding events and encoding computed responses for *each* different handler protocol and for both `String` and `JSON` formats. This has resulted in a lot of boilerplate code which can very easily be made generic and simplified in v2. +In the current API, there are extensions and Codable wrapper classes for decoding events and encoding computed responses for _each_ different handler protocol and for both `String` and `JSON` formats. This has resulted in a lot of boilerplate code which can very easily be made generic and simplified in v2. ### New features @@ -77,18 +77,18 @@ In May [AWS described in a blog post that you can run background tasks in Lambda ## Proposed Solution -#### async/await-first API +### async/await-first API Large parts of `Lambda`, `LambdaHandler`, and `LambdaRuntime` will be re-written to use async/await constructs in place of the `EventLoop` family of interfaces. -#### Providing ownership of main() and support for swift-service-lifecycle +### Providing ownership of main() and support for swift-service-lifecycle -* Instead of conforming to a handler protocol, users can now create a `LambdaRuntime` by passing in a handler closure. -* `LambdaRuntime` conforms to `ServiceLifecycle.Service` by implementing a `run()` method that contains initialization and graceful shutdown logic. -* This allows the lifecycle of the `LambdaRuntime` to be managed with `swift-service-lifecycle` *alongside* and in the same way the lifecycles of the required services are managed, e.g. `try await ServiceGroup(services: [postgresClient, ..., lambdaRuntime], ...).run()`. -* Dependencies can now be injected into `LambdaRuntime` — `swift-service-lifecycle` guarantees that the services will be initialized *before* the `LambdaRuntime`’s `run()` function is called. -* The required services can then be used within the handler in a structured concurrency manner. `swift-service-lifecycle` takes care of listening for termination signals and terminating the services as well as the `LambdaRuntime` in correct order. -* `LambdaTerminator` can now be eliminated because its role is replaced with `swift-service-lifecycle`. The termination logic of the Lambda function will be implemented in the conforming `run()` function of `LambdaRuntime`. +- Instead of conforming to a handler protocol, users can now create a `LambdaRuntime` by passing in a handler closure. +- `LambdaRuntime` conforms to `ServiceLifecycle.Service` by implementing a `run()` method that contains initialization and graceful shutdown logic. +- This allows the lifecycle of the `LambdaRuntime` to be managed with `swift-service-lifecycle` _alongside_ and in the same way the lifecycles of the required services are managed, e.g. `try await ServiceGroup(services: [postgresClient, ..., lambdaRuntime], ...).run()`. +- Dependencies can now be injected into `LambdaRuntime` — `swift-service-lifecycle` guarantees that the services will be initialized _before_ the `LambdaRuntime`’s `run()` function is called. +- The required services can then be used within the handler in a structured concurrency manner. `swift-service-lifecycle` takes care of listening for termination signals and terminating the services as well as the `LambdaRuntime` in correct order. +- `LambdaTerminator` can now be eliminated because its role is replaced with `swift-service-lifecycle`. The termination logic of the Lambda function will be implemented in the conforming `run()` function of `LambdaRuntime`. With this, the earlier code snippet can be replaced with something much easier to read, maintain, and debug: @@ -112,13 +112,13 @@ let serviceGroup = ServiceGroup( try await serviceGroup.run() ``` -#### Simplifying Codable support +### Simplifying Codable support A detailed explanation is provided in the Codable Support section. In short, much of the boilerplate code defined for each handler protocol in `Lambda+Codable` and `Lambda+String` will be replaced with a single `LambdaCodableAdapter` class. This adapter class is generic over (1) any handler conforming to `LambdaHandler`, (2) the user-specified input and output types, and (3) any decoder and encoder conforming to `LambdaEventDecoder` and `LambdaOutputDecoder`. The adapter will wrap the underlying handler with encoding/decoding logic. -#### Simplifying the handler protocols +### Simplifying the handler protocols There are four different handler protocols in the current API (`SimpleLambdaHandler`, `LambdaHandler`, `EventLoopLambdaHandler`, `ByteBufferLambdaHandler`). As noted in the **Current Limitations** section, the ease-of-use varies for each handler protocol and users may not be able to easily determine which protocol best serves their use-case without spending time digging into the library. To reduce this problem and provide users with clear-cut options, we propose replacing all of the existing handler protocols with just two: `LambdaHandler` and `StreamingLambdaHandler`. Both will be explained in the **Detailed Solution** section. @@ -262,7 +262,7 @@ public final class LambdaRuntime: ServiceLifecycle.Service, Sendable } ``` -The current API allows for a Lambda function to be tested locally through a mock server by requiring an environment variable named `LOCAL_LAMBDA_SERVER_ENABLED` to be set to `true`. If this environment variable is not set, the program immediately crashes as the user will not have the `AWS_LAMBDA_RUNTIME_API` environment variable on their local machine (set automatically when deployed to AWS Lambda). However, making the user set the `LOCAL_LAMBDA_SERVER_ENABLED` environment variable is an unnecessary step that can be avoided. In the v2 API, the `run()` function will automatically start the mock server when the `AWS_LAMBDA_RUNTIME_API` environment variable cannot be found. +The current API allows for a Lambda function to be tested locally through a mock server by requiring an environment variable named `LOCAL_LAMBDA_SERVER_ENABLED` to be set to `true`. If this environment variable is not set, the program immediately crashes as the user will not have the `AWS_LAMBDA_RUNTIME_API` environment variable on their local machine (set automatically when deployed to AWS Lambda). However, making the user set the `LOCAL_LAMBDA_SERVER_ENABLED` environment variable is an unnecessary step that can be avoided. In the v2 API, the `run()` function will automatically start the mock server when the `AWS_LAMBDA_RUNTIME_API` environment variable cannot be found. ### Lambda @@ -283,15 +283,15 @@ Since the library now provides ownership of the `main()` function and allows use To retain support for initialization error reporting, the `Lambda.reportStartupError(any Error)` function gives users the option to manually report initialization errors in their closure handler. Although this should ideally happen implicitly like it currently does in v1, we believe this is a small compromise in comparison to the benefits gained in now being able to cleanly manage the lifecycles of required services in a structured concurrency manner. - ->Use-case: +> Use-case: +> +> Assume we want to load a secret for the Lambda function from a secret vault first. +> If this fails, we want to report the error to the control plane: > ->Assume we want to load a secret for the Lambda function from a secret vault first. ->If this fails, we want to report the error to the control plane: ->```swift ->let secretVault = SecretVault() +> ```swift +> let secretVault = SecretVault() > ->do { +> do { > /// !!! Error thrown: secret "foo" does not exist !!! > let secret = try await secretVault.getSecret("foo") > @@ -305,11 +305,11 @@ To retain support for initialization error reporting, the `Lambda.reportStartupE > logger: logger > ) > try await serviceGroup.run() ->} catch { +> } catch { > /// Report startup error straight away to the dedicated initialization error endpoint > try await Lambda.reportStartupError(error) ->} ->``` +> } +> ``` ### Codable support @@ -334,7 +334,7 @@ public protocol LambdaHandler { } ``` -2. Accepts *any* encoder and decoder object conforming to the `LambdaEventDecoder` and `LambdaOutputEncoder` protocols: +2. Accepts _any_ encoder and decoder object conforming to the `LambdaEventDecoder` and `LambdaOutputEncoder` protocols: #### LambdaEventDecoder and LambdaOutputEncoder protocols @@ -354,13 +354,13 @@ public protocol LambdaOutputEncoder { We provide conformances for Foundation’s `JSONDecoder` to `LambdaEventDecoder` and `JSONEncoder` to `LambdaOutputEncoder`. 3. Implements its `handle()` method by: - 1. Decoding the `ByteBuffer` event into the generic `Event` type. - 2. Passing the generic `Event` instance to the underlying handler's `handle()` method. - 3. Encoding the generic `Output` returned from the underlying `handle()` into JSON and returning it. + 1. Decoding the `ByteBuffer` event into the generic `Event` type. + 2. Passing the generic `Event` instance to the underlying handler's `handle()` method. + 3. Encoding the generic `Output` returned from the underlying `handle()` into JSON and returning it. #### LambdaCodableAdapter -`LambdaCodableAdapter` can implement encoding/decoding for *any* handler conforming to `LambdaHandler` if `Event` is `Decodable` and the `Output` is `Encodable`, meaning that the encoding/decoding stubs do not need to be implemented by the user. +`LambdaCodableAdapter` can implement encoding/decoding for _any_ handler conforming to `LambdaHandler` if `Event` is `Decodable` and the `Output` is `Encodable`, meaning that the encoding/decoding stubs do not need to be implemented by the user. ```swift /// Wraps an underlying handler conforming to `CodableLambdaHandler` From ea76068053e2b3d1c927630d07f454916c425640 Mon Sep 17 00:00:00 2001 From: Aryan Shah Date: Thu, 8 Aug 2024 13:49:01 +0100 Subject: [PATCH 03/10] Add line breaks --- .../Proposals/0001-v2-api.md | 211 +++++++++++++----- 1 file changed, 159 insertions(+), 52 deletions(-) diff --git a/Sources/AWSLambdaRuntimeCore/Documentation.docc/Proposals/0001-v2-api.md b/Sources/AWSLambdaRuntimeCore/Documentation.docc/Proposals/0001-v2-api.md index 80ce2b77..7afdb484 100644 --- a/Sources/AWSLambdaRuntimeCore/Documentation.docc/Proposals/0001-v2-api.md +++ b/Sources/AWSLambdaRuntimeCore/Documentation.docc/Proposals/0001-v2-api.md @@ -1,6 +1,10 @@ # v2 API proposal for swift-aws-lambda-runtime -`swift-aws-lambda-runtime` is an important library for the Swift on Server ecosystem. The initial API was written before async/await was introduced to Swift. When async/await was introduced, shims were added to bridge between the underlying SwiftNIO `EventLoop` interfaces and async/await. However, just like `gRPC-swift` and `postgres-nio`, we now want to shift to solely using async/await instead of `EventLoop` interfaces. For this, large parts of the current API have to be reconsidered. +`swift-aws-lambda-runtime` is an important library for the Swift on Server ecosystem. The initial API was written before +async/await was introduced to Swift. When async/await was introduced, shims were added to bridge between the underlying +SwiftNIO `EventLoop` interfaces and async/await. However, just like `gRPC-swift` and `postgres-nio`, we now want to +shift to solely using async/await instead of `EventLoop` interfaces. For this, large parts of the current API have to be +reconsidered. ## Motivation @@ -8,23 +12,52 @@ #### EventLoop interfaces -The current API extensively uses the `EventLoop` family of interfaces from SwiftNIO in many areas. To use these interfaces correctly though, it requires developers to exercise great care and be aware of certain details such as never running blocking code on the same `EventLoop` the library uses. Developers also need to understand the various transform methods that are used to work with `EventLoop`s and `EventLoopFuture`s. This results in a lot of cognitive complexity and makes the code in the current API hard to reason about and maintain. For these reasons, the overarching trend in the Swift on Server ecosystem is to shift to newer, more readable, Swift concurrency constructs and de-couple from SwiftNIO’s `EventLoop` interfaces. +The current API extensively uses the `EventLoop` family of interfaces from SwiftNIO in many areas. To use these +interfaces correctly though, it requires developers to exercise great care and be aware of certain details such as never +running blocking code on the same `EventLoop` the library uses. Developers also need to understand the various transform +methods that are used to work with `EventLoop`s and `EventLoopFuture`s. This results in a lot of cognitive complexity +and makes the code in the current API hard to reason about and maintain. For these reasons, the overarching trend in the +Swift on Server ecosystem is to shift to newer, more readable, Swift concurrency constructs and de-couple from +SwiftNIO’s `EventLoop` interfaces. #### No ownership of the main() function -A Lambda function can currently be implemented through conformance to the various handler protocols defined in `AWSLambdaRuntimeCore/LambdaHandler`. Each of these protocols have an extension which implements a `static func main()`. This allows users to annotate their `LambdaHandler` conforming object with `@main`. The `static func main()` calls the internal `Lambda.run()` function, which starts the Lambda function. Since the `Lambda.run()` method is internal, users cannot override the default implementation. This has proven challenging for users who want to [set up global properties before the Lambda starts-up](https://github.com/swift-server/swift-aws-lambda-runtime/issues/265). Setting up global properties is required to customize the Swift Logging, Metric and Tracing backend. +A Lambda function can currently be implemented through conformance to the various handler protocols defined in +`AWSLambdaRuntimeCore/LambdaHandler`. Each of these protocols have an extension which implements a `static func main()`. +This allows users to annotate their `LambdaHandler` conforming object with `@main`. The `static func main()` calls the +internal `Lambda.run()` function, which starts the Lambda function. Since the `Lambda.run()` method is internal, users +cannot override the default implementation. This has proven challenging for users who want to +[set up global properties before the Lambda starts-up](https://github.com/swift-server/swift-aws-lambda-runtime/issues/265). +Setting up global properties is required to customize the Swift Logging, Metric and Tracing backend. #### Non-trivial transition from SimpleLambdaHandler to LambdaHandler -The `SimpleLambdaHandler` protocol provides a quick and easy way to implement a basic Lambda function. It only requires an implementation of the `handle` function where the business logic of the Lambda function can be written. `SimpleLambdaHandler` is perfectly sufficient for small use-cases as the user does not need to spend much time looking into the library. - -However, `SimpleLambdaHandler` cannot be used when services such as a database client need to be initalized before the Lambda runtime starts and then also gracefully shutdown prior to the runtime terminating. This is because the only way to register termination logic is through the `LambdaInitializationContext` (containing a field `terminator: LambdaTerminator`) which is created and used _internally_ within `LambdaRuntime` and never exposed through `SimpleLambdaHandler`. For such use-cases, other handler protocols like `LambdaHandler` must be used. `LambdaHandler` exposes a `context` argument of type `LambdaInitializationContext` through its initializer. Within the initializer, required services can be initalized and their graceful shutdown logic can be registered with the `context.terminator.register` function. - -Yet, `LambdaHandler` is quite cumbersome to use in such use-cases as users have to deviate from the established norms of the Swift on Server ecosystem in order to cleanly manage the lifecycle of the services intended to be used. This is because the convenient `swift-service-lifecycle` v2 library — which is commonly used for cleanly managing the lifecycles of required services and widely supported by many libraries — cannot be used in a structured concurrency manner. +The `SimpleLambdaHandler` protocol provides a quick and easy way to implement a basic Lambda function. It only requires +an implementation of the `handle` function where the business logic of the Lambda function can be written. +`SimpleLambdaHandler` is perfectly sufficient for small use-cases as the user does not need to spend much time looking +into the library. + +However, `SimpleLambdaHandler` cannot be used when services such as a database client need to be initalized before the +Lambda runtime starts and then also gracefully shutdown prior to the runtime terminating. This is because the only way +to register termination logic is through the `LambdaInitializationContext` (containing a field +`terminator: LambdaTerminator`) which is created and used _internally_ within `LambdaRuntime` and never exposed through +`SimpleLambdaHandler`. For such use-cases, other handler protocols like `LambdaHandler` must be used. `LambdaHandler` +exposes a `context` argument of type `LambdaInitializationContext` through its initializer. Within the initializer, +required services can be initalized and their graceful shutdown logic can be registered with the +`context.terminator.register` function. + +Yet, `LambdaHandler` is quite cumbersome to use in such use-cases as users have to deviate from the established norms of +the Swift on Server ecosystem in order to cleanly manage the lifecycle of the services intended to be used. This is +because the convenient `swift-service-lifecycle` v2 library — which is commonly used for cleanly managing the lifecycles +of required services and widely supported by many libraries — cannot be used in a structured concurrency manner. #### Does not integrate well with swift-service-lifecycle in a structured concurrency manner -The Lambda runtime can only be started using the **internal** `Lambda.run()` function. This function is called by the `main()` function defined by the `LambdaHandler` protocol, preventing users from injecting initialized services into the runtime _prior_ to it starting. As shown below, this forces users to use an **unstructured concurrency** approach and manually initialize services, leading to the issue of the user then perhaps forgetting to gracefully shutdown the initalized services: +The Lambda runtime can only be started using the **internal** `Lambda.run()` function. This function is called by the +`main()` function defined by the `LambdaHandler` protocol, preventing users from injecting initialized services into the +runtime _prior_ to it starting. As shown below, this forces users to use an **unstructured concurrency** approach and +manually initialize services, leading to the issue of the user then perhaps forgetting to gracefully shutdown the +initalized services: ```swift struct MyLambda: LambdaHandler { @@ -63,32 +96,47 @@ struct MyLambda: LambdaHandler { #### Verbose Codable support -In the current API, there are extensions and Codable wrapper classes for decoding events and encoding computed responses for _each_ different handler protocol and for both `String` and `JSON` formats. This has resulted in a lot of boilerplate code which can very easily be made generic and simplified in v2. +In the current API, there are extensions and Codable wrapper classes for decoding events and encoding computed responses +for _each_ different handler protocol and for both `String` and `JSON` formats. This has resulted in a lot of +boilerplate code which can very easily be made generic and simplified in v2. ### New features #### Support response streaming -In April 2023 [AWS introduced support for response streaming](https://aws.amazon.com/blogs/compute/introducing-aws-lambda-response-streaming/) in Lambda. The current API does not support streaming. For v2 we want to change this. +In April 2023 +[AWS introduced support for response streaming](https://aws.amazon.com/blogs/compute/introducing-aws-lambda-response-streaming/) +in Lambda. The current API does not support streaming. For v2 we want to change this. #### Scheduling background work -In May [AWS described in a blog post that you can run background tasks in Lambda](https://aws.amazon.com/blogs/compute/running-code-after-returning-a-response-from-an-aws-lambda-function/) until the runtime asks for more work from the control plane. We want to support this by adding new API that allows background processing, even after the response has been returned. +In May +[AWS described in a blog post that you can run background tasks in Lambda](https://aws.amazon.com/blogs/compute/running-code-after-returning-a-response-from-an-aws-lambda-function/) +until the runtime asks for more work from the control plane. We want to support this by adding new API that allows +background processing, even after the response has been returned. ## Proposed Solution ### async/await-first API -Large parts of `Lambda`, `LambdaHandler`, and `LambdaRuntime` will be re-written to use async/await constructs in place of the `EventLoop` family of interfaces. +Large parts of `Lambda`, `LambdaHandler`, and `LambdaRuntime` will be re-written to use async/await constructs in place +of the `EventLoop` family of interfaces. ### Providing ownership of main() and support for swift-service-lifecycle - Instead of conforming to a handler protocol, users can now create a `LambdaRuntime` by passing in a handler closure. -- `LambdaRuntime` conforms to `ServiceLifecycle.Service` by implementing a `run()` method that contains initialization and graceful shutdown logic. -- This allows the lifecycle of the `LambdaRuntime` to be managed with `swift-service-lifecycle` _alongside_ and in the same way the lifecycles of the required services are managed, e.g. `try await ServiceGroup(services: [postgresClient, ..., lambdaRuntime], ...).run()`. -- Dependencies can now be injected into `LambdaRuntime` — `swift-service-lifecycle` guarantees that the services will be initialized _before_ the `LambdaRuntime`’s `run()` function is called. -- The required services can then be used within the handler in a structured concurrency manner. `swift-service-lifecycle` takes care of listening for termination signals and terminating the services as well as the `LambdaRuntime` in correct order. -- `LambdaTerminator` can now be eliminated because its role is replaced with `swift-service-lifecycle`. The termination logic of the Lambda function will be implemented in the conforming `run()` function of `LambdaRuntime`. +- `LambdaRuntime` conforms to `ServiceLifecycle.Service` by implementing a `run()` method that contains initialization + and graceful shutdown logic. +- This allows the lifecycle of the `LambdaRuntime` to be managed with `swift-service-lifecycle` _alongside_ and in the + same way the lifecycles of the required services are managed, e.g. + `try await ServiceGroup(services: [postgresClient, ..., lambdaRuntime], ...).run()`. +- Dependencies can now be injected into `LambdaRuntime` — `swift-service-lifecycle` guarantees that the services will be + initialized _before_ the `LambdaRuntime`’s `run()` function is called. +- The required services can then be used within the handler in a structured concurrency manner. + `swift-service-lifecycle` takes care of listening for termination signals and terminating the services as well as the + `LambdaRuntime` in correct order. +- `LambdaTerminator` can now be eliminated because its role is replaced with `swift-service-lifecycle`. The termination + logic of the Lambda function will be implemented in the conforming `run()` function of `LambdaRuntime`. With this, the earlier code snippet can be replaced with something much easier to read, maintain, and debug: @@ -114,13 +162,22 @@ try await serviceGroup.run() ### Simplifying Codable support -A detailed explanation is provided in the Codable Support section. In short, much of the boilerplate code defined for each handler protocol in `Lambda+Codable` and `Lambda+String` will be replaced with a single `LambdaCodableAdapter` class. +A detailed explanation is provided in the Codable Support section. In short, much of the boilerplate code defined for +each handler protocol in `Lambda+Codable` and `Lambda+String` will be replaced with a single `LambdaCodableAdapter` +class. -This adapter class is generic over (1) any handler conforming to `LambdaHandler`, (2) the user-specified input and output types, and (3) any decoder and encoder conforming to `LambdaEventDecoder` and `LambdaOutputDecoder`. The adapter will wrap the underlying handler with encoding/decoding logic. +This adapter class is generic over (1) any handler conforming to `LambdaHandler`, (2) the user-specified input and +output types, and (3) any decoder and encoder conforming to `LambdaEventDecoder` and `LambdaOutputDecoder`. The adapter +will wrap the underlying handler with encoding/decoding logic. ### Simplifying the handler protocols -There are four different handler protocols in the current API (`SimpleLambdaHandler`, `LambdaHandler`, `EventLoopLambdaHandler`, `ByteBufferLambdaHandler`). As noted in the **Current Limitations** section, the ease-of-use varies for each handler protocol and users may not be able to easily determine which protocol best serves their use-case without spending time digging into the library. To reduce this problem and provide users with clear-cut options, we propose replacing all of the existing handler protocols with just two: `LambdaHandler` and `StreamingLambdaHandler`. Both will be explained in the **Detailed Solution** section. +There are four different handler protocols in the current API (`SimpleLambdaHandler`, `LambdaHandler`, +`EventLoopLambdaHandler`, `ByteBufferLambdaHandler`). As noted in the **Current Limitations** section, the ease-of-use +varies for each handler protocol and users may not be able to easily determine which protocol best serves their use-case +without spending time digging into the library. To reduce this problem and provide users with clear-cut options, we +propose replacing all of the existing handler protocols with just two: `LambdaHandler` and `StreamingLambdaHandler`. +Both will be explained in the **Detailed Solution** section. ## Detailed Solution @@ -128,7 +185,8 @@ Below you’ll find explanation’s for all types that we want to use in AWS Lam ### LambdaResponseWriter -We will introduce a new `LambdaResponseWriter` protocol. It is used in the new `StreamingLambdaHandler` (defined below), which is the new base protocol for the `LambdaRuntime` (defined below as well). +We will introduce a new `LambdaResponseWriter` protocol. It is used in the new `StreamingLambdaHandler` (defined below), +which is the new base protocol for the `LambdaRuntime` (defined below as well). ```swift /// A writer object to write the Lambda response stream into @@ -146,9 +204,11 @@ public protocol LambdaResponseWriter: ~Copyable { ### LambdaContext -`LambdaContext` will be largely unchanged, but the `eventLoop` property as well as the `allocator` property (of type `ByteBuffer`) will be removed. +`LambdaContext` will be largely unchanged, but the `eventLoop` property as well as the `allocator` property (of type +`ByteBuffer`) will be removed. -A new function `backgroundTask()` will also be added. This will allow tasks to be run in the background while and after the response is/has been sent. Please note that `LambdaContext` will not be Sendable anymore. +A new function `backgroundTask()` will also be added. This will allow tasks to be run in the background while and after +the response is/has been sent. Please note that `LambdaContext` will not be Sendable anymore. ```swift /// A context object passed as part of an invocation in LambdaHandler handle functions. @@ -186,7 +246,8 @@ public struct LambdaContext { ### StreamingLambdaHandler -The new `StreamingLambdaHandler` protocol is the base protocol to implement a Lambda function. Most users will not use this protocol and instead use the `LambdaHandler` protocol defined in the `Codable` Support section. +The new `StreamingLambdaHandler` protocol is the base protocol to implement a Lambda function. Most users will not use +this protocol and instead use the `LambdaHandler` protocol defined in the `Codable` Support section. ```swift /// The base LambdaHandler protocol @@ -202,7 +263,8 @@ public protocol StreamingLambdaHandler: ~Copyable { } ``` -The protocol is defined in a way that supports a broad range of use-cases. The handle method is marked as `mutating` to allow handlers to be implemented with a `struct`. Users can opt to even mark the handler as non-copyable. +The protocol is defined in a way that supports a broad range of use-cases. The handle method is marked as `mutating` to +allow handlers to be implemented with a `struct`. Users can opt to even mark the handler as non-copyable. An implementation that sends the number 1 to 10 every 500ms could look like this: @@ -222,11 +284,15 @@ struct SendNumbersWithPause: StreamingLambdaHandler { } ``` -The method is not marked as `throws` as we want the users of this API to handle errors themselves and call `LambdaResponseWriter.reportError(_:)` if needed. +The method is not marked as `throws` as we want the users of this API to handle errors themselves and call +`LambdaResponseWriter.reportError(_:)` if needed. ### LambdaRuntime -`LambdaRuntime` is the class that communicates with the Lambda control plane as defined in [Building a custom runtime for AWS Lambda](https://docs.aws.amazon.com/lambda/latest/dg/runtimes-custom.html) and forward the invocations to the provided `StreamingLambdaHandler`. It will conform to `ServiceLifecycle.Service` to provide support for `swift-service-lifecycle`. +`LambdaRuntime` is the class that communicates with the Lambda control plane as defined in +[Building a custom runtime for AWS Lambda](https://docs.aws.amazon.com/lambda/latest/dg/runtimes-custom.html) and +forward the invocations to the provided `StreamingLambdaHandler`. It will conform to `ServiceLifecycle.Service` to +provide support for `swift-service-lifecycle`. ```swift /// The LambdaRuntime object. This object communicates with the Lambda control plane @@ -262,11 +328,17 @@ public final class LambdaRuntime: ServiceLifecycle.Service, Sendable } ``` -The current API allows for a Lambda function to be tested locally through a mock server by requiring an environment variable named `LOCAL_LAMBDA_SERVER_ENABLED` to be set to `true`. If this environment variable is not set, the program immediately crashes as the user will not have the `AWS_LAMBDA_RUNTIME_API` environment variable on their local machine (set automatically when deployed to AWS Lambda). However, making the user set the `LOCAL_LAMBDA_SERVER_ENABLED` environment variable is an unnecessary step that can be avoided. In the v2 API, the `run()` function will automatically start the mock server when the `AWS_LAMBDA_RUNTIME_API` environment variable cannot be found. +The current API allows for a Lambda function to be tested locally through a mock server by requiring an environment +variable named `LOCAL_LAMBDA_SERVER_ENABLED` to be set to `true`. If this environment variable is not set, the program +immediately crashes as the user will not have the `AWS_LAMBDA_RUNTIME_API` environment variable on their local machine +(set automatically when deployed to AWS Lambda). However, making the user set the `LOCAL_LAMBDA_SERVER_ENABLED` +environment variable is an unnecessary step that can be avoided. In the v2 API, the `run()` function will automatically +start the mock server when the `AWS_LAMBDA_RUNTIME_API` environment variable cannot be found. ### Lambda -We also add an enum to store a static function and a property on. We put this on the static `Lambda` because `LambdaRuntime` is generic and thus has bad ergonomics for static properties and functions. +We also add an enum to store a static function and a property on. We put this on the static `Lambda` because +`LambdaRuntime` is generic and thus has bad ergonomics for static properties and functions. ```swift enum Lambda { @@ -279,14 +351,21 @@ enum Lambda { } ``` -Since the library now provides ownership of the `main()` function and allows users to initialize services before the `LambdaRuntime` is initialized, the library cannot implicitly report [errors that occur during initialization to the dedicated endpoint AWS exposes](https://docs.aws.amazon.com/lambda/latest/dg/runtimes-api.html#runtimes-api-initerror) like it currently does through the `initialize()` function of `LambdaRunner` which wraps the handler’s `init(...)` and handles any errors thrown by reporting it to the dedicated AWS endpoint. +Since the library now provides ownership of the `main()` function and allows users to initialize services before the +`LambdaRuntime` is initialized, the library cannot implicitly report +[errors that occur during initialization to the dedicated endpoint AWS exposes](https://docs.aws.amazon.com/lambda/latest/dg/runtimes-api.html#runtimes-api-initerror) +like it currently does through the `initialize()` function of `LambdaRunner` which wraps the handler’s `init(...)` and +handles any errors thrown by reporting it to the dedicated AWS endpoint. -To retain support for initialization error reporting, the `Lambda.reportStartupError(any Error)` function gives users the option to manually report initialization errors in their closure handler. Although this should ideally happen implicitly like it currently does in v1, we believe this is a small compromise in comparison to the benefits gained in now being able to cleanly manage the lifecycles of required services in a structured concurrency manner. +To retain support for initialization error reporting, the `Lambda.reportStartupError(any Error)` function gives users +the option to manually report initialization errors in their closure handler. Although this should ideally happen +implicitly like it currently does in v1, we believe this is a small compromise in comparison to the benefits gained in +now being able to cleanly manage the lifecycles of required services in a structured concurrency manner. > Use-case: > -> Assume we want to load a secret for the Lambda function from a secret vault first. -> If this fails, we want to report the error to the control plane: +> Assume we want to load a secret for the Lambda function from a secret vault first. If this fails, we want to report +> the error to the control plane: > > ```swift > let secretVault = SecretVault() @@ -315,7 +394,8 @@ To retain support for initialization error reporting, the `Lambda.reportStartupE For v2 we want to introduce a `LambdaCodableAdapter` that: -1. Accepts any generic underlying handler conforming to a new protocol `LambdaHandler` specifying `associatedtype`s `Event` and `Output`, and a `handle(_ event: Event, context: LambdaContext) async throws -> Output` function. +1. Accepts any generic underlying handler conforming to a new protocol `LambdaHandler` specifying `associatedtype`s + `Event` and `Output`, and a `handle(_ event: Event, context: LambdaContext) async throws -> Output` function. #### LambdaHandler @@ -351,7 +431,8 @@ public protocol LambdaOutputEncoder { } ``` -We provide conformances for Foundation’s `JSONDecoder` to `LambdaEventDecoder` and `JSONEncoder` to `LambdaOutputEncoder`. +We provide conformances for Foundation’s `JSONDecoder` to `LambdaEventDecoder` and `JSONEncoder` to +`LambdaOutputEncoder`. 3. Implements its `handle()` method by: 1. Decoding the `ByteBuffer` event into the generic `Event` type. @@ -360,7 +441,9 @@ We provide conformances for Foundation’s `JSONDecoder` to `LambdaEventDecoder` #### LambdaCodableAdapter -`LambdaCodableAdapter` can implement encoding/decoding for _any_ handler conforming to `LambdaHandler` if `Event` is `Decodable` and the `Output` is `Encodable`, meaning that the encoding/decoding stubs do not need to be implemented by the user. +`LambdaCodableAdapter` can implement encoding/decoding for _any_ handler conforming to `LambdaHandler` if `Event` is +`Decodable` and the `Output` is `Encodable`, meaning that the encoding/decoding stubs do not need to be implemented by +the user. ```swift /// Wraps an underlying handler conforming to `CodableLambdaHandler` @@ -399,11 +482,14 @@ public struct LambdaCodableAdapter< ### Handler as a Closure -To create a Lambda function using the current API, a user first has to create an object and conform it to one of the handler protocols by implementing the initializer and the `handle(...)` function. Now that `LambdaRuntime` is public, this verbosity can very easily be simplified. +To create a Lambda function using the current API, a user first has to create an object and conform it to one of the +handler protocols by implementing the initializer and the `handle(...)` function. Now that `LambdaRuntime` is public, +this verbosity can very easily be simplified. **ClosureHandler** -This handler is generic over any `Event` type conforming to `Decodable` and any `Output` type conforming to `Encodable` or `Void`. +This handler is generic over any `Event` type conforming to `Decodable` and any `Output` type conforming to `Encodable` +or `Void`. ```swift public struct ClosureHandler: LambdaHandler { @@ -437,7 +523,8 @@ extension LambdaRuntime { } ``` -We can now significantly reduce the verbosity and leverage Swift’s trailing closure syntax to cleanly create and run a Lambda function, abstracting away the decoding and encoding logic from the user: +We can now significantly reduce the verbosity and leverage Swift’s trailing closure syntax to cleanly create and run a +Lambda function, abstracting away the decoding and encoding logic from the user: ```swift /// The type the handler will use as input @@ -458,7 +545,8 @@ let runtime = LambdaRuntime { (event: Input, context: LambdaContext) in try await runtime.run() ``` -We also add a `StreamingClosureHandler` conforming to `StreamingLambdaHandler` for use-cases where the user wants to handle encoding/decoding themselves: +We also add a `StreamingClosureHandler` conforming to `StreamingLambdaHandler` for use-cases where the user wants to +handle encoding/decoding themselves: ```swift public struct StreamingClosureHandler: StreamingLambdaHandler { @@ -485,15 +573,22 @@ extension LambdaRuntime { ### [UInt8] instead of ByteBuffer -We considered using `[UInt8]` instead of `ByteBuffer` in the base `LambdaHandler` API. We decided to use `ByteBuffer` for two reasons. +We considered using `[UInt8]` instead of `ByteBuffer` in the base `LambdaHandler` API. We decided to use `ByteBuffer` +for two reasons. -1. 99% of use-cases will use the JSON codable API and will not directly get in touch with ByteBuffer anyway. For those users it does not matter if the base API uses `ByteBuffer` or `[UInt8]`. -2. The incoming and outgoing data must be in the `ByteBuffer` format anyway, as Lambda uses SwiftNIO under the hood and SwiftNIO uses `ByteBuffer` in its APIs. By using `ByteBuffer` we can save a copies to and from `[UInt8]`. This will reduce the invocation time for all users. -3. The base `LambdaHandler` API is most likely mainly being used by developers that want to integrate their web framework with Lambda (examples: Vapor, Hummingbird, ...). Those developers will most likely prefer to get the data in the `ByteBuffer` format anyway, as their lower level networking stack also depends on SwiftNIO. +1. 99% of use-cases will use the JSON codable API and will not directly get in touch with ByteBuffer anyway. For those + users it does not matter if the base API uses `ByteBuffer` or `[UInt8]`. +2. The incoming and outgoing data must be in the `ByteBuffer` format anyway, as Lambda uses SwiftNIO under the hood and + SwiftNIO uses `ByteBuffer` in its APIs. By using `ByteBuffer` we can save a copies to and from `[UInt8]`. This will + reduce the invocation time for all users. +3. The base `LambdaHandler` API is most likely mainly being used by developers that want to integrate their web + framework with Lambda (examples: Vapor, Hummingbird, ...). Those developers will most likely prefer to get the data + in the `ByteBuffer` format anyway, as their lower level networking stack also depends on SwiftNIO. ### Users create a LambdaResponse, that supports streaming instead of being passed a LambdaResponseWriter -Instead of passing the `LambdaResponseWriter` in the invocation we considered a new type `LambdaResponse`, that users must return in the `StreamingLambdaHandler`. +Instead of passing the `LambdaResponseWriter` in the invocation we considered a new type `LambdaResponse`, that users +must return in the `StreamingLambdaHandler`. Its API would look like this: @@ -540,17 +635,29 @@ public protocol StreamingLambdaHandler: ~Copyable { } ``` -There are pros and cons for the API that returns the `LambdaResponses` and there are pros and cons for the API that receives a `LambdaResponseWriter` as a parameter. +There are pros and cons for the API that returns the `LambdaResponses` and there are pros and cons for the API that +receives a `LambdaResponseWriter` as a parameter. -Concerning following structured concurrency principles the approach that receives a `LambdaResponseWriter` as a parameter has benefits as the lifetime of the handle function is tied to the invocation runtime. -The approach that returns a `LambdaResponse` splits the invocation into two separate function calls. First the handle method is invoked, second the `LambdaResponse` writer closure is invoked. This means that it is impossible to use Swift APIs that use `with` style lifecycle management patterns from before creating the response until sending the full response stream off. This means for example: That users instrumenting their lambdas with Swift tracing likely can not use the `withSpan` API for the full lifetime of the request, if they return a streamed response. +Concerning following structured concurrency principles the approach that receives a `LambdaResponseWriter` as a +parameter has benefits as the lifetime of the handle function is tied to the invocation runtime. The approach that +returns a `LambdaResponse` splits the invocation into two separate function calls. First the handle method is invoked, +second the `LambdaResponse` writer closure is invoked. This means that it is impossible to use Swift APIs that use +`with` style lifecycle management patterns from before creating the response until sending the full response stream off. +This means for example: That users instrumenting their lambdas with Swift tracing likely can not use the `withSpan` API +for the full lifetime of the request, if they return a streamed response. -However, if it comes to consistency with the larger Swift on server ecosystem, the API that returns a `LambdaResponse` is likely the better choice. Hummingbird v2, OpenAPI and the new Swift gRPC v2 implementation all use this approach. This might be due to the fact that writing middleware becomes easier, if a Response is explicitly returned. +However, if it comes to consistency with the larger Swift on server ecosystem, the API that returns a `LambdaResponse` +is likely the better choice. Hummingbird v2, OpenAPI and the new Swift gRPC v2 implementation all use this approach. +This might be due to the fact that writing middleware becomes easier, if a Response is explicitly returned. -We decided to implement the approach in which a `LambdaResponseWriter` is passed to the function, since the approach in which a `LambdaResponse` is returned can trivially be built on top of it. This is not true vice versa. +We decided to implement the approach in which a `LambdaResponseWriter` is passed to the function, since the approach in +which a `LambdaResponse` is returned can trivially be built on top of it. This is not true vice versa. We welcome the discussion on this topic and are open to change our minds and API here. ## A word about versioning -We are aware that AWS Lambda Runtime has not reached a proper 1.0. We intend to keep the current implementation around at 1.0-alpha. We don’t want to change the current API without releasing a new major. We think there are lots of adopters out there that depend on the API in v1. Because of this we intend to release the proposed API here as AWS Lambda Runtime v2. +We are aware that AWS Lambda Runtime has not reached a proper 1.0. We intend to keep the current implementation around +at 1.0-alpha. We don’t want to change the current API without releasing a new major. We think there are lots of adopters +out there that depend on the API in v1. Because of this we intend to release the proposed API here as AWS Lambda Runtime +v2. From 77ad9a500b29c149622f91c8797b0a0d83a6771c Mon Sep 17 00:00:00 2001 From: Aryan Shah Date: Thu, 8 Aug 2024 14:35:49 +0100 Subject: [PATCH 04/10] Fix inaccuracies --- .../Proposals/0001-v2-api.md | 46 +++++++++---------- 1 file changed, 23 insertions(+), 23 deletions(-) diff --git a/Sources/AWSLambdaRuntimeCore/Documentation.docc/Proposals/0001-v2-api.md b/Sources/AWSLambdaRuntimeCore/Documentation.docc/Proposals/0001-v2-api.md index 7afdb484..e6afefaf 100644 --- a/Sources/AWSLambdaRuntimeCore/Documentation.docc/Proposals/0001-v2-api.md +++ b/Sources/AWSLambdaRuntimeCore/Documentation.docc/Proposals/0001-v2-api.md @@ -18,7 +18,7 @@ running blocking code on the same `EventLoop` the library uses. Developers also methods that are used to work with `EventLoop`s and `EventLoopFuture`s. This results in a lot of cognitive complexity and makes the code in the current API hard to reason about and maintain. For these reasons, the overarching trend in the Swift on Server ecosystem is to shift to newer, more readable, Swift concurrency constructs and de-couple from -SwiftNIO’s `EventLoop` interfaces. +SwiftNIO's `EventLoop` interfaces. #### No ownership of the main() function @@ -37,13 +37,13 @@ an implementation of the `handle` function where the business logic of the Lambd `SimpleLambdaHandler` is perfectly sufficient for small use-cases as the user does not need to spend much time looking into the library. -However, `SimpleLambdaHandler` cannot be used when services such as a database client need to be initalized before the +However, `SimpleLambdaHandler` cannot be used when services such as a database client need to be initialized before the Lambda runtime starts and then also gracefully shutdown prior to the runtime terminating. This is because the only way to register termination logic is through the `LambdaInitializationContext` (containing a field `terminator: LambdaTerminator`) which is created and used _internally_ within `LambdaRuntime` and never exposed through `SimpleLambdaHandler`. For such use-cases, other handler protocols like `LambdaHandler` must be used. `LambdaHandler` exposes a `context` argument of type `LambdaInitializationContext` through its initializer. Within the initializer, -required services can be initalized and their graceful shutdown logic can be registered with the +required services can be initialized and their graceful shutdown logic can be registered with the `context.terminator.register` function. Yet, `LambdaHandler` is quite cumbersome to use in such use-cases as users have to deviate from the established norms of @@ -57,7 +57,7 @@ The Lambda runtime can only be started using the **internal** `Lambda.run()` fun `main()` function defined by the `LambdaHandler` protocol, preventing users from injecting initialized services into the runtime _prior_ to it starting. As shown below, this forces users to use an **unstructured concurrency** approach and manually initialize services, leading to the issue of the user then perhaps forgetting to gracefully shutdown the -initalized services: +initialized services: ```swift struct MyLambda: LambdaHandler { @@ -88,7 +88,7 @@ struct MyLambda: LambdaHandler { } func handle(_ event: Event, context: LambdaContext) async throws -> Output { - /// Use the initalized service stored in `self.pgClient` + /// Use the initialized service stored in `self.pgClient` try await self.pgClient.query(...) } } @@ -131,7 +131,7 @@ of the `EventLoop` family of interfaces. same way the lifecycles of the required services are managed, e.g. `try await ServiceGroup(services: [postgresClient, ..., lambdaRuntime], ...).run()`. - Dependencies can now be injected into `LambdaRuntime` — `swift-service-lifecycle` guarantees that the services will be - initialized _before_ the `LambdaRuntime`’s `run()` function is called. + initialized _before_ the `LambdaRuntime`'s `run()` function is called. - The required services can then be used within the handler in a structured concurrency manner. `swift-service-lifecycle` takes care of listening for termination signals and terminating the services as well as the `LambdaRuntime` in correct order. @@ -162,7 +162,7 @@ try await serviceGroup.run() ### Simplifying Codable support -A detailed explanation is provided in the Codable Support section. In short, much of the boilerplate code defined for +A detailed explanation is provided in the **Codable Support** section. In short, much of the boilerplate code defined for each handler protocol in `Lambda+Codable` and `Lambda+String` will be replaced with a single `LambdaCodableAdapter` class. @@ -181,7 +181,7 @@ Both will be explained in the **Detailed Solution** section. ## Detailed Solution -Below you’ll find explanation’s for all types that we want to use in AWS Lambda Runtime v2. +Below are explanations for all types that we want to use in AWS Lambda Runtime v2. ### LambdaResponseWriter @@ -205,10 +205,10 @@ public protocol LambdaResponseWriter: ~Copyable { ### LambdaContext `LambdaContext` will be largely unchanged, but the `eventLoop` property as well as the `allocator` property (of type -`ByteBuffer`) will be removed. +`ByteBufferAllocator`) will be removed. -A new function `backgroundTask()` will also be added. This will allow tasks to be run in the background while and after -the response is/has been sent. Please note that `LambdaContext` will not be Sendable anymore. +A new function `addBackgroundTask(_:)` will also be added. This will allow tasks to be run in the background while and after +the response is/has been sent. Please note that `LambdaContext` will not be `Sendable` anymore. ```swift /// A context object passed as part of an invocation in LambdaHandler handle functions. @@ -247,10 +247,10 @@ public struct LambdaContext { ### StreamingLambdaHandler The new `StreamingLambdaHandler` protocol is the base protocol to implement a Lambda function. Most users will not use -this protocol and instead use the `LambdaHandler` protocol defined in the `Codable` Support section. +this protocol and instead use the `LambdaHandler` protocol defined in the **Codable Support** section. ```swift -/// The base LambdaHandler protocol +/// The base StreamingLambdaHandler protocol public protocol StreamingLambdaHandler: ~Copyable { /// The business logic of the Lambda function /// - Parameters: @@ -354,7 +354,7 @@ enum Lambda { Since the library now provides ownership of the `main()` function and allows users to initialize services before the `LambdaRuntime` is initialized, the library cannot implicitly report [errors that occur during initialization to the dedicated endpoint AWS exposes](https://docs.aws.amazon.com/lambda/latest/dg/runtimes-api.html#runtimes-api-initerror) -like it currently does through the `initialize()` function of `LambdaRunner` which wraps the handler’s `init(...)` and +like it currently does through the `initialize()` function of `LambdaRunner` which wraps the handler's `init(...)` and handles any errors thrown by reporting it to the dedicated AWS endpoint. To retain support for initialization error reporting, the `Lambda.reportStartupError(any Error)` function gives users @@ -431,7 +431,7 @@ public protocol LambdaOutputEncoder { } ``` -We provide conformances for Foundation’s `JSONDecoder` to `LambdaEventDecoder` and `JSONEncoder` to +We provide conformances for Foundation's `JSONDecoder` to `LambdaEventDecoder` and `JSONEncoder` to `LambdaOutputEncoder`. 3. Implements its `handle()` method by: @@ -446,7 +446,7 @@ We provide conformances for Foundation’s `JSONDecoder` to `LambdaEventDecoder` the user. ```swift -/// Wraps an underlying handler conforming to `CodableLambdaHandler` +/// Wraps an underlying handler conforming to `LambdaHandler` /// with encoding/decoding logic public struct LambdaCodableAdapter< Handler: LambdaHandler, @@ -486,16 +486,16 @@ To create a Lambda function using the current API, a user first has to create an handler protocols by implementing the initializer and the `handle(...)` function. Now that `LambdaRuntime` is public, this verbosity can very easily be simplified. -**ClosureHandler** +#### ClosureHandler This handler is generic over any `Event` type conforming to `Decodable` and any `Output` type conforming to `Encodable` or `Void`. ```swift public struct ClosureHandler: LambdaHandler { - /// Intialize with a closure handler over generic Input and Output types + /// Initialize with a closure handler over generic Input and Output types public init(body: @escaping (Event, LambdaContext) async throws -> Output) where Output: Encoda - /// Intialize with a closure handler over a generic Input type (Void Output). + /// Initialize with a closure handler over a generic Input type (Void Output). public init(body: @escaping (Event, LambdaContext) async throws -> Void) where Output == Void /// The business logic of the Lambda function. public func handle(_ event: Event, context: LambdaContext) async throws -> Output @@ -523,7 +523,7 @@ extension LambdaRuntime { } ``` -We can now significantly reduce the verbosity and leverage Swift’s trailing closure syntax to cleanly create and run a +We can now significantly reduce the verbosity and leverage Swift's trailing closure syntax to cleanly create and run a Lambda function, abstracting away the decoding and encoding logic from the user: ```swift @@ -643,8 +643,8 @@ parameter has benefits as the lifetime of the handle function is tied to the inv returns a `LambdaResponse` splits the invocation into two separate function calls. First the handle method is invoked, second the `LambdaResponse` writer closure is invoked. This means that it is impossible to use Swift APIs that use `with` style lifecycle management patterns from before creating the response until sending the full response stream off. -This means for example: That users instrumenting their lambdas with Swift tracing likely can not use the `withSpan` API -for the full lifetime of the request, if they return a streamed response. +For example, users instrumenting their lambdas with Swift tracing likely can not use the `withSpan` API for the full +lifetime of the request, if they return a streamed response. However, if it comes to consistency with the larger Swift on server ecosystem, the API that returns a `LambdaResponse` is likely the better choice. Hummingbird v2, OpenAPI and the new Swift gRPC v2 implementation all use this approach. @@ -658,6 +658,6 @@ We welcome the discussion on this topic and are open to change our minds and API ## A word about versioning We are aware that AWS Lambda Runtime has not reached a proper 1.0. We intend to keep the current implementation around -at 1.0-alpha. We don’t want to change the current API without releasing a new major. We think there are lots of adopters +at 1.0-alpha. We don't want to change the current API without releasing a new major. We think there are lots of adopters out there that depend on the API in v1. Because of this we intend to release the proposed API here as AWS Lambda Runtime v2. From 4eea6ec98010b8495cef7e6392981e7504ac37f1 Mon Sep 17 00:00:00 2001 From: Aryan Shah Date: Thu, 8 Aug 2024 14:45:06 +0100 Subject: [PATCH 05/10] Add explanation for `LambdaContext` not being `Sendable` --- .../Documentation.docc/Proposals/0001-v2-api.md | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/Sources/AWSLambdaRuntimeCore/Documentation.docc/Proposals/0001-v2-api.md b/Sources/AWSLambdaRuntimeCore/Documentation.docc/Proposals/0001-v2-api.md index e6afefaf..3830f261 100644 --- a/Sources/AWSLambdaRuntimeCore/Documentation.docc/Proposals/0001-v2-api.md +++ b/Sources/AWSLambdaRuntimeCore/Documentation.docc/Proposals/0001-v2-api.md @@ -208,7 +208,11 @@ public protocol LambdaResponseWriter: ~Copyable { `ByteBufferAllocator`) will be removed. A new function `addBackgroundTask(_:)` will also be added. This will allow tasks to be run in the background while and after -the response is/has been sent. Please note that `LambdaContext` will not be `Sendable` anymore. +the response is/has been sent. Please note that `LambdaContext` will not be `Sendable` anymore, because in order +to allow adding background tasks in a structured way, the `LambdaContext` will need to be backed with a `TaskGroup`. +Since `TaskGroup` is not `Sendable` we cannot mark `LambdaContext` as `Sendable`. If users need any property out of the +`LambdaContext` they can just extract the property and then pass it around or close over it as all the properties are +`Sendable`. ```swift /// A context object passed as part of an invocation in LambdaHandler handle functions. From be972669f7741481f087667a196020fa4154c532 Mon Sep 17 00:00:00 2001 From: Aryan Shah Date: Fri, 9 Aug 2024 15:22:04 +0100 Subject: [PATCH 06/10] Remove mention of `LambdaCodableAdapter` being a class and update code blocks --- .../Documentation.docc/Proposals/0001-v2-api.md | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/Sources/AWSLambdaRuntimeCore/Documentation.docc/Proposals/0001-v2-api.md b/Sources/AWSLambdaRuntimeCore/Documentation.docc/Proposals/0001-v2-api.md index 3830f261..f7285606 100644 --- a/Sources/AWSLambdaRuntimeCore/Documentation.docc/Proposals/0001-v2-api.md +++ b/Sources/AWSLambdaRuntimeCore/Documentation.docc/Proposals/0001-v2-api.md @@ -164,9 +164,9 @@ try await serviceGroup.run() A detailed explanation is provided in the **Codable Support** section. In short, much of the boilerplate code defined for each handler protocol in `Lambda+Codable` and `Lambda+String` will be replaced with a single `LambdaCodableAdapter` -class. +struct. -This adapter class is generic over (1) any handler conforming to `LambdaHandler`, (2) the user-specified input and +This adapter struct is generic over (1) any handler conforming to `LambdaHandler`, (2) the user-specified input and output types, and (3) any decoder and encoder conforming to `LambdaEventDecoder` and `LambdaOutputDecoder`. The adapter will wrap the underlying handler with encoding/decoding logic. @@ -424,14 +424,14 @@ public protocol LambdaHandler { ```swift public protocol LambdaEventDecoder { - /// Decode the ByteBuffer representing the received event into the generic type T + /// Decode the ByteBuffer representing the received event into the generic type Event /// the handler will receive - func decode(_ type: T.Type, from buffer: ByteBuffer) throws -> T + func decode(_ type: Event.Type, from buffer: ByteBuffer) throws -> Event } public protocol LambdaOutputEncoder { - /// Encode the generic type T the handler has produced into a ByteBuffer - func encode(_ value: T, into buffer: inout ByteBuffer) throws + /// Encode the generic type Output the handler has produced into a ByteBuffer + func encode(_ value: Output, into buffer: inout ByteBuffer) throws } ``` @@ -498,7 +498,7 @@ or `Void`. ```swift public struct ClosureHandler: LambdaHandler { /// Initialize with a closure handler over generic Input and Output types - public init(body: @escaping (Event, LambdaContext) async throws -> Output) where Output: Encoda + public init(body: @escaping (Event, LambdaContext) async throws -> Output) where Output: Encodable /// Initialize with a closure handler over a generic Input type (Void Output). public init(body: @escaping (Event, LambdaContext) async throws -> Void) where Output == Void /// The business logic of the Lambda function. From bffb114768c3143555b11f1158497067af3f18dd Mon Sep 17 00:00:00 2001 From: aryan-25 Date: Mon, 19 Aug 2024 13:55:38 +0100 Subject: [PATCH 07/10] Revision 1 - Remove the `reportError(_:)` method from `LambdaResponseStreamWriter` and instead make the `handle(...)` method of `StreamingLambdaHandler` throwing. - Remove the `addBackgroundTask(_:)` method from `LambdaContext` due to structured concurrency concerns and introduce the `LambdaWithBackgroundProcessingHandler` protocol as a solution. - Introduce `LambdaHandlerAdapter`, which adapts handlers conforming to `LambdaHandler` with `LambdaWithBackgroundProcessingHandler`. - Update `LambdaCodableAdapter` to now be generic over any handler conforming to `LambdaWithBackgroundProcessingHandler` instead of `LambdaHandler`. --- .../Proposals/0001-v2-api.md | 402 ++++++++++++++---- 1 file changed, 313 insertions(+), 89 deletions(-) diff --git a/Sources/AWSLambdaRuntimeCore/Documentation.docc/Proposals/0001-v2-api.md b/Sources/AWSLambdaRuntimeCore/Documentation.docc/Proposals/0001-v2-api.md index f7285606..c9d730b3 100644 --- a/Sources/AWSLambdaRuntimeCore/Documentation.docc/Proposals/0001-v2-api.md +++ b/Sources/AWSLambdaRuntimeCore/Documentation.docc/Proposals/0001-v2-api.md @@ -6,6 +6,21 @@ SwiftNIO `EventLoop` interfaces and async/await. However, just like `gRPC-swift` shift to solely using async/await instead of `EventLoop` interfaces. For this, large parts of the current API have to be reconsidered. +## Overview + +Versions: + +- v1 (2024-08-07): Initial version +- v1.1: + - Remove the `reportError(_:)` method from `LambdaResponseStreamWriter` and instead make the `handle(...)` method of + `StreamingLambdaHandler` throwing. + - Remove the `addBackgroundTask(_:)` method from `LambdaContext` due to structured concurrency concerns and introduce + the `LambdaWithBackgroundProcessingHandler` protocol as a solution. + - Introduce `LambdaHandlerAdapter`, which adapts handlers conforming to `LambdaHandler` with + `LambdaWithBackgroundProcessingHandler`. + - Update `LambdaCodableAdapter` to now be generic over any handler conforming to + `LambdaWithBackgroundProcessingHandler` instead of `LambdaHandler`. + ## Motivation ### Current Limitations @@ -162,61 +177,54 @@ try await serviceGroup.run() ### Simplifying Codable support -A detailed explanation is provided in the **Codable Support** section. In short, much of the boilerplate code defined for -each handler protocol in `Lambda+Codable` and `Lambda+String` will be replaced with a single `LambdaCodableAdapter` +A detailed explanation is provided in the **Codable Support** section. In short, much of the boilerplate code defined +for each handler protocol in `Lambda+Codable` and `Lambda+String` will be replaced with a single `LambdaCodableAdapter` struct. -This adapter struct is generic over (1) any handler conforming to `LambdaHandler`, (2) the user-specified input and -output types, and (3) any decoder and encoder conforming to `LambdaEventDecoder` and `LambdaOutputDecoder`. The adapter -will wrap the underlying handler with encoding/decoding logic. - -### Simplifying the handler protocols - -There are four different handler protocols in the current API (`SimpleLambdaHandler`, `LambdaHandler`, -`EventLoopLambdaHandler`, `ByteBufferLambdaHandler`). As noted in the **Current Limitations** section, the ease-of-use -varies for each handler protocol and users may not be able to easily determine which protocol best serves their use-case -without spending time digging into the library. To reduce this problem and provide users with clear-cut options, we -propose replacing all of the existing handler protocols with just two: `LambdaHandler` and `StreamingLambdaHandler`. -Both will be explained in the **Detailed Solution** section. +This adapter struct is generic over (1) any handler conforming to a new handler protocol +`LambdaWithBackgroundProcessingHandler`, (2) the user-specified input and output types, and (3) any decoder and encoder +conforming to protocols `LambdaEventDecoder` and `LambdaOutputDecoder`. The adapter will wrap the underlying handler +with encoding/decoding logic. ## Detailed Solution Below are explanations for all types that we want to use in AWS Lambda Runtime v2. -### LambdaResponseWriter +### LambdaResponseStreamWriter -We will introduce a new `LambdaResponseWriter` protocol. It is used in the new `StreamingLambdaHandler` (defined below), -which is the new base protocol for the `LambdaRuntime` (defined below as well). +We will introduce a new `LambdaResponseStreamWriter` protocol. It is used in the new `StreamingLambdaHandler` (defined +below), which is the new base protocol for the `LambdaRuntime` (defined below as well). ```swift /// A writer object to write the Lambda response stream into -public protocol LambdaResponseWriter: ~Copyable { - /// Write a response part into the stream +public protocol LambdaResponseStreamWriter: ~Copyable { + /// Write a response part into the stream. The HTTP response is started lazily before the first call to `write(_:)`. + /// Bytes written to the writer are streamed continually. func write(_ buffer: ByteBuffer) async throws - /// End the response stream + /// End the response stream and the underlying HTTP response. consuming func finish() - /// Write a response part into the stream and end the response stream + /// Write a response part into the stream and end the response stream as well as the underlying HTTP response. consuming func writeAndFinish(_ buffer: ByteBuffer) async throws - /// Report an error in the response stream - consuming func reportError(_ error: any Error) async throws } ``` -### LambdaContext +It is important to note that `LambdaResponseStreamWriter` will be non-copyable, with the `finish()` and +`writeAndFinish(_:)` functions having the `consuming` ownership keyword. This is so that the compiler can enforce the +restriction of not being able to call `write(_:)` after any of the finishing functions have been called. The compiler +can also prevent the finishing functions from being called multiple times. If the user does not call `finish()`, the +library will automatically finish the stream after the last `write` utilizing the writers `deinit`. -`LambdaContext` will be largely unchanged, but the `eventLoop` property as well as the `allocator` property (of type -`ByteBufferAllocator`) will be removed. +### LambdaContext -A new function `addBackgroundTask(_:)` will also be added. This will allow tasks to be run in the background while and after -the response is/has been sent. Please note that `LambdaContext` will not be `Sendable` anymore, because in order -to allow adding background tasks in a structured way, the `LambdaContext` will need to be backed with a `TaskGroup`. -Since `TaskGroup` is not `Sendable` we cannot mark `LambdaContext` as `Sendable`. If users need any property out of the -`LambdaContext` they can just extract the property and then pass it around or close over it as all the properties are -`Sendable`. +`LambdaContext` will be largely unchanged, but the `eventLoop` property will be removed. The `allocator` property of +type `ByteBufferAllocator` will also be removed because (1), we generally want to reduce the number of SwiftNIO types +exposed in the API, and (2), `ByteBufferAllocator` does not optimize the allocation strategies. The common pattern +observed across many libraries is to re-use existing `ByteBuffer`s as much as possible. This is also what we do for the +`LambdaCodableAdapter` (explained in the **Codable Support** section) implementation. ```swift /// A context object passed as part of an invocation in LambdaHandler handle functions. -public struct LambdaContext { +public struct LambdaContext: Sendable { /// The request ID, which identifies the request that triggered the function invocation. public var requestID: String { get } @@ -239,19 +247,18 @@ public struct LambdaContext { /// /// - note: The `LogLevel` can be configured using the `LOG_LEVEL` environment variable. public var logger: Logger { get } - - /// Schedule background work in lambda. This work must be completed before - /// the runtime can ask for further events. Note that AWS will continue to - /// charge you after the response has been returned, but work is still - /// processing in the background. - public func addBackgroundTask(_ body: sending @escaping () async -> ()) } ``` -### StreamingLambdaHandler +### Handlers + +We introduce three handler protocols: `StreamingLambdaHandler`, `LambdaHandler`, and +`LambdaWithBackgroundProcessingHandler`. + +#### StreamingLambdaHandler The new `StreamingLambdaHandler` protocol is the base protocol to implement a Lambda function. Most users will not use -this protocol and instead use the `LambdaHandler` protocol defined in the **Codable Support** section. +this protocol and instead use the `LambdaHandler` protocol defined below. ```swift /// The base StreamingLambdaHandler protocol @@ -259,14 +266,35 @@ public protocol StreamingLambdaHandler: ~Copyable { /// The business logic of the Lambda function /// - Parameters: /// - event: The invocation's input data - /// - responseWriter: A ``LambdaResponseWriter`` to write the invocations response to. + /// - responseWriter: A ``LambdaResponseStreamWriter`` to write the invocation's response to. /// If no response or error is written to the `responseWriter` it will /// report an error to the invoker. /// - context: The LambdaContext containing the invocation's metadata - mutating func handle(_ event: ByteBuffer, responseWriter: consuming some LambdaResponseWriter, context: LambdaContext) async + /// - Throws + /// How the thrown error will be handled by the runtime: + /// - An invocation error will be reported if the error is thrown before the first call to + /// ``LambdaResponseStreamWriter.write(_:)``. + /// - If the error is thrown after call(s) to ``LambdaResponseStreamWriter.write(_:)`` but before + /// a call to ``LambdaResponseStreamWriter.finish()``, the response stream will be closed and trailing + /// headers will be sent. + /// - If ``LambdaResponseStreamWriter.finish()`` has already been called before the error is thrown, the + /// error will be logged. + mutating func handle(_ event: ByteBuffer, responseWriter: consuming some LambdaResponseStreamWriter, context: LambdaContext) async throws } ``` +Using this protocol requires the `handle` method to receive the incoming event as a `ByteBuffer` and return the output +as a `ByteBuffer` too. + +Through the `LambdaResponseStreamWriter`, which is passed as an argument in the `handle` function, the **response can be +streamed** by calling the `write(_:)` function of the `LambdaResponseStreamWriter` with partial data repeatedly before +finally closing the response stream by calling `finish()`. Users can also choose to return the entire output and not +stream the response by calling `writeAndFinish(_:)`. + +This protocol also allows for background tasks to be run after a result has been reported to the AWS Lambda control +plane, since the `handle(...)` function is free to implement any background work after the call to +`responseWriter.finish()`. + The protocol is defined in a way that supports a broad range of use-cases. The handle method is marked as `mutating` to allow handlers to be implemented with a `struct`. Users can opt to even mark the handler as non-copyable. @@ -276,20 +304,141 @@ An implementation that sends the number 1 to 10 every 500ms could look like this struct SendNumbersWithPause: StreamingLambdaHandler { func handle( _ event: ByteBuffer, - responseWriter: consuming some LambdaResponseWriter, + responseWriter: consuming some LambdaResponseStreamWriter, context: LambdaContext - ) async { + ) async throws { for i in 1...10 { + // Send partial data responseWriter.write(ByteBuffer(string: #"\#(i)\n\r"#)) - try? await Task.sleep(for: .milliseconds(500)) + // Perform some long asynchronous work + try await Task.sleep(for: .milliseconds(500)) } + // All data has been sent. Close off the response stream. responseWriter.finish() } } ``` -The method is not marked as `throws` as we want the users of this API to handle errors themselves and call -`LambdaResponseWriter.reportError(_:)` if needed. +#### LambdaHandler: + +This handler protocol will be the go-to choice for most use-cases because it is completely agnostic to any +encoding/decoding logic -- conforming objects simply have to implement the `handle` function where the input and return +types are Swift objects. + +Note that the `handle` function does not receive a `LambdaResponseStreamWriter` as an argument. Response streaming is +not viable for `LambdaHandler` because the output has to be encoded prior to it being sent, e.g. it is not possible to +encode a partial/incomplete JSON string. + +```swift +public protocol LambdaHandler { + /// Generic input type + /// The body of the request sent to Lambda will be decoded into this type for the handler to consume + associatedtype Event + /// Generic output type + /// This is the return type of the handle() function. + associatedtype Output + + /// The business logic of the Lambda function. Receives a generic input type and returns a generic output type. + /// Agnostic to encoding/decoding + mutating func handle(_ event: Event, context: LambdaContext) async throws -> Output +} +``` + +#### LambdaWithBackgroundProcessingHandler: + +This protocol is exactly like `LambdaHandler`, with the only difference being the added support for executing background +work after the result has been sent to the AWS Lambda control plane. + +This is achieved by not having a return type in the `handle` function. The output is instead written into a +`LambdaResponseWriter` that is passed in as an argument, meaning that the `handle` function is then free to implement +any background work after the result has been sent to the AWS Lambda control plane. + +`LambdaResponseWriter` has different semantics to the `LambdaResponseStreamWriter`. Where the `write(_:)` function of +`LambdaResponseStreamWriter` means writing into a response stream, the `write(_:)` function of `LambdaResponseWriter` +simply serves as a mechanism to return the output without explicitly returning from the `handle` function. + +```swift +public protocol LambdaResponseWriter: ~Copyable { + associatedtype Output + + /// Sends the generic Output object (representing the computed result of the handler) + /// to the AWS Lambda response endpoint. + consuming func write(_: Output) async throws +} + +public protocol LambdaWithBackgroundProcessingHandler { + /// Generic input type + /// The body of the request sent to Lambda will be decoded into this type for the handler to consume + associatedtype Event + /// Generic output type + /// This is the type that the handle() function will send through the ``LambdaResponseWriter``. + associatedtype Output + + /// The business logic of the Lambda function. Receives a generic input type and returns a generic output type. + /// Agnostic to JSON encoding/decoding + func handle( + _ event: Event, + outputWriter: consuming some LambdaResponseWriter, + context: LambdaContext + ) async throws +} +``` + +##### Example Usage: + +```swift +struct BackgroundProcessingHandler: LambdaWithBackgroundProcessingHandler { + struct Input: Decodable { + let message: String + } + + struct Greeting: Encodable { + let echoedMessage: String + } + + typealias Event = Input + typealias Output = Greeting + + func handle( + _ event: Event, + outputWriter: consuming some LambdaResponseWriter, + context: LambdaContext + ) async throws { + // Return result to the Lambda control plane + try await outputWriter.write(result: Greeting(echoedMessage: event.messageToEcho)) + + // Perform some background work, e.g: + try await Task.sleep(for: .seconds(10)) + + // Exit the function. All asynchronous work has been executed before exiting the scope of this function. + // Follows structured concurrency principles. + return + } +} +``` + +#### Handler Adapters + +Since the `StreamingLambdaHandler` protocol is the base protocol the `LambdaRuntime` works with, there are adapters to +make both `LambdaHandler` and `LambdaWithBackgroundProcessingHandler` compatible with `StreamingLambdaHandler`. + +1. `LambdaHandlerAdapter` accepts a `LambdaHandler` and conforms it to `LambdaWithBackgroundProcessingHandler`. This is + achieved by taking the generic `Output` object returned from the `handle` function of `LambdaHandler` and passing it + to the `write(_:)` function of the `LambdaResponseWriter`. + +2. `LambdaCodableAdapter` accepts a `LambdaWithBackgroundProcessingHandler` and conforms it to `StreamingLambdaHandler`. + This is achieved by wrapping the `LambdaResponseWriter` with the `LambdaResponseStreamWriter` provided by + `StreamingLambdaHandler`. A call to the `write(_:)` function of `LambdaResponseWriter` is translated into a call to + the `writeAndFinish(_:)` function of `LambdaResponseStreamWriter`. + +Both `LambdaHandlerAdapter` and `LambdaCodableAdapter` are described in greater detail in the **Codable Support** +section. + +To summarize, `LambdaHandler` can be used with the `LambdaRuntime` by first going through `LambdaHandlerAdapter` and +then through `LambdaCodableAdapter`. `LambdaWithBackgroundHandler` just requires `LambdaCodableAdapter`. + +For the common JSON-in and JSON-out use-case, there is an extension on `LambdaRuntime` that abstracts away this wrapping +from the user. ### LambdaRuntime @@ -396,31 +545,42 @@ now being able to cleanly manage the lifecycles of required services in a struct ### Codable support -For v2 we want to introduce a `LambdaCodableAdapter` that: +The `LambdaHandler` and `LambdaWithBackgroundProcessingHandler` protocols abstract away encoding/decoding logic from the +conformers as they are generic over custom `Event` and `Output` types. We introduce two adapters `LambdaHandlerAdapter` +and `CodableLambdaAdapter` that implement the encoding/decoding logic and in turn allow the respective handlers to +conform to `StreamingLambdaHandler`. -1. Accepts any generic underlying handler conforming to a new protocol `LambdaHandler` specifying `associatedtype`s - `Event` and `Output`, and a `handle(_ event: Event, context: LambdaContext) async throws -> Output` function. +#### LambdaHandlerAdapter -#### LambdaHandler +Any handler conforming to `LambdaHandler` can be conformed to `LambdaWithBackgroundProcessingHandler` through +`LambdaHandlerAdapter`. ```swift -public protocol LambdaHandler { - /// Generic input type - /// The body of the request sent to Lambda will be decoded into this type for the handler to consume - associatedtype Event - /// Generic output type - /// This is the return type of the handle() function. - associatedtype Output +/// Wraps an underlying handler conforming to ``LambdaHandler`` +/// with ``LambdaWithBackgroundProcessingHandler``. +public struct LambdaHandlerAdapter< + Event: Decodable, + Output, + Handler: LambdaHandler +>: LambdaWithBackgroundProcessingHandler where Handler.Event == Event, Handler.Output == Output { + let handler: Handler - /// The business logic of the Lambda function. Receives a generic input type and returns a generic output type. - /// Agnostic to JSON encoding/decoding - mutating func handle(_ event: Event, context: LambdaContext) async throws -> Output + /// Register the concrete handler. + public init(handler: Handler) + + /// 1. Call the `self.handler.handle(...)` with `event` and `context`. + /// 2. Pass the generic `Output` object returned from `self.handler.handle(...)` to `outputWriter.write(_:)` + public func handle(_ event: Event, outputWriter: consuming some LambdaResponseWriter, context: LambdaContext) async throws } ``` -2. Accepts _any_ encoder and decoder object conforming to the `LambdaEventDecoder` and `LambdaOutputEncoder` protocols: +#### LambdaCodableAdapter -#### LambdaEventDecoder and LambdaOutputEncoder protocols +`LambdaCodableAdapter` accepts any generic underlying handler conforming to `LambdaWithBackgroundProcessingHandler`. It +also accepts _any_ encoder and decoder object conforming to the `LambdaEventDecoder` and `LambdaOutputEncoder` +protocols: + +##### LambdaEventDecoder and LambdaOutputEncoder protocols ```swift public protocol LambdaEventDecoder { @@ -438,22 +598,27 @@ public protocol LambdaOutputEncoder { We provide conformances for Foundation's `JSONDecoder` to `LambdaEventDecoder` and `JSONEncoder` to `LambdaOutputEncoder`. -3. Implements its `handle()` method by: - 1. Decoding the `ByteBuffer` event into the generic `Event` type. - 2. Passing the generic `Event` instance to the underlying handler's `handle()` method. - 3. Encoding the generic `Output` returned from the underlying `handle()` into JSON and returning it. +`LambdaCodableAdapter` implements its `handle()` method by: -#### LambdaCodableAdapter +1. Decoding the `ByteBuffer` event into the generic `Event` type. +2. Wrapping the `LambdaResponseStreamWriter` with a concrete `LambdaResponseWriter` such that calls to + `LambdaResponseWriter`s `write(_:)` are mapped to `LambdaResponseStreamWriter`s `writeAndFinish(_:)`. + - Note that the argument to `LambdaResponseWriter`s `write(_:)` is a generic `Output` object whereas + `LambdaResponseStreamWriter`s `writeAndFinish(_:)` requires a `ByteBuffer`. + - Therefore, the concrete implementation of `LambdaResponseWriter` also accepts an encoder. Its `write(_:)` function + first encodes the generic `Output` object and then passes it to the underlying `LambdaResponseStreamWriter`. +3. Passing the generic `Event` instance, the concrete `LambdaResponseWriter`, as well as the `LambdaContext` to the + underlying handler's `handle()` method. -`LambdaCodableAdapter` can implement encoding/decoding for _any_ handler conforming to `LambdaHandler` if `Event` is -`Decodable` and the `Output` is `Encodable`, meaning that the encoding/decoding stubs do not need to be implemented by -the user. +`LambdaCodableAdapter` can implement encoding/decoding for _any_ handler conforming to +`LambdaWithBackgroundProcessingHandler` if `Event` is `Decodable` and the `Output` is `Encodable` or `Void`, meaning +that the encoding/decoding stubs do not need to be implemented by the user. ```swift -/// Wraps an underlying handler conforming to `LambdaHandler` +/// Wraps an underlying handler conforming to `LambdaWithBackgroundProcessingHandler` /// with encoding/decoding logic public struct LambdaCodableAdapter< - Handler: LambdaHandler, + Handler: LambdaWithBackgroundProcessingHandler, Event: Decodable, Output, Decoder: LambdaEventDecoder, @@ -474,13 +639,14 @@ public struct LambdaCodableAdapter< ) where Output == Void, Encoder == VoidEncoder /// 1. Decode the invocation event using `self.decoder` - /// 2. Call the underlying `self.handler.handle()` - /// method with the decoded event data. - /// 3. Return the encoded output of (2) with `self.encoder` + /// 2. Create a concrete `LambdaResponseWriter` that maps calls to `write(_:)` with the `responseWriter`s `writeAndFinish(_:)` + /// 2. Call the underlying `self.handler.handle()` method with the decoded event data, the concrete `LambdaResponseWriter`, + /// and the `LambdaContext`. public mutating func handle( _ request: ByteBuffer, + responseWriter: consuming some LambdaResponseStreamWriter, context: LambdaContext - ) async throws -> LambdaResponse + ) async throws } ``` @@ -556,19 +722,19 @@ handle encoding/decoding themselves: public struct StreamingClosureHandler: StreamingLambdaHandler { public init( - body: @escaping sending (ByteBuffer, consuming LambdaResponseWriter, LambdaContext) async -> () + body: @escaping sending (ByteBuffer, consuming LambdaResponseStreamWriter, LambdaContext) async throws -> () ) public func handle( _ request: ByteBuffer, - responseWriter: consuming LambdaResponseWriter, + responseWriter: consuming LambdaResponseStreamWriter, context: LambdaContext - ) async + ) async throws } extension LambdaRuntime { public init( - body: @escaping sending (ByteBuffer, consuming LambdaResponseWriter, LambdaContext) async -> () + body: @escaping sending (ByteBuffer, consuming LambdaResponseStreamWriter, LambdaContext) async throws -> () ) } ``` @@ -589,10 +755,10 @@ for two reasons. framework with Lambda (examples: Vapor, Hummingbird, ...). Those developers will most likely prefer to get the data in the `ByteBuffer` format anyway, as their lower level networking stack also depends on SwiftNIO. -### Users create a LambdaResponse, that supports streaming instead of being passed a LambdaResponseWriter +### Users create a LambdaResponse, that supports streaming instead of being passed a LambdaResponseStreamWriter -Instead of passing the `LambdaResponseWriter` in the invocation we considered a new type `LambdaResponse`, that users -must return in the `StreamingLambdaHandler`. +Instead of passing the `LambdaResponseStreamWriter` in the invocation we considered a new type `LambdaResponse`, that +users must return in the `StreamingLambdaHandler`. Its API would look like this: @@ -640,9 +806,9 @@ public protocol StreamingLambdaHandler: ~Copyable { ``` There are pros and cons for the API that returns the `LambdaResponses` and there are pros and cons for the API that -receives a `LambdaResponseWriter` as a parameter. +receives a `LambdaResponseStreamWriter` as a parameter. -Concerning following structured concurrency principles the approach that receives a `LambdaResponseWriter` as a +Concerning following structured concurrency principles the approach that receives a `LambdaResponseStreamWriter` as a parameter has benefits as the lifetime of the handle function is tied to the invocation runtime. The approach that returns a `LambdaResponse` splits the invocation into two separate function calls. First the handle method is invoked, second the `LambdaResponse` writer closure is invoked. This means that it is impossible to use Swift APIs that use @@ -654,11 +820,69 @@ However, if it comes to consistency with the larger Swift on server ecosystem, t is likely the better choice. Hummingbird v2, OpenAPI and the new Swift gRPC v2 implementation all use this approach. This might be due to the fact that writing middleware becomes easier, if a Response is explicitly returned. -We decided to implement the approach in which a `LambdaResponseWriter` is passed to the function, since the approach in -which a `LambdaResponse` is returned can trivially be built on top of it. This is not true vice versa. +We decided to implement the approach in which a `LambdaResponseStreamWriter` is passed to the function, since the +approach in which a `LambdaResponse` is returned can trivially be built on top of it. This is not true vice versa. We welcome the discussion on this topic and are open to change our minds and API here. +### Adding a function `addBackgroundTask(_ body: sending @escaping () async -> ())` in `LambdaContext` + +Initially we proposed an explicit `addBackgroundTask(_:)` function in `LambdaContext` that users could call from their +handler object to schedule a background task to be run after the result is reported to AWS. We received feedback that +this approach for supporting background tasks does not exhibit structured concurrency, as code could still be in +execution after leaving the scope of the `handle(...)` function. + +For handlers conforming to the `StreamingLambdaHandler`, `addBackgroundTask(_:)` was anyways unnecessary as background +work could be executed in a structured concurrency manner within the `handle(...)` function after the call to +`LambdaResponseStreamWriter.finish()`. + +For handlers conforming to the `LambdaHandler` protocol, we considered extending `LambdaHandler` with a +`performPostHandleWork(...)` function that will be called after the `handle` function by the library. Users wishing to +add background work can override this function in their `LambdaHandler` conforming object. + +```swift +public protocol LambdaHandler: ~Copyable { + associatedtype Event + associatedtype Output + + func handle(_ event: Event, context: LambdaContext) async throws -> Output + + func performPostHandleWork(...) async throws -> Void +} + +extension LambdaHandler { + // User's can override this function if they wish to perform background work + // after returning a response from ``handle``. + func performPostHandleWork(...) async throws -> Void { + // nothing to do + } +} +``` + +Yet this poses difficulties when the user wishes to use any state created in the `handle(...)` function as part of the +background work. + +In general, the most common use-case for this library will be to implement simple Lambda functions that do not have +requirements for response streaming, nor to perform any background work after returning the output. To keep things easy +for the common use-case, and with Swift's principle of progressive disclosure of complexity in mind, we settled on three +handler protocols: + +1. `LambdaHandler`: Most common use-case. JSON-in, JSON-out. Does not support background work execution. An intuitive + `handle(event: Event, context: LambdaContext) -> Output` API that is simple to understand, i.e. users are not exposed + to the concept of sending their response through a writer. `LambdaHandler` can be very cleanly implemented and used + with `LambdaRuntime`, especially with `ClosureHandler`. +2. `LambdaWithBackgroundProcessingHandler`: If users wish to augment their `LambdaHandler` with the ability to run + background tasks, they can easily migrate. A user simply has to: + 1. Change the conformance to `LambdaWithBackgroundProcessingHandler`. + 2. Add an additional `outputWriter: some LambdaResponseWriter` argument to the `handle` function. + 3. Replace the `return ...` with `outputWriter.write(...)`. + 4. Implement any background work after `outputWriter.write(...)`. +3. `StreamingLambdaHandler`: This is the base handler protocol which is intended to be used directly only for advanced + use-cases. Users are provided the invocation event as a `ByteBuffer` and a `LambdaResponseStreamWriter` where the + computed result (as `ByteBuffer`) can either be streamed (with repeated calls to `write(_:)`) or sent all at once + (with a single call to `writeAndFinish(_:)`). After closing the `LambdaResponseStreamWriter`, any background work can + be implemented. + ## A word about versioning We are aware that AWS Lambda Runtime has not reached a proper 1.0. We intend to keep the current implementation around From b01eff841e2d1361fbabeb360d8df8360447ef42 Mon Sep 17 00:00:00 2001 From: Aryan Shah Date: Wed, 21 Aug 2024 10:56:51 +0100 Subject: [PATCH 08/10] Revision: Remove ~Copyable from StreamingLambdaHandler + Update EventLoop paragraph --- .../Proposals/0001-v2-api.md | 21 +++++++++---------- 1 file changed, 10 insertions(+), 11 deletions(-) diff --git a/Sources/AWSLambdaRuntimeCore/Documentation.docc/Proposals/0001-v2-api.md b/Sources/AWSLambdaRuntimeCore/Documentation.docc/Proposals/0001-v2-api.md index c9d730b3..b0fe6fec 100644 --- a/Sources/AWSLambdaRuntimeCore/Documentation.docc/Proposals/0001-v2-api.md +++ b/Sources/AWSLambdaRuntimeCore/Documentation.docc/Proposals/0001-v2-api.md @@ -28,12 +28,11 @@ Versions: #### EventLoop interfaces The current API extensively uses the `EventLoop` family of interfaces from SwiftNIO in many areas. To use these -interfaces correctly though, it requires developers to exercise great care and be aware of certain details such as never -running blocking code on the same `EventLoop` the library uses. Developers also need to understand the various transform -methods that are used to work with `EventLoop`s and `EventLoopFuture`s. This results in a lot of cognitive complexity -and makes the code in the current API hard to reason about and maintain. For these reasons, the overarching trend in the -Swift on Server ecosystem is to shift to newer, more readable, Swift concurrency constructs and de-couple from -SwiftNIO's `EventLoop` interfaces. +interfaces correctly though, it requires developers to exercise great care and understand the various transform methods +that are used to work with `EventLoop`s and `EventLoopFuture`s. This results in a lot of cognitive complexity and makes +the code in the current API hard to reason about and maintain. For these reasons, the overarching trend in the Swift on +Server ecosystem is to shift to newer, more readable, Swift concurrency constructs and de-couple from SwiftNIO's +`EventLoop` interfaces. #### No ownership of the main() function @@ -262,7 +261,7 @@ this protocol and instead use the `LambdaHandler` protocol defined below. ```swift /// The base StreamingLambdaHandler protocol -public protocol StreamingLambdaHandler: ~Copyable { +public protocol StreamingLambdaHandler { /// The business logic of the Lambda function /// - Parameters: /// - event: The invocation's input data @@ -270,7 +269,7 @@ public protocol StreamingLambdaHandler: ~Copyable { /// If no response or error is written to the `responseWriter` it will /// report an error to the invoker. /// - context: The LambdaContext containing the invocation's metadata - /// - Throws + /// - Throws: /// How the thrown error will be handled by the runtime: /// - An invocation error will be reported if the error is thrown before the first call to /// ``LambdaResponseStreamWriter.write(_:)``. @@ -296,7 +295,7 @@ plane, since the `handle(...)` function is free to implement any background work `responseWriter.finish()`. The protocol is defined in a way that supports a broad range of use-cases. The handle method is marked as `mutating` to -allow handlers to be implemented with a `struct`. Users can opt to even mark the handler as non-copyable. +allow handlers to be implemented with a `struct`. An implementation that sends the number 1 to 10 every 500ms could look like this: @@ -451,7 +450,7 @@ provide support for `swift-service-lifecycle`. /// The LambdaRuntime object. This object communicates with the Lambda control plane /// to fetch work and report errors. public final class LambdaRuntime: ServiceLifecycle.Service, Sendable - where Handler: StreamingLambdaHandler, Handler: ~Copyable + where Handler: StreamingLambdaHandler { /// Create a LambdaRuntime by passing a handler, an eventLoop and a logger. @@ -792,7 +791,7 @@ The `StreamingLambdaHandler` would look like this: ```swift /// The base LambdaHandler protocol -public protocol StreamingLambdaHandler: ~Copyable { +public protocol StreamingLambdaHandler { /// The business logic of the Lambda function /// - Parameters: /// - event: The invocation's input data From e75c0d464b01715011c0dc33b63130e8e7d31207 Mon Sep 17 00:00:00 2001 From: aryan-25 Date: Fri, 23 Aug 2024 11:40:20 +0100 Subject: [PATCH 09/10] Revision: Remove ~Copyable from LambdaResponseStreamWriter and LambdaResponseWriter Remove `~Copyable` from `LambdaResponseStreamWriter` and `LambdaResponseWriter`. Instead throw an error when `finish()` is called multiple times or when `write`/`writeAndFinish` is called after `finish()`. --- .../Proposals/0001-v2-api.md | 52 ++++++++++--------- 1 file changed, 27 insertions(+), 25 deletions(-) diff --git a/Sources/AWSLambdaRuntimeCore/Documentation.docc/Proposals/0001-v2-api.md b/Sources/AWSLambdaRuntimeCore/Documentation.docc/Proposals/0001-v2-api.md index b0fe6fec..85fc5901 100644 --- a/Sources/AWSLambdaRuntimeCore/Documentation.docc/Proposals/0001-v2-api.md +++ b/Sources/AWSLambdaRuntimeCore/Documentation.docc/Proposals/0001-v2-api.md @@ -20,6 +20,9 @@ Versions: `LambdaWithBackgroundProcessingHandler`. - Update `LambdaCodableAdapter` to now be generic over any handler conforming to `LambdaWithBackgroundProcessingHandler` instead of `LambdaHandler`. +- v1.2: + - Remove `~Copyable` from `LambdaResponseStreamWriter` and `LambdaResponseWriter`. Instead throw an error when + `finish()` is called multiple times or when `write`/`writeAndFinish` is called after `finish()`. ## Motivation @@ -144,8 +147,8 @@ of the `EventLoop` family of interfaces. - This allows the lifecycle of the `LambdaRuntime` to be managed with `swift-service-lifecycle` _alongside_ and in the same way the lifecycles of the required services are managed, e.g. `try await ServiceGroup(services: [postgresClient, ..., lambdaRuntime], ...).run()`. -- Dependencies can now be injected into `LambdaRuntime` — `swift-service-lifecycle` guarantees that the services will be - initialized _before_ the `LambdaRuntime`'s `run()` function is called. +- Dependencies can now be injected into `LambdaRuntime`. With `swift-service-lifecycle`, services will be initialized + together with `LambdaRuntime`. - The required services can then be used within the handler in a structured concurrency manner. `swift-service-lifecycle` takes care of listening for termination signals and terminating the services as well as the `LambdaRuntime` in correct order. @@ -196,22 +199,20 @@ below), which is the new base protocol for the `LambdaRuntime` (defined below as ```swift /// A writer object to write the Lambda response stream into -public protocol LambdaResponseStreamWriter: ~Copyable { +public protocol LambdaResponseStreamWriter { /// Write a response part into the stream. The HTTP response is started lazily before the first call to `write(_:)`. /// Bytes written to the writer are streamed continually. func write(_ buffer: ByteBuffer) async throws /// End the response stream and the underlying HTTP response. - consuming func finish() + func finish() async throws /// Write a response part into the stream and end the response stream as well as the underlying HTTP response. - consuming func writeAndFinish(_ buffer: ByteBuffer) async throws + func writeAndFinish(_ buffer: ByteBuffer) async throws } ``` -It is important to note that `LambdaResponseStreamWriter` will be non-copyable, with the `finish()` and -`writeAndFinish(_:)` functions having the `consuming` ownership keyword. This is so that the compiler can enforce the -restriction of not being able to call `write(_:)` after any of the finishing functions have been called. The compiler -can also prevent the finishing functions from being called multiple times. If the user does not call `finish()`, the -library will automatically finish the stream after the last `write` utilizing the writers `deinit`. +If the user does not call `finish()`, the library will automatically finish the stream after the last `write`. +Appropriate errors will be thrown if `finish()` is called multiple times, or if `write`/`writeAndFinish` is called after +`finish()`. ### LambdaContext @@ -278,7 +279,7 @@ public protocol StreamingLambdaHandler { /// headers will be sent. /// - If ``LambdaResponseStreamWriter.finish()`` has already been called before the error is thrown, the /// error will be logged. - mutating func handle(_ event: ByteBuffer, responseWriter: consuming some LambdaResponseStreamWriter, context: LambdaContext) async throws + mutating func handle(_ event: ByteBuffer, responseWriter: some LambdaResponseStreamWriter, context: LambdaContext) async throws } ``` @@ -303,7 +304,7 @@ An implementation that sends the number 1 to 10 every 500ms could look like this struct SendNumbersWithPause: StreamingLambdaHandler { func handle( _ event: ByteBuffer, - responseWriter: consuming some LambdaResponseStreamWriter, + responseWriter: some LambdaResponseStreamWriter, context: LambdaContext ) async throws { for i in 1...10 { @@ -357,12 +358,13 @@ any background work after the result has been sent to the AWS Lambda control pla simply serves as a mechanism to return the output without explicitly returning from the `handle` function. ```swift -public protocol LambdaResponseWriter: ~Copyable { +public protocol LambdaResponseWriter { associatedtype Output /// Sends the generic Output object (representing the computed result of the handler) /// to the AWS Lambda response endpoint. - consuming func write(_: Output) async throws + /// An error will be thrown if this function is called more than once. + func write(_: Output) async throws } public protocol LambdaWithBackgroundProcessingHandler { @@ -377,7 +379,7 @@ public protocol LambdaWithBackgroundProcessingHandler { /// Agnostic to JSON encoding/decoding func handle( _ event: Event, - outputWriter: consuming some LambdaResponseWriter, + outputWriter: some LambdaResponseWriter, context: LambdaContext ) async throws } @@ -400,7 +402,7 @@ struct BackgroundProcessingHandler: LambdaWithBackgroundProcessingHandler { func handle( _ event: Event, - outputWriter: consuming some LambdaResponseWriter, + outputWriter: some LambdaResponseWriter, context: LambdaContext ) async throws { // Return result to the Lambda control plane @@ -460,13 +462,13 @@ public final class LambdaRuntime: ServiceLifecycle.Service, Sendable /// ``NIOSingletons.posixEventLoopGroup``. /// - Parameter logger: A logger public init( - handler: consuming sending Handler, + handler: sending Handler, eventLoop: EventLoop = Lambda.defaultEventLoop, logger: Logger = Logger(label: "Lambda") ) /// Create a LambdaRuntime by passing a ``StreamingLambdaHandler``. - public convenience init(handler: consuming sending Handler) + public convenience init(handler: sending Handler) /// Starts the LambdaRuntime by connecting to the Lambda control plane to ask /// for events to process. If the environment variable AWS_LAMBDA_RUNTIME_API is @@ -569,7 +571,7 @@ public struct LambdaHandlerAdapter< /// 1. Call the `self.handler.handle(...)` with `event` and `context`. /// 2. Pass the generic `Output` object returned from `self.handler.handle(...)` to `outputWriter.write(_:)` - public func handle(_ event: Event, outputWriter: consuming some LambdaResponseWriter, context: LambdaContext) async throws + public func handle(_ event: Event, outputWriter: some LambdaResponseWriter, context: LambdaContext) async throws } ``` @@ -643,7 +645,7 @@ public struct LambdaCodableAdapter< /// and the `LambdaContext`. public mutating func handle( _ request: ByteBuffer, - responseWriter: consuming some LambdaResponseStreamWriter, + responseWriter: some LambdaResponseStreamWriter, context: LambdaContext ) async throws } @@ -721,19 +723,19 @@ handle encoding/decoding themselves: public struct StreamingClosureHandler: StreamingLambdaHandler { public init( - body: @escaping sending (ByteBuffer, consuming LambdaResponseStreamWriter, LambdaContext) async throws -> () + body: @escaping sending (ByteBuffer, LambdaResponseStreamWriter, LambdaContext) async throws -> () ) public func handle( _ request: ByteBuffer, - responseWriter: consuming LambdaResponseStreamWriter, + responseWriter: LambdaResponseStreamWriter, context: LambdaContext ) async throws } extension LambdaRuntime { public init( - body: @escaping sending (ByteBuffer, consuming LambdaResponseStreamWriter, LambdaContext) async throws -> () + body: @escaping sending (ByteBuffer, LambdaResponseStreamWriter, LambdaContext) async throws -> () ) } ``` @@ -766,7 +768,7 @@ Its API would look like this: /// The response can be empty, a single ByteBuffer or a response stream. public struct LambdaResponse { /// A writer to be used when creating a streamed response. - public struct Writer: ~Copyable { + public struct Writer { /// Writes data to the response stream public func write(_ byteBuffer: ByteBuffer) async throws /// Closes off the response stream @@ -840,7 +842,7 @@ For handlers conforming to the `LambdaHandler` protocol, we considered extending add background work can override this function in their `LambdaHandler` conforming object. ```swift -public protocol LambdaHandler: ~Copyable { +public protocol LambdaHandler { associatedtype Event associatedtype Output From ca059f8aeeaaa085f1fcdd2f0486368668187c97 Mon Sep 17 00:00:00 2001 From: aryan-25 Date: Fri, 23 Aug 2024 14:39:27 +0100 Subject: [PATCH 10/10] Revision: Explain why ~Copyable was removed --- .../Documentation.docc/Proposals/0001-v2-api.md | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/Sources/AWSLambdaRuntimeCore/Documentation.docc/Proposals/0001-v2-api.md b/Sources/AWSLambdaRuntimeCore/Documentation.docc/Proposals/0001-v2-api.md index 85fc5901..e4ff259b 100644 --- a/Sources/AWSLambdaRuntimeCore/Documentation.docc/Proposals/0001-v2-api.md +++ b/Sources/AWSLambdaRuntimeCore/Documentation.docc/Proposals/0001-v2-api.md @@ -884,6 +884,19 @@ handler protocols: (with a single call to `writeAndFinish(_:)`). After closing the `LambdaResponseStreamWriter`, any background work can be implemented. +### Making LambdaResponseStreamWriter and LambdaResponseWriter ~Copyable + +We initially proposed to make the `LambdaResponseStreamWriter` and `LambdaResponseWriter` protocols `~Copyable`, with +the functions that close the response having the `consuming` ownership keyword. This was so that the compiler could +enforce the restriction of not being able to interact with the writer after the response stream has closed. + +However, non-copyable types do not compose nicely and add complexity for users. Further, for the compiler to actually +enforce the `consuming` restrictions, user's have to explicitly mark the writer argument as `consuming` in the `handle` +function. + +Therefore, throwing appropriate errors to prevent abnormal interaction with the writers seems to be the simplest +approach. + ## A word about versioning We are aware that AWS Lambda Runtime has not reached a proper 1.0. We intend to keep the current implementation around