Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Migrating Swift AWS lambdas to AsyncLambdaHandler #29

Open
eneko opened this issue Jun 21, 2021 · 0 comments
Open

Migrating Swift AWS lambdas to AsyncLambdaHandler #29

eneko opened this issue Jun 21, 2021 · 0 comments

Comments

@eneko
Copy link
Owner

eneko commented Jun 21, 2021

Last December I started using AWS lambda as part of the engine for my blog. Just for fun, because both Lambda and Swift on the server are pretty cool and fun to work with (eg. no need to wait for 2 years to use async/await 😉).

I'm going to be writing this as I go through the code updates, so this article will look more like a notepad than a step-by-step guide.

Let's get started

First step is to update Package.swift to pull the latest from main.

    dependencies: [
        .package(url: "https://github.com/swift-server/swift-aws-lambda-runtime", .branch("main")), //from: "0.3.0"),
        ....
    ]

This will ensure I get the unreleased async/await updates.

Async updates

The existing code had a lambda handler with a closure-based method. The lambda uses to agents to process messages from SQS. First, each message is decoded with IssueParser, then processed with IssueProcessor.

struct Handler: LambdaHandler {
    typealias In = SQS.Event
    typealias Out = String // Response type

    let parser: IssueParser
    let processor: IssueProcessor

    /// Business logic is initialized during lambda cold start
    /// - Parameter context: Lambda initialization context, provided by AWS
    init(context: Lambda.InitializationContext) {
        parser = IssueParser(logger: context.logger)
        processor = IssueProcessor(logger: context.logger)
    }

    func handle(context: Lambda.Context, event: In, callback: @escaping (Result<Out, Error>) -> Void) {
        do {
            let group = DispatchGroup()
            for message in event.records {
                group.enter()
                let githubContext = try parser.parseContext(json: message.body)
                try processor.process(githubEvent: githubContext.event) {
                    group.leave()
                }
            }
            group.wait()
            callback(.success(""))
        }
        catch {
            callback(.failure(error))
        }
    }
}

IssueParser is fully synchronous, since it consists mostly in decoding a JSON payload.

IssueProcessor does an HTTP call to trigger a GitHub Action in my eneko.github.com repository. This is a perfect candidate for converting to an async/await call.

So far, I've updated my lambda function to conform to AsyncLambdaHandler, and replaced the closure-based handler method with the new async one.

New code:

@available(macOS 9999, iOS 9999, watchOS 9999, tvOS 9999, *)
struct Handler: AsyncLambdaHandler {
    typealias In = SQS.Event
    typealias Out = String // Response type

    let parser: IssueParser
    let processor: IssueProcessor

    /// Business logic is initialized during lambda cold start
    /// - Parameter context: Lambda initialization context, provided by AWS
    init(context: Lambda.InitializationContext) {
        parser = IssueParser(logger: context.logger)
        processor = IssueProcessor(logger: context.logger)
    }

    func handle(event: SQS.Event, context: Lambda.Context) async throws -> Out {
        for message in event.records {
            let githubContext = try parser.parseContext(json: message.body)
            try await processor.process(githubEvent: githubContext.event)
        }
        return ""
    }
}

The handler method is much cleaner already, but I'm haven't updated IssueProcessor yet, so the above does not yet compile.

Note I'm doing updates from the top to bottom. I think this will be easier for this small lambda.

Updating network calls to async/await

For this network request, I was making a network call and checking for success, discarding any response data received from the server. Completion blocks had no parameters. Logs would be logged, but otherwise ignored.

let dataTask = URLSession.shared.dataTask(with: request) { data, _, error in
            if let error = error {
                self.logger.error("Failed to submit workflow dispatch request to GitHub Actions")
                self.logger.error("\(error.localizedDescription)")
            } else {
                self.logger.debug("Submitted workflow dispatch request to GitHub Actions")
            }
            completion()
        }
        dataTask.resume()

Updating this code to async/await makes it easier to handle those errors, and removes those completion blocks, which makes the code very linear.

        do {
            _ = try await URLSession.shared.data(for: request, delegate: nil)
            logger.debug("Submitted workflow dispatch request to GitHub Actions")
        }
        catch {
            logger.error("Failed to submit workflow dispatch request to GitHub Actions")
            logger.error("\(error.localizedDescription)")
        }

Here, I could re-throw captured errors, but for now I'll leave the logic as it was before.

Building the Lambda for Linux

The latest Swift version released as of today is 5.4.1, which requires compiler flags to enable async/await.

First, we update the docker image to FROM swift:5.4.1-amazonlinux2

Second, we add the compiler flags to Package.swift

        .target(
            name: "IssueProcessorLambda",
            dependencies: [
                "Blog",
                "IssueParser",
                .product(name: "AWSLambdaRuntime", package: "swift-aws-lambda-runtime"),
                .product(name: "AWSLambdaEvents", package: "swift-aws-lambda-runtime"),
            ],
            swiftSettings: [
                .unsafeFlags([
                    "-Xfrontend",
                    "-enable-experimental-concurrency"
                ])
            ]
        ),
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

No branches or pull requests

1 participant