From 12f5cb74987ea22dad61621669885d0ee01184bc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A9bastien=20Stormacq?= Date: Sat, 26 Oct 2024 14:40:09 +0200 Subject: [PATCH 1/5] add an example for background tasks --- Examples/BackgroundTasks/.gitignore | 8 ++ Examples/BackgroundTasks/Package.swift | 60 +++++++++ Examples/BackgroundTasks/README.md | 116 ++++++++++++++++++ Examples/BackgroundTasks/Sources/main.swift | 53 ++++++++ Sources/AWSLambdaRuntime/Lambda+Codable.swift | 8 +- 5 files changed, 241 insertions(+), 4 deletions(-) create mode 100644 Examples/BackgroundTasks/.gitignore create mode 100644 Examples/BackgroundTasks/Package.swift create mode 100644 Examples/BackgroundTasks/README.md create mode 100644 Examples/BackgroundTasks/Sources/main.swift diff --git a/Examples/BackgroundTasks/.gitignore b/Examples/BackgroundTasks/.gitignore new file mode 100644 index 00000000..0023a534 --- /dev/null +++ b/Examples/BackgroundTasks/.gitignore @@ -0,0 +1,8 @@ +.DS_Store +/.build +/Packages +xcuserdata/ +DerivedData/ +.swiftpm/configuration/registries.json +.swiftpm/xcode/package.xcworkspace/contents.xcworkspacedata +.netrc diff --git a/Examples/BackgroundTasks/Package.swift b/Examples/BackgroundTasks/Package.swift new file mode 100644 index 00000000..84ca71ad --- /dev/null +++ b/Examples/BackgroundTasks/Package.swift @@ -0,0 +1,60 @@ +// swift-tools-version:6.0 + +import PackageDescription + +// needed for CI to test the local version of the library +import struct Foundation.URL + +#if os(macOS) +let platforms: [PackageDescription.SupportedPlatform]? = [.macOS(.v15)] +#else +let platforms: [PackageDescription.SupportedPlatform]? = nil +#endif + +let package = Package( + name: "swift-aws-lambda-runtime-example", + platforms: platforms, + products: [ + .executable(name: "BackgroundTasks", targets: ["BackgroundTasks"]) + ], + dependencies: [ + // during CI, the dependency on local version of swift-aws-lambda-runtime is added dynamically below + .package(url: "https://github.com/swift-server/swift-aws-lambda-runtime.git", branch: "main") + ], + targets: [ + .executableTarget( + name: "BackgroundTasks", + dependencies: [ + .product(name: "AWSLambdaRuntime", package: "swift-aws-lambda-runtime") + ], + path: "." + ) + ] +) + +if let localDepsPath = Context.environment["LAMBDA_USE_LOCAL_DEPS"], + localDepsPath != "", + let v = try? URL(fileURLWithPath: localDepsPath).resourceValues(forKeys: [.isDirectoryKey]), + v.isDirectory == true +{ + // when we use the local runtime as deps, let's remove the dependency added above + let indexToRemove = package.dependencies.firstIndex { dependency in + if case .sourceControl( + name: _, + location: "https://github.com/swift-server/swift-aws-lambda-runtime.git", + requirement: _ + ) = dependency.kind { + return true + } + return false + } + if let indexToRemove { + package.dependencies.remove(at: indexToRemove) + } + + // then we add the dependency on LAMBDA_USE_LOCAL_DEPS' path (typically ../..) + print("[INFO] Compiling against swift-aws-lambda-runtime located at \(localDepsPath)") + package.dependencies += [ + .package(name: "swift-aws-lambda-runtime", path: localDepsPath) + ] +} diff --git a/Examples/BackgroundTasks/README.md b/Examples/BackgroundTasks/README.md new file mode 100644 index 00000000..64e86d1e --- /dev/null +++ b/Examples/BackgroundTasks/README.md @@ -0,0 +1,116 @@ +# Background Tasks + +This is an example for running background tasks in an AWS Lambda function. + +Background tasks allow code to execute asynchronously after the main response has been returned, enabling additional processing without affecting response latency. This approach is ideal for scenarios like logging, data updates, or notifications that can be deferred. The code leverages Lambda's "Response Streaming" feature, which is effective for balancing real-time user responsiveness with the ability to perform extended tasks post-response. + +For more information about Lambda background tasks, see [this AWS blog post](https://aws.amazon.com/blogs/compute/running-code-after-returning-a-response-from-an-aws-lambda-function/). + +## Code + +The sample code creates a `BackgroundProcessingHandler` struct that conforms to the `LambdaWithBackgroundProcessingHandler` protocol provided by the Swift AWS Lambda Runtime. + +The `BackgroundProcessingHandler` struct defines the input and output JSON received and returned by the Handler. + +The `handle(...)` method of this protocol receives incoming events as `Input` and returns the output as a `Greeting`. The `handle(...)` methods receives an `outputWriter` parameter to write the output before the function returns, giving some opportunities to run long-lasting tasks after the response has been returned to the client but before the function returns. + +The `handle(...)` method uses the `outputWriter` to return the response as soon as possible. It then waits for 10 seconds to simulate a long background work. When the 10 seconds elapsed, the function returns. The billing cycle ends when the function returns. + +The `handle(...)` method is marked as `mutating` to allow handlers to be implemented with a `struct`. + +Once the struct is created and the `handle(...)` method is defined, the sample code creates a `LambdaCodableAdapter` adapter to adapt the `LambdaWithBackgroundProcessingHandler` to a type accepted by the `LambdaRuntime` struct. Then, the sample code initializes the `LambdaRuntime` with the adapter just created. Finally, the code calls `run()` to start the interaction with the AWS Lambda control plane. + +## Build & Package + +To build & archive the package, type the following commands. + +```bash +swift package archive --allow-network-connections docker +``` + +If there is no error, there is a ZIP file ready to deploy. +The ZIP file is located at `.build/plugins/AWSLambdaPackager/outputs/AWSLambdaPackager/BackgroundTasks/BackgroundTasks.zip` + +## Deploy with the AWS CLI + +Here is how to deploy using the `aws` command line. + +### Create the function +```bash +AWS_ACCOUNT_ID=012345678901 +aws lambda create-function \ +--function-name BackgroundTasks \ +--zip-file fileb://.build/plugins/AWSLambdaPackager/outputs/AWSLambdaPackager/BackgroundTasks/BackgroundTasks.zip \ +--runtime provided.al2 \ +--handler provided \ +--architectures arm64 \ +--role arn:aws:iam::${AWS_ACCOUNT_ID}:role/lambda_basic_execution \ +--environment "Variables={LOG_LEVEL=debug}" \ +--timeout 15 +``` + +> [!IMPORTANT] +> The timeout value must be bigger than the time it takes for your function to complete its background tasks. Otherwise, the Lambda control plane will terminate the execution environment before your code has a chance to finish the tasks. Here, the sample function waits for 10 seconds and we set the timeout for 15 seconds. + +The `--environment` arguments sets the `LOG_LEVEL` environment variable to `debug`. This will ensure the debugging statements in the handler `context.logger.debug("...")` are printed in the Lambda function logs. + +The `--architectures` flag is only required when you build the binary on an Apple Silicon machine (Apple M1 or more recent). It defaults to `x64`. + +Be sure to set `AWS_ACCOUNT_ID` with your actual AWS account ID (for example: 012345678901). + +### Invoke your Lambda function + +To invoke the Lambda function, use `aws` command line. +```bash +aws lambda invoke \ + --function-name BackgroundTasks \ + --cli-binary-format raw-in-base64-out \ + --payload '{ "message" : "Hello Background Tasks" }' \ + response.json +``` + +This should immediately output the following result. + +``` +{ + "StatusCode": 200, + "ExecutedVersion": "$LATEST" +} +``` + +The response is visible in the `response.json` file. + +```bash +cat response.json +{"echoedMessage":"Hello Background Tasks"} +``` + +You can observe additional messages being logged after the response is received. + +To tail the log, use the AWS CLI: +```bash +aws logs tail /aws/lambda/BackgroundTasks --follow +``` + +This produces an output like: +```text +INIT_START Runtime Version: provided:al2.v59 Runtime Version ARN: arn:aws:lambda:us-east-1::runtime:974c4a90f22278a2ef1c3f53c5c152167318aaf123fbb07c055a4885a4e97e52 +START RequestId: 4c8edd74-d776-4df9-9714-19086ab59bfd Version: $LATEST +debug LambdaRuntime : [BackgroundTasks] BackgroundProcessingHandler - message received +debug LambdaRuntime : [BackgroundTasks] BackgroundProcessingHandler - response sent. Performing background tasks. +debug LambdaRuntime : [BackgroundTasks] BackgroundProcessingHandler - Background tasks completed. Returning +END RequestId: 4c8edd74-d776-4df9-9714-19086ab59bfd +REPORT RequestId: 4c8edd74-d776-4df9-9714-19086ab59bfd Duration: 10160.89 ms Billed Duration: 10250 ms Memory Size: 128 MB Max Memory Used: 27 MB Init Duration: 88.20 ms +``` +> [!NOTE] +> The `debug` message are sent by the code inside the `handler()` function. Note that the `Duration` and `Billed Duration` on the last line are for 10.1 and 10.2 seconds respectively. + +Type CTRL-C to stop tailing the logs. + +### Undeploy + +When done testing, you can delete the Lambda function with this command. + +```bash +aws lambda delete-function --function-name BackgroundTasks +``` \ No newline at end of file diff --git a/Examples/BackgroundTasks/Sources/main.swift b/Examples/BackgroundTasks/Sources/main.swift new file mode 100644 index 00000000..b0c84b7a --- /dev/null +++ b/Examples/BackgroundTasks/Sources/main.swift @@ -0,0 +1,53 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the SwiftAWSLambdaRuntime open source project +// +// Copyright (c) 2024 Apple Inc. and the SwiftAWSLambdaRuntime project authors +// Licensed under Apache License v2.0 +// +// See LICENSE.txt for license information +// See CONTRIBUTORS.txt for the list of SwiftAWSLambdaRuntime project authors +// +// SPDX-License-Identifier: Apache-2.0 +// +//===----------------------------------------------------------------------===// + +import AWSLambdaRuntime +import NIOCore +import Foundation + +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: some LambdaResponseWriter, + context: LambdaContext + ) async throws { + // Return result to the Lambda control plane + context.logger.debug("BackgroundProcessingHandler - message received") + try await outputWriter.write(Greeting(echoedMessage: event.message)) + + // Perform some background work, e.g: + context.logger.debug("BackgroundProcessingHandler - response sent. Performing background tasks.") + 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. + context.logger.debug("BackgroundProcessingHandler - Background tasks completed. Returning") + return + } +} + +let adapter = LambdaCodableAdapter(handler: BackgroundProcessingHandler()) +let runtime = LambdaRuntime.init(handler: adapter) +try await runtime.run() diff --git a/Sources/AWSLambdaRuntime/Lambda+Codable.swift b/Sources/AWSLambdaRuntime/Lambda+Codable.swift index 5dffbdfe..fb6d2ca7 100644 --- a/Sources/AWSLambdaRuntime/Lambda+Codable.swift +++ b/Sources/AWSLambdaRuntime/Lambda+Codable.swift @@ -60,12 +60,12 @@ public struct LambdaJSONOutputEncoder: LambdaOutputEncoder { extension LambdaCodableAdapter { /// Initializes an instance given an encoder, decoder, and a handler with a non-`Void` output. /// - Parameters: - /// - encoder: The encoder object that will be used to encode the generic `Output` obtained from the `handler`'s `outputWriter` into a `ByteBuffer`. - /// - decoder: The decoder object that will be used to decode the received `ByteBuffer` event into the generic `Event` type served to the `handler`. + /// - encoder: The encoder object that will be used to encode the generic `Output` obtained from the `handler`'s `outputWriter` into a `ByteBuffer`. By default, a JSONEncoder is used. + /// - decoder: The decoder object that will be used to decode the received `ByteBuffer` event into the generic `Event` type served to the `handler`. By default, a JSONDecoder is used. /// - handler: The handler object. public init( - encoder: JSONEncoder, - decoder: JSONDecoder, + encoder: JSONEncoder = JSONEncoder(), + decoder: JSONDecoder = JSONDecoder(), handler: Handler ) where From 7b11e92fa90185143d9fbcbf9cb1fa09201fe88f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A9bastien=20Stormacq?= Date: Sat, 26 Oct 2024 14:46:32 +0200 Subject: [PATCH 2/5] swift-format --- Examples/BackgroundTasks/Sources/main.swift | 1 - 1 file changed, 1 deletion(-) diff --git a/Examples/BackgroundTasks/Sources/main.swift b/Examples/BackgroundTasks/Sources/main.swift index b0c84b7a..929e676a 100644 --- a/Examples/BackgroundTasks/Sources/main.swift +++ b/Examples/BackgroundTasks/Sources/main.swift @@ -13,7 +13,6 @@ //===----------------------------------------------------------------------===// import AWSLambdaRuntime -import NIOCore import Foundation struct BackgroundProcessingHandler: LambdaWithBackgroundProcessingHandler { From 199b427bf739a13eeca5f2017b6a34fae7eeb4bf Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A9bastien=20Stormacq?= Date: Sat, 26 Oct 2024 14:47:30 +0200 Subject: [PATCH 3/5] add CI --- .github/workflows/pull_request.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/pull_request.yml b/.github/workflows/pull_request.yml index 5e9a8fc5..8481e02c 100644 --- a/.github/workflows/pull_request.yml +++ b/.github/workflows/pull_request.yml @@ -35,7 +35,7 @@ jobs: # We pass the list of examples here, but we can't pass an array as argument # Instead, we pass a String with a valid JSON array. # The workaround is mentioned here https://github.com/orgs/community/discussions/11692 - examples: "[ 'HelloWorld', 'APIGateway','S3_AWSSDK', 'S3_Soto' ]" + examples: "[ 'HelloWorld', 'APIGateway','S3_AWSSDK', 'S3_Soto', 'BackgroundTasks' ]" archive_plugin_enabled: true From 71472ce1a53cb1d3f1128c9a5751704591afd285 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A9bastien=20Stormacq?= Date: Sat, 26 Oct 2024 15:00:34 +0200 Subject: [PATCH 4/5] add background task section in the main readme --- readme.md | 49 +++++++++++++++++++++++++++++++++++++++++++++++-- 1 file changed, 47 insertions(+), 2 deletions(-) diff --git a/readme.md b/readme.md index ca8097e8..3b7d4685 100644 --- a/readme.md +++ b/readme.md @@ -160,6 +160,51 @@ tbd + link to docc tbd + link to docc -### Background Tasks +### Use Lambda Background Tasks -tbd + link to docc +Background tasks allow code to execute asynchronously after the main response has been returned, enabling additional processing without affecting response latency. This approach is ideal for scenarios like logging, data updates, or notifications that can be deferred. The code leverages Lambda's "Response Streaming" feature, which is effective for balancing real-time user responsiveness with the ability to perform extended tasks post-response. For more information about Lambda background tasks, see [this AWS blog post](https://aws.amazon.com/blogs/compute/running-code-after-returning-a-response-from-an-aws-lambda-function/). + + +Here is an example of a minimal function that waits 10 seconds after it returned a response but before the handler returns. +```swift +import AWSLambdaRuntime +import Foundation + +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: some LambdaResponseWriter, + context: LambdaContext + ) async throws { + // Return result to the Lambda control plane + context.logger.debug("BackgroundProcessingHandler - message received") + try await outputWriter.write(Greeting(echoedMessage: event.message)) + + // Perform some background work, e.g: + context.logger.debug("BackgroundProcessingHandler - response sent. Performing background tasks.") + 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. + context.logger.debug("BackgroundProcessingHandler - Background tasks completed. Returning") + return + } +} + +let adapter = LambdaCodableAdapter(handler: BackgroundProcessingHandler()) +let runtime = LambdaRuntime.init(handler: adapter) +try await runtime.run() +``` + +You can learn how to deploy and invoke this function in [the example README file](Examples/BackgroundTasks/README.md). \ No newline at end of file From c7ac7d2a326311c9cfd078f564c0a990e71bb4ac Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A9bastien=20Stormacq?= Date: Sat, 26 Oct 2024 15:03:19 +0200 Subject: [PATCH 5/5] minor formatting changes --- Examples/BackgroundTasks/README.md | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/Examples/BackgroundTasks/README.md b/Examples/BackgroundTasks/README.md index 64e86d1e..e1bf0ddd 100644 --- a/Examples/BackgroundTasks/README.md +++ b/Examples/BackgroundTasks/README.md @@ -35,7 +35,8 @@ The ZIP file is located at `.build/plugins/AWSLambdaPackager/outputs/AWSLambdaPa Here is how to deploy using the `aws` command line. -### Create the function +### Create the function + ```bash AWS_ACCOUNT_ID=012345678901 aws lambda create-function \ @@ -85,6 +86,8 @@ cat response.json {"echoedMessage":"Hello Background Tasks"} ``` +### View the function's logs + You can observe additional messages being logged after the response is received. To tail the log, use the AWS CLI: @@ -107,7 +110,7 @@ REPORT RequestId: 4c8edd74-d776-4df9-9714-19086ab59bfd Duration: 10160.89 ms Type CTRL-C to stop tailing the logs. -### Undeploy +## Cleanup When done testing, you can delete the Lambda function with this command.