Skip to content

Commit 12f5cb7

Browse files
committed
add an example for background tasks
1 parent 4992ba5 commit 12f5cb7

File tree

5 files changed

+241
-4
lines changed

5 files changed

+241
-4
lines changed

Examples/BackgroundTasks/.gitignore

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
.DS_Store
2+
/.build
3+
/Packages
4+
xcuserdata/
5+
DerivedData/
6+
.swiftpm/configuration/registries.json
7+
.swiftpm/xcode/package.xcworkspace/contents.xcworkspacedata
8+
.netrc
Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
// swift-tools-version:6.0
2+
3+
import PackageDescription
4+
5+
// needed for CI to test the local version of the library
6+
import struct Foundation.URL
7+
8+
#if os(macOS)
9+
let platforms: [PackageDescription.SupportedPlatform]? = [.macOS(.v15)]
10+
#else
11+
let platforms: [PackageDescription.SupportedPlatform]? = nil
12+
#endif
13+
14+
let package = Package(
15+
name: "swift-aws-lambda-runtime-example",
16+
platforms: platforms,
17+
products: [
18+
.executable(name: "BackgroundTasks", targets: ["BackgroundTasks"])
19+
],
20+
dependencies: [
21+
// during CI, the dependency on local version of swift-aws-lambda-runtime is added dynamically below
22+
.package(url: "https://github.com/swift-server/swift-aws-lambda-runtime.git", branch: "main")
23+
],
24+
targets: [
25+
.executableTarget(
26+
name: "BackgroundTasks",
27+
dependencies: [
28+
.product(name: "AWSLambdaRuntime", package: "swift-aws-lambda-runtime")
29+
],
30+
path: "."
31+
)
32+
]
33+
)
34+
35+
if let localDepsPath = Context.environment["LAMBDA_USE_LOCAL_DEPS"],
36+
localDepsPath != "",
37+
let v = try? URL(fileURLWithPath: localDepsPath).resourceValues(forKeys: [.isDirectoryKey]),
38+
v.isDirectory == true
39+
{
40+
// when we use the local runtime as deps, let's remove the dependency added above
41+
let indexToRemove = package.dependencies.firstIndex { dependency in
42+
if case .sourceControl(
43+
name: _,
44+
location: "https://github.com/swift-server/swift-aws-lambda-runtime.git",
45+
requirement: _
46+
) = dependency.kind {
47+
return true
48+
}
49+
return false
50+
}
51+
if let indexToRemove {
52+
package.dependencies.remove(at: indexToRemove)
53+
}
54+
55+
// then we add the dependency on LAMBDA_USE_LOCAL_DEPS' path (typically ../..)
56+
print("[INFO] Compiling against swift-aws-lambda-runtime located at \(localDepsPath)")
57+
package.dependencies += [
58+
.package(name: "swift-aws-lambda-runtime", path: localDepsPath)
59+
]
60+
}

Examples/BackgroundTasks/README.md

Lines changed: 116 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,116 @@
1+
# Background Tasks
2+
3+
This is an example for running background tasks in an AWS Lambda function.
4+
5+
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.
6+
7+
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/).
8+
9+
## Code
10+
11+
The sample code creates a `BackgroundProcessingHandler` struct that conforms to the `LambdaWithBackgroundProcessingHandler` protocol provided by the Swift AWS Lambda Runtime.
12+
13+
The `BackgroundProcessingHandler` struct defines the input and output JSON received and returned by the Handler.
14+
15+
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.
16+
17+
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.
18+
19+
The `handle(...)` method is marked as `mutating` to allow handlers to be implemented with a `struct`.
20+
21+
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.
22+
23+
## Build & Package
24+
25+
To build & archive the package, type the following commands.
26+
27+
```bash
28+
swift package archive --allow-network-connections docker
29+
```
30+
31+
If there is no error, there is a ZIP file ready to deploy.
32+
The ZIP file is located at `.build/plugins/AWSLambdaPackager/outputs/AWSLambdaPackager/BackgroundTasks/BackgroundTasks.zip`
33+
34+
## Deploy with the AWS CLI
35+
36+
Here is how to deploy using the `aws` command line.
37+
38+
### Create the function
39+
```bash
40+
AWS_ACCOUNT_ID=012345678901
41+
aws lambda create-function \
42+
--function-name BackgroundTasks \
43+
--zip-file fileb://.build/plugins/AWSLambdaPackager/outputs/AWSLambdaPackager/BackgroundTasks/BackgroundTasks.zip \
44+
--runtime provided.al2 \
45+
--handler provided \
46+
--architectures arm64 \
47+
--role arn:aws:iam::${AWS_ACCOUNT_ID}:role/lambda_basic_execution \
48+
--environment "Variables={LOG_LEVEL=debug}" \
49+
--timeout 15
50+
```
51+
52+
> [!IMPORTANT]
53+
> 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.
54+
55+
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.
56+
57+
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`.
58+
59+
Be sure to set `AWS_ACCOUNT_ID` with your actual AWS account ID (for example: 012345678901).
60+
61+
### Invoke your Lambda function
62+
63+
To invoke the Lambda function, use `aws` command line.
64+
```bash
65+
aws lambda invoke \
66+
--function-name BackgroundTasks \
67+
--cli-binary-format raw-in-base64-out \
68+
--payload '{ "message" : "Hello Background Tasks" }' \
69+
response.json
70+
```
71+
72+
This should immediately output the following result.
73+
74+
```
75+
{
76+
"StatusCode": 200,
77+
"ExecutedVersion": "$LATEST"
78+
}
79+
```
80+
81+
The response is visible in the `response.json` file.
82+
83+
```bash
84+
cat response.json
85+
{"echoedMessage":"Hello Background Tasks"}
86+
```
87+
88+
You can observe additional messages being logged after the response is received.
89+
90+
To tail the log, use the AWS CLI:
91+
```bash
92+
aws logs tail /aws/lambda/BackgroundTasks --follow
93+
```
94+
95+
This produces an output like:
96+
```text
97+
INIT_START Runtime Version: provided:al2.v59 Runtime Version ARN: arn:aws:lambda:us-east-1::runtime:974c4a90f22278a2ef1c3f53c5c152167318aaf123fbb07c055a4885a4e97e52
98+
START RequestId: 4c8edd74-d776-4df9-9714-19086ab59bfd Version: $LATEST
99+
debug LambdaRuntime : [BackgroundTasks] BackgroundProcessingHandler - message received
100+
debug LambdaRuntime : [BackgroundTasks] BackgroundProcessingHandler - response sent. Performing background tasks.
101+
debug LambdaRuntime : [BackgroundTasks] BackgroundProcessingHandler - Background tasks completed. Returning
102+
END RequestId: 4c8edd74-d776-4df9-9714-19086ab59bfd
103+
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
104+
```
105+
> [!NOTE]
106+
> 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.
107+
108+
Type CTRL-C to stop tailing the logs.
109+
110+
### Undeploy
111+
112+
When done testing, you can delete the Lambda function with this command.
113+
114+
```bash
115+
aws lambda delete-function --function-name BackgroundTasks
116+
```
Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
//===----------------------------------------------------------------------===//
2+
//
3+
// This source file is part of the SwiftAWSLambdaRuntime open source project
4+
//
5+
// Copyright (c) 2024 Apple Inc. and the SwiftAWSLambdaRuntime project authors
6+
// Licensed under Apache License v2.0
7+
//
8+
// See LICENSE.txt for license information
9+
// See CONTRIBUTORS.txt for the list of SwiftAWSLambdaRuntime project authors
10+
//
11+
// SPDX-License-Identifier: Apache-2.0
12+
//
13+
//===----------------------------------------------------------------------===//
14+
15+
import AWSLambdaRuntime
16+
import NIOCore
17+
import Foundation
18+
19+
struct BackgroundProcessingHandler: LambdaWithBackgroundProcessingHandler {
20+
struct Input: Decodable {
21+
let message: String
22+
}
23+
24+
struct Greeting: Encodable {
25+
let echoedMessage: String
26+
}
27+
28+
typealias Event = Input
29+
typealias Output = Greeting
30+
31+
func handle(
32+
_ event: Event,
33+
outputWriter: some LambdaResponseWriter<Output>,
34+
context: LambdaContext
35+
) async throws {
36+
// Return result to the Lambda control plane
37+
context.logger.debug("BackgroundProcessingHandler - message received")
38+
try await outputWriter.write(Greeting(echoedMessage: event.message))
39+
40+
// Perform some background work, e.g:
41+
context.logger.debug("BackgroundProcessingHandler - response sent. Performing background tasks.")
42+
try await Task.sleep(for: .seconds(10))
43+
44+
// Exit the function. All asynchronous work has been executed before exiting the scope of this function.
45+
// Follows structured concurrency principles.
46+
context.logger.debug("BackgroundProcessingHandler - Background tasks completed. Returning")
47+
return
48+
}
49+
}
50+
51+
let adapter = LambdaCodableAdapter(handler: BackgroundProcessingHandler())
52+
let runtime = LambdaRuntime.init(handler: adapter)
53+
try await runtime.run()

Sources/AWSLambdaRuntime/Lambda+Codable.swift

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -60,12 +60,12 @@ public struct LambdaJSONOutputEncoder<Output: Encodable>: LambdaOutputEncoder {
6060
extension LambdaCodableAdapter {
6161
/// Initializes an instance given an encoder, decoder, and a handler with a non-`Void` output.
6262
/// - Parameters:
63-
/// - encoder: The encoder object that will be used to encode the generic `Output` obtained from the `handler`'s `outputWriter` into a `ByteBuffer`.
64-
/// - decoder: The decoder object that will be used to decode the received `ByteBuffer` event into the generic `Event` type served to the `handler`.
63+
/// - 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.
64+
/// - 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.
6565
/// - handler: The handler object.
6666
public init(
67-
encoder: JSONEncoder,
68-
decoder: JSONDecoder,
67+
encoder: JSONEncoder = JSONEncoder(),
68+
decoder: JSONDecoder = JSONDecoder(),
6969
handler: Handler
7070
)
7171
where

0 commit comments

Comments
 (0)