Hello :wave:

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!

9 Likes

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.

1 Like

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:

Current example (click to expand)
// main.swift

import AWSLambdaRuntime
import Logging
import ServiceLifecycle

struct Hello: Decodable { var name: String }
struct Goodbye: Encodable { var name: String }

let runtime = LambdaRuntime { (event: Hello, context: LambdaContext) in
	Goodbye(name: event.name)
}

let serviceGroup = ServiceGroup(
    services: [runtime],
    configuration: .init(gracefulShutdownSignals: [.sigterm]),
    logger: Logger(label: “service”)
)
do {
	try await serviceGroup.run()
} catch {
	await Lambda.reportStartupError(error)
}

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:

Proposed example (click to expand)
// main.swift

import AWSLambdaRuntime

struct Hello: Decodable { var name: String }
struct Goodbye: Encodable { var name: String }

await LambdaRuntime.run { (event: Hello, context: LambdaContext) in
	Goodbye(name: event.name)
}

I think this is achievable while maintaining the goal of making this integrate well with ServiceLifecycle. I would do this in a few steps:

  1. Remove the conformance of LambdaRuntime to Service.
  2. 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.
  3. 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)
}

Hi @willft,

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!