I am Aryan, a Computer Science student and currently an intern at Apple on the Swift on Server team. As part of my internship, I have been working with @fabianfett to propose a new v2 API for the swift-aws-lambda-runtime library.
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 constructs instead of EventLoop interfaces. For this, large parts of the current API have to be reconsidered. This also provides a good opportunity to add support for new AWS Lambda features such as response streaming.
We have written a document that explains the current limitations of the library and proposes a detailed design for the v2 API. The document can be found on GitHub.
Please read the proposal and voice your opinions here or on the PR. We are looking forward to your feedback!
And please consider taking steps to reduce the resulting package size (if you haven't already) to improve cold start times. Maybe adopt swift-embedded if appropriate.
While I agree that binary size is important adopting Embedded Swift is a way bigger undertaking. swift-aws-lambda-runtime relies on other packages such as swift-nio and adopting the Embedded Swift mode in such packages is way harder and not necessarily something that we want to do in NIO.
Now that doesn't mean we shouldn't try to reduce the binary size in other ways. The Embedded mode has brought a lot of other compiler/linker optimisations that help with binary size that we could take advantage of regardless if we are using the Embedded Swift mode.
Thanks for working on this! I’m excited to see this new API take shape. This will be a great update! I do have some comments though.
I’m bummed to lose the ability for the library to automatically handle initialization errors. This is a real potential stumbling block for new users. And I'm 100% sure that I personally will forget to do the reportInitializationError dance at some point, since there are no warnings/errors for missing it. I think we should strongly consider what can be done to fix this. It’s a bit of a tricky problem if you want to get rid of structs with @main. I don’t have a good solution for that.
Is there an end-to-end example of what a basic Lambda with JSON Codable support would look like? My guess from the document is something like this:
My main concern about this is that IMO a Hello World lambda shouldn’t require answering “what graceful shutdown signals do I want.” It’s a simple enough question, but since we’re targeting a known environment (AWS Lambda) we should be able to at least provide a good default (just SIGTERM, I think). Users shouldn’t need to know the details of the Lambda environment & what signals it sends for graceful shutdown. I also think we should probably have a default Logger here.
The flexibility afforded by conforming LambdaRuntime to Service is great. But compared to Hello World examples in Rust, Go, and Python, our Hello World has the user making a bunch more decisions (and being exposed to more concepts) than they really need to. Progressive disclosure of complexity is important here.
It would be really great if the Hello World could look a little more like this:
I think this is achievable while maintaining the goal of making this integrate well with ServiceLifecycle. I would do this in a few steps:
Remove the conformance of LambdaRuntime to Service.
Add a property to LambdaRuntime, called service that vends a wrapper around LambdaRuntime that conforms to Service. This wrapper allows for complex customization like in the original, but reserves it away for those who need it. This also opens up the name run for our simplified entrypoint without confusing users.
Add a new static method run<In, Out>(with additionalServices: [any Service] = [], handler: (In, LambdaContext) -> Out) to LambdaRuntime. This would just set up a ServiceGroup with the default configuration & logger, then report initialization errors on failure. The services would be additionalServices + [runtime].
More complex proposed example (click to expand)
// main.swift
import AWSLambdaRuntime
struct Hello: Decodable { var name: String }
struct Goodbye: Encodable { var name: String }
let client = PostgresClient() // Defined elsewhere
await LambdaRuntime.run(with: [client]) { (event: Hello, context: LambdaContext) in
try await client.insertHello(event)
context.logger.info("Saved important Hello message")
return try await client.selectGoodbye(for: event.name)
}
thanks for your thoughtful feedback. Let me address your concerns:
We think it should look something like this (and we should likely make this explicit in the proposal):
// main.swift
import AWSLambdaRuntime
struct Hello: Decodable { var name: String }
struct Goodbye: Encodable { var name: String }
let runtime = LambdaRuntime { (event: Hello, context: LambdaContext) in
Goodbye(name: event.name)
}
try await runtime.run()
However this would not have any interruption handling (which is not needed when the Lambda runs at AWS anyway).
Do you think this would be easy enough?
I want to call out, that you don't have to use ServiceLifecycle with the LambdaRuntime. ServiceLifecycle makes sense for users that have dependencies that also need to be run or shutdown. But you can just call the required run() method from the Service protocol directly and everything will work fine (except for interruption handling in local debugging).
I'm not sure I agree here. See above. We need a run() method anyway. And we already have the implementation for it.
I think we should focus on integrating with the ecosystem here, why create a new spelling, if there already is a spelling that works and that the ecosystem has agreed on? Lambda should work in the same way Vapor and Hummingbird integrate with ServiceLifecycle instead of inventing its own approach.
This is an interesting view. One thing that I could see is something like this:
/// A protocol that vends a `main` function, that sets
/// up listening for interruptions and reports startup
/// errors if needed and then calls the provided run
/// method.
protocol LambdaRunner {
static func run() async throws
}
Potential usage:
@main
enum MyLambda: LambdaRunner {
struct Hello: Decodable { var name: String }
struct Goodbye: Encodable { var name: String }
// interruption handling and startup error reporting is automatically
// added for you in the enclosing main().
static func run() async throws {
let runtime = LambdaRuntime { (event: Hello, context: LambdaContext) in
Goodbye(name: event.name)
}
try await runtime.run()
}
}
@willft would this solve your concerns? Let us know what you think! Thanks!
Out of curiosity, is there a possible approach to follow ArgumentParser API style? Maybe is because I’ve used it a lot but that feels very stylistic for Swift code.
Such an approach can work. However it runs into extendability issues really fast. We suffer those extendability constraints in Lambda v1. (See proposal for more details). For this reason this is not an approach that we want to pursue in v2. As a user you are of course free to write this API on top of the API that the Lambda Runtime package provides/will provide.
This API looks like exactly what I want. Your call out here is exactly what I was misunderstanding about this API. In this case, I’m very happy! I’m only just getting into switching my Swift-based services over to ServiceLifecycle, so I maybe have less understanding of this library than I thought I did!
Since interruption handling isn’t needed when the Lambda runs on AWS, I see no reason I’d want it when running locally either. So just sounds great!
Having rectified the above misunderstanding, I agree. I wanted the separate run() method so that we could hide away the run() method I thought needed to cooperate with ServiceLifecycle. Since that’s not the case, no need to jump through all these hoops. Much simpler as originally proposed.
My only reservation is that I don’t understand whether it will be very common to need libraries which integrate with ServiceLifecycle. My intuition is no — that most Lambdas just talk to AWS services & that neither AWS Swift SDK that I’m aware of uses ServiceLifecycle. In that case, I’m very happy. I think people who are accessing, e.g., a Postgres database in a Lambda are already at a place where they are either likely to understand ServiceLifecycle already (if they’ve written something else with Postgres in Swift) or are already jumping through a few extra hoops (like setting up pgbouncer/RDS proxy).
To be clear here: it’s not that I want to hide ServiceLifecycle entirely. I just want to make sure that the initial ramp up for writing lambdas in Swift is very simple for people who are already Swift programmers, the way that def handler(event, context): … is for Python users — so simple you don’t feel like you’re learning something new. A bonus would be non-Swift programmers finding this an accessible way to learn.
@fabianfett What are the consequences of failing to report errors at startup? Are we still able to get a print-out of the error in the logs, or is it just Status: error Error Type: Runtime.ExitError? Or something worse?
I tried poking through the AWS custom runtime docs on this, but I didn’t find a satisfactory answer.
I'm trying to sort out my thoughts on your LambdaRunner idea