Skip to content

Commit

Permalink
Merge branch 'main' into sebsto/streaming
Browse files Browse the repository at this point in the history
  • Loading branch information
sebsto authored Nov 7, 2024
2 parents c95f635 + ca82709 commit 010f6b5
Show file tree
Hide file tree
Showing 9 changed files with 328 additions and 40 deletions.
6 changes: 3 additions & 3 deletions .github/workflows/pull_request.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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', 'Streaming' ]"
examples: "[ 'HelloWorld', 'APIGateway','S3_AWSSDK', 'S3_Soto', 'Streaming', 'BackgroundTasks' ]"

archive_plugin_enabled: true

Expand All @@ -47,12 +47,12 @@ jobs:
# https://github.com/swiftlang/github-workflows/issues/34
musl:
runs-on: ubuntu-latest
container: swift:6.0-noble
container: swift:6.0.2-noble
timeout-minutes: 30
steps:
- name: Check out code
uses: actions/checkout@v4
- name: Install SDK
run: swift sdk install https://download.swift.org/swift-6.0.1-release/static-sdk/swift-6.0.1-RELEASE/swift-6.0.1-RELEASE_static-linux-0.0.1.artifactbundle.tar.gz --checksum d4f46ba40e11e697387468e18987ee622908bc350310d8af54eb5e17c2ff5481
run: swift sdk install https://download.swift.org/swift-6.0.2-release/static-sdk/swift-6.0.2-RELEASE/swift-6.0.2-RELEASE_static-linux-0.0.1.artifactbundle.tar.gz --checksum aa5515476a403797223fc2aad4ca0c3bf83995d5427fb297cab1d93c68cee075
- name: Build
run: swift build --swift-sdk x86_64-swift-linux-musl
8 changes: 8 additions & 0 deletions Examples/BackgroundTasks/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
.DS_Store
/.build
/Packages
xcuserdata/
DerivedData/
.swiftpm/configuration/registries.json
.swiftpm/xcode/package.xcworkspace/contents.xcworkspacedata
.netrc
60 changes: 60 additions & 0 deletions Examples/BackgroundTasks/Package.swift
Original file line number Diff line number Diff line change
@@ -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)
]
}
119 changes: 119 additions & 0 deletions Examples/BackgroundTasks/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,119 @@
# 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"}
```

### 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:
```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.

## Cleanup

When done testing, you can delete the Lambda function with this command.

```bash
aws lambda delete-function --function-name BackgroundTasks
```
52 changes: 52 additions & 0 deletions Examples/BackgroundTasks/Sources/main.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
//===----------------------------------------------------------------------===//
//
// 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 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<Output>,
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()
28 changes: 1 addition & 27 deletions Sources/AWSLambdaRuntime/Docs.docc/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ Swift AWS Lambda Runtime was designed to make building Lambda functions in Swift

## Getting started

If you have never used AWS Lambda or Docker before, check out this [getting started guide](https://fabianfett.de/getting-started-with-swift-aws-lambda-runtime) which helps you with every step from zero to a running Lambda.
If you have never used AWS Lambda or Docker before, check out this [getting started guide](https://swiftpackageindex.com/swift-server/swift-aws-lambda-runtime/1.0.0-alpha.3/tutorials/table-of-content) which helps you with every step from zero to a running Lambda.

First, create a SwiftPM project and pull Swift AWS Lambda Runtime as dependency into your project

Expand Down Expand Up @@ -120,32 +120,6 @@ First, add a dependency on the event packages:

Modeling Lambda functions as Closures is both simple and safe. Swift AWS Lambda Runtime will ensure that the user-provided code is offloaded from the network processing thread such that even if the code becomes slow to respond or gets stuck, the underlying process can continue to function. This safety comes at a small performance penalty from context switching between threads. In many cases, the simplicity and safety of using the Closure based API is often preferred over the complexity of the performance-oriented API.

### Using EventLoopLambdaHandler

Performance sensitive Lambda functions may choose to use a more complex API which allows user code to run on the same thread as the networking handlers. Swift AWS Lambda Runtime uses [SwiftNIO](https://github.com/apple/swift-nio) as its underlying networking engine which means the APIs are based on [SwiftNIO](https://github.com/apple/swift-nio) concurrency primitives like the `EventLoop` and `EventLoopFuture`. For example:

```swift
// Import the modules
import AWSLambdaRuntime
import AWSLambdaEvents
import NIO

// Our Lambda handler, conforms to EventLoopLambdaHandler
struct Handler: EventLoopLambdaHandler {
typealias In = SNS.Message // Request type
typealias Out = Void // Response type

// In this example we are receiving an SNS Message, with no response (Void).
func handle(context: Lambda.Context, event: In) -> EventLoopFuture<Out> {
...
context.eventLoop.makeSucceededFuture(Void())
}
}

Lambda.run(Handler())
```

Beyond the small cognitive complexity of using the `EventLoopFuture` based APIs, note these APIs should be used with extra care. An [`EventLoopLambdaHandler`][ellh] will execute the user code on the same `EventLoop` (thread) as the library, making processing faster but requiring the user code to never call blocking APIs as it might prevent the underlying process from functioning.

## Deploying to AWS Lambda

Expand Down
8 changes: 4 additions & 4 deletions Sources/AWSLambdaRuntime/Lambda+Codable.swift
Original file line number Diff line number Diff line change
Expand Up @@ -60,12 +60,12 @@ public struct LambdaJSONOutputEncoder<Output: Encodable>: 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
Expand Down
6 changes: 6 additions & 0 deletions Sources/AWSLambdaRuntimeCore/LambdaRuntime.swift
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,12 @@ public final class LambdaRuntime<Handler>: @unchecked Sendable where Handler: St
) {
self.handlerMutex = NIOLockedValueBox(handler)
self.eventLoop = eventLoop

// by setting the log level here, we understand it can not be changed dynamically at runtime
// developers have to wait for AWS Lambda to dispose and recreate a runtime environment to pickup a change
// this approach is less flexible but more performant than reading the value of the environment variable at each invocation
var log = logger
log.logLevel = Lambda.env("LOG_LEVEL").flatMap(Logger.Level.init) ?? .info
self.logger = logger
}

Expand Down
Loading

0 comments on commit 010f6b5

Please sign in to comment.