From 752bd41792abe2ce2642ae1a646d82dd62e3638a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A9bastien=20Stormacq?= Date: Sun, 12 Jan 2025 17:21:33 +0100 Subject: [PATCH 01/24] move doc images under Resources directory (#460) Standardize the place where doc resources are located --- .../deployment/console-10-regions.png | Bin .../deployment/console-20-dashboard.png | Bin .../deployment/console-30-create-function.png | Bin .../deployment/console-40-select-zip-file.png | Bin .../deployment/console-50-upload-zip.png | Bin .../deployment/console-60-prepare-test-event.png | Bin .../console-70-view-invocation-response.png | Bin .../deployment/console-80-delete-function.png | Bin .../deployment/console-80-delete-role.png | Bin 9 files changed, 0 insertions(+), 0 deletions(-) rename Sources/AWSLambdaRuntimeCore/Documentation.docc/{img => Resources}/deployment/console-10-regions.png (100%) rename Sources/AWSLambdaRuntimeCore/Documentation.docc/{img => Resources}/deployment/console-20-dashboard.png (100%) rename Sources/AWSLambdaRuntimeCore/Documentation.docc/{img => Resources}/deployment/console-30-create-function.png (100%) rename Sources/AWSLambdaRuntimeCore/Documentation.docc/{img => Resources}/deployment/console-40-select-zip-file.png (100%) rename Sources/AWSLambdaRuntimeCore/Documentation.docc/{img => Resources}/deployment/console-50-upload-zip.png (100%) rename Sources/AWSLambdaRuntimeCore/Documentation.docc/{img => Resources}/deployment/console-60-prepare-test-event.png (100%) rename Sources/AWSLambdaRuntimeCore/Documentation.docc/{img => Resources}/deployment/console-70-view-invocation-response.png (100%) rename Sources/AWSLambdaRuntimeCore/Documentation.docc/{img => Resources}/deployment/console-80-delete-function.png (100%) rename Sources/AWSLambdaRuntimeCore/Documentation.docc/{img => Resources}/deployment/console-80-delete-role.png (100%) diff --git a/Sources/AWSLambdaRuntimeCore/Documentation.docc/img/deployment/console-10-regions.png b/Sources/AWSLambdaRuntimeCore/Documentation.docc/Resources/deployment/console-10-regions.png similarity index 100% rename from Sources/AWSLambdaRuntimeCore/Documentation.docc/img/deployment/console-10-regions.png rename to Sources/AWSLambdaRuntimeCore/Documentation.docc/Resources/deployment/console-10-regions.png diff --git a/Sources/AWSLambdaRuntimeCore/Documentation.docc/img/deployment/console-20-dashboard.png b/Sources/AWSLambdaRuntimeCore/Documentation.docc/Resources/deployment/console-20-dashboard.png similarity index 100% rename from Sources/AWSLambdaRuntimeCore/Documentation.docc/img/deployment/console-20-dashboard.png rename to Sources/AWSLambdaRuntimeCore/Documentation.docc/Resources/deployment/console-20-dashboard.png diff --git a/Sources/AWSLambdaRuntimeCore/Documentation.docc/img/deployment/console-30-create-function.png b/Sources/AWSLambdaRuntimeCore/Documentation.docc/Resources/deployment/console-30-create-function.png similarity index 100% rename from Sources/AWSLambdaRuntimeCore/Documentation.docc/img/deployment/console-30-create-function.png rename to Sources/AWSLambdaRuntimeCore/Documentation.docc/Resources/deployment/console-30-create-function.png diff --git a/Sources/AWSLambdaRuntimeCore/Documentation.docc/img/deployment/console-40-select-zip-file.png b/Sources/AWSLambdaRuntimeCore/Documentation.docc/Resources/deployment/console-40-select-zip-file.png similarity index 100% rename from Sources/AWSLambdaRuntimeCore/Documentation.docc/img/deployment/console-40-select-zip-file.png rename to Sources/AWSLambdaRuntimeCore/Documentation.docc/Resources/deployment/console-40-select-zip-file.png diff --git a/Sources/AWSLambdaRuntimeCore/Documentation.docc/img/deployment/console-50-upload-zip.png b/Sources/AWSLambdaRuntimeCore/Documentation.docc/Resources/deployment/console-50-upload-zip.png similarity index 100% rename from Sources/AWSLambdaRuntimeCore/Documentation.docc/img/deployment/console-50-upload-zip.png rename to Sources/AWSLambdaRuntimeCore/Documentation.docc/Resources/deployment/console-50-upload-zip.png diff --git a/Sources/AWSLambdaRuntimeCore/Documentation.docc/img/deployment/console-60-prepare-test-event.png b/Sources/AWSLambdaRuntimeCore/Documentation.docc/Resources/deployment/console-60-prepare-test-event.png similarity index 100% rename from Sources/AWSLambdaRuntimeCore/Documentation.docc/img/deployment/console-60-prepare-test-event.png rename to Sources/AWSLambdaRuntimeCore/Documentation.docc/Resources/deployment/console-60-prepare-test-event.png diff --git a/Sources/AWSLambdaRuntimeCore/Documentation.docc/img/deployment/console-70-view-invocation-response.png b/Sources/AWSLambdaRuntimeCore/Documentation.docc/Resources/deployment/console-70-view-invocation-response.png similarity index 100% rename from Sources/AWSLambdaRuntimeCore/Documentation.docc/img/deployment/console-70-view-invocation-response.png rename to Sources/AWSLambdaRuntimeCore/Documentation.docc/Resources/deployment/console-70-view-invocation-response.png diff --git a/Sources/AWSLambdaRuntimeCore/Documentation.docc/img/deployment/console-80-delete-function.png b/Sources/AWSLambdaRuntimeCore/Documentation.docc/Resources/deployment/console-80-delete-function.png similarity index 100% rename from Sources/AWSLambdaRuntimeCore/Documentation.docc/img/deployment/console-80-delete-function.png rename to Sources/AWSLambdaRuntimeCore/Documentation.docc/Resources/deployment/console-80-delete-function.png diff --git a/Sources/AWSLambdaRuntimeCore/Documentation.docc/img/deployment/console-80-delete-role.png b/Sources/AWSLambdaRuntimeCore/Documentation.docc/Resources/deployment/console-80-delete-role.png similarity index 100% rename from Sources/AWSLambdaRuntimeCore/Documentation.docc/img/deployment/console-80-delete-role.png rename to Sources/AWSLambdaRuntimeCore/Documentation.docc/Resources/deployment/console-80-delete-role.png From b4f016422ebd5e9c2e6aa97b8c74e924dad2f4c9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A9bastien=20Stormacq?= Date: Sun, 12 Jan 2025 17:40:17 +0100 Subject: [PATCH 02/24] Update readme.md (#461) Add a note about current status for Swift Service Lifecycle support and a link to the issue --- readme.md | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/readme.md b/readme.md index 4d1ad7a4..e844dd0a 100644 --- a/readme.md +++ b/readme.md @@ -1,9 +1,6 @@ > [!IMPORTANT] > The documentation included here refers to the Swift AWS Lambda Runtime v2 (code from the main branch). If you're developing for the runtime v1.x, check this [readme](https://github.com/swift-server/swift-aws-lambda-runtime/blob/v1/readme.md) instead. -> [!WARNING] -> The Swift AWS Runtime v2 is work in progress. We will add more documentation and code examples over time. - This guide contains the follwoing sections: - [The Swift AWS Lambda Runtime](#the-swift-aws-lambda-runtime) @@ -282,7 +279,7 @@ try await runtime.run() ### Integration with Swift Service LifeCycle -tbd + link to docc +Support for [Swift Service Lifecycle](https://github.com/swift-server/swift-service-lifecycle) is currently being implemented. You can follow https://github.com/swift-server/swift-aws-lambda-runtime/issues/374 for more details and teh current status. Your contributions are welcome. ### Use Lambda Background Tasks From 71d49b049ec1325ae064661376e586577923555a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A9bastien=20Stormacq?= Date: Tue, 21 Jan 2025 08:06:51 +0100 Subject: [PATCH 03/24] [plugin] support for resources packaging on ubuntu (#467) When including resources in the package and packaging on Ubuntu, `FileManager` throws a FilePermission error. The docker daemon runs as root and files to be copied are owned by `root` while the archiver runs as the current user (`ubuntu` on EC2 Ubuntu). The `FileManager` manages to copy the files but throws an error after the copy. We suspect the `FileManager` to perform some kind of operation after the copy and it fails because of the `root` permission of the files. See https://github.com/swift-server/swift-aws-lambda-runtime/issues/449#issuecomment-2595978246 for a description of the problem. This PR contains code to reproduce the problem, a very simple workaround, and an integration test. The workaround consists of - trapping all errors - verify if the error is the permission error (Code = 513) - verify if the files have been copied or not - if the two above conditions are met, ignore the error, otherwise re-throw it I would rather prefer a solution that solves the root cause rather than just ignoring the error. We're still investigating the root cause (see [this thread](https://forums.swift.org/t/filemanager-copyitem-on-linux-fails-after-copying-the-files/77282) on the Swift Forum and this issue on Swift Foundation https://github.com/swiftlang/swift-foundation/issues/1125 --- .github/workflows/integration_tests.yml | 15 +++- .github/workflows/pull_request.yml | 4 +- .../workflows/scripts/check-archive-plugin.sh | 16 ++++- .licenseignore | 3 +- Examples/ResourcesPackaging/.gitignore | 8 +++ Examples/ResourcesPackaging/Package.swift | 57 +++++++++++++++ .../ResourcesPackaging/Sources/main.swift | 26 +++++++ Examples/ResourcesPackaging/hello.txt | 1 + Plugins/AWSLambdaPackager/Plugin.swift | 28 ++++++-- scripts/ubuntu-install-swift.sh | 70 +++++++++++++++++++ scripts/ubuntu-test-plugin.sh | 28 ++++++++ 11 files changed, 243 insertions(+), 13 deletions(-) create mode 100644 Examples/ResourcesPackaging/.gitignore create mode 100644 Examples/ResourcesPackaging/Package.swift create mode 100644 Examples/ResourcesPackaging/Sources/main.swift create mode 100644 Examples/ResourcesPackaging/hello.txt create mode 100644 scripts/ubuntu-install-swift.sh create mode 100644 scripts/ubuntu-test-plugin.sh diff --git a/.github/workflows/integration_tests.yml b/.github/workflows/integration_tests.yml index 468fc295..1a67f637 100644 --- a/.github/workflows/integration_tests.yml +++ b/.github/workflows/integration_tests.yml @@ -19,6 +19,11 @@ on: type: boolean description: "Boolean to enable the compilation of examples. Defaults to true." default: true + archive_plugin_examples: + type: string + description: "The list of examples to run through the archive plugin test. Pass a String with a valid JSON array such as \"[ 'HelloWorld', 'APIGateway' ]\"" + required: true + default: "" archive_plugin_enabled: type: boolean description: "Boolean to enable the test of the archive plugin. Defaults to true." @@ -54,7 +59,7 @@ jobs: # We are using only one Swift version swift: - image: ${{ inputs.matrix_linux_swift_container_image }} - swift_version: "6.0.1-amazonlinux2" + swift_version: "6.0.3-amazonlinux2" container: image: ${{ matrix.swift.image }} steps: @@ -98,6 +103,10 @@ jobs: name: Test archive plugin if: ${{ inputs.archive_plugin_enabled }} runs-on: ubuntu-latest + strategy: + fail-fast: false + matrix: + examples: ${{ fromJson(inputs.archive_plugin_examples) }} steps: - name: Checkout repository uses: actions/checkout@v4 @@ -107,8 +116,10 @@ jobs: # https://github.com/actions/checkout/issues/766 run: git config --global --add safe.directory ${GITHUB_WORKSPACE} - name: Test the archive plugin + env: + EXAMPLE: ${{ matrix.examples }} run: | - .github/workflows/scripts/check-archive-plugin.sh + .github/workflows/scripts/check-archive-plugin.sh check-foundation: name: No dependencies on Foundation diff --git a/.github/workflows/pull_request.yml b/.github/workflows/pull_request.yml index 2051c091..72c2a24c 100644 --- a/.github/workflows/pull_request.yml +++ b/.github/workflows/pull_request.yml @@ -36,8 +36,8 @@ 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: "[ 'APIGateway', 'APIGateway+LambdaAuthorizer', 'BackgroundTasks', 'HelloJSON', 'HelloWorld', 'S3_AWSSDK', 'S3_Soto', 'Streaming', 'Testing', 'Tutorial' ]" - + examples: "[ 'APIGateway', 'APIGateway+LambdaAuthorizer', 'BackgroundTasks', 'HelloJSON', 'HelloWorld', 'ResourcesPackaging', 'S3_AWSSDK', 'S3_Soto', 'Streaming', 'Testing', 'Tutorial' ]" + archive_plugin_examples: "[ 'HelloWorld', 'ResourcesPackaging' ]" archive_plugin_enabled: true swift-6-language-mode: diff --git a/.github/workflows/scripts/check-archive-plugin.sh b/.github/workflows/scripts/check-archive-plugin.sh index 3a15c138..3c127be8 100755 --- a/.github/workflows/scripts/check-archive-plugin.sh +++ b/.github/workflows/scripts/check-archive-plugin.sh @@ -13,12 +13,17 @@ ## ##===----------------------------------------------------------------------===## -EXAMPLE=HelloWorld +log() { printf -- "** %s\n" "$*" >&2; } +error() { printf -- "** ERROR: %s\n" "$*" >&2; } +fatal() { error "$@"; exit 1; } + +test -n "${EXAMPLE:-}" || fatal "EXAMPLE unset" + OUTPUT_DIR=.build/plugins/AWSLambdaPackager/outputs/AWSLambdaPackager OUTPUT_FILE=${OUTPUT_DIR}/MyLambda/bootstrap ZIP_FILE=${OUTPUT_DIR}/MyLambda/MyLambda.zip -pushd Examples/${EXAMPLE} || exit 1 +pushd "Examples/${EXAMPLE}" || exit 1 # package the example (docker and swift toolchain are installed on the GH runner) LAMBDA_USE_LOCAL_DEPS=../.. swift package archive --allow-network-connections docker || exit 1 @@ -33,5 +38,10 @@ file "${OUTPUT_FILE}" | grep --silent ELF # does the ZIP file contain the bootstrap? unzip -l "${ZIP_FILE}" | grep --silent bootstrap -echo "✅ The archive plugin is OK" +# if EXAMPLE is ResourcesPackaging, check if the ZIP file contains hello.txt +if [ "$EXAMPLE" == "ResourcesPackaging" ]; then + unzip -l "${ZIP_FILE}" | grep --silent hello.txt +fi + +echo "✅ The archive plugin is OK with example ${EXAMPLE}" popd || exit 1 diff --git a/.licenseignore b/.licenseignore index db42f1da..d47f45a2 100644 --- a/.licenseignore +++ b/.licenseignore @@ -33,4 +33,5 @@ Package.resolved *.yaml *.yml **/.npmignore -**/*.json \ No newline at end of file +**/*.json +**/*.txt \ No newline at end of file diff --git a/Examples/ResourcesPackaging/.gitignore b/Examples/ResourcesPackaging/.gitignore new file mode 100644 index 00000000..0023a534 --- /dev/null +++ b/Examples/ResourcesPackaging/.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/ResourcesPackaging/Package.swift b/Examples/ResourcesPackaging/Package.swift new file mode 100644 index 00000000..4680b74a --- /dev/null +++ b/Examples/ResourcesPackaging/Package.swift @@ -0,0 +1,57 @@ +// swift-tools-version: 6.0 +// The swift-tools-version declares the minimum version of Swift required to build this package. + +import PackageDescription + +// needed for CI to test the local version of the library +import struct Foundation.URL + +let package = Package( + name: "ResourcesPackaging", + platforms: [.macOS(.v15)], + products: [ + .executable(name: "MyLambda", targets: ["MyLambda"]) + ], + dependencies: [ + .package(url: "https://github.com/swift-server/swift-aws-lambda-runtime.git", branch: "main") + ], + targets: [ + .executableTarget( + name: "MyLambda", + dependencies: [ + .product(name: "AWSLambdaRuntime", package: "swift-aws-lambda-runtime") + ], + path: ".", + resources: [ + .process("hello.txt") + ] + ) + ] +) + +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/ResourcesPackaging/Sources/main.swift b/Examples/ResourcesPackaging/Sources/main.swift new file mode 100644 index 00000000..dccbd863 --- /dev/null +++ b/Examples/ResourcesPackaging/Sources/main.swift @@ -0,0 +1,26 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the SwiftAWSLambdaRuntime open source project +// +// Copyright (c) 2025 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 + +let runtime = LambdaRuntime { + (event: String, context: LambdaContext) in + guard let fileURL = Bundle.module.url(forResource: "hello", withExtension: "txt") else { + fatalError("no file url") + } + return try String(contentsOf: fileURL, encoding: .utf8) +} + +try await runtime.run() diff --git a/Examples/ResourcesPackaging/hello.txt b/Examples/ResourcesPackaging/hello.txt new file mode 100644 index 00000000..557db03d --- /dev/null +++ b/Examples/ResourcesPackaging/hello.txt @@ -0,0 +1 @@ +Hello World diff --git a/Plugins/AWSLambdaPackager/Plugin.swift b/Plugins/AWSLambdaPackager/Plugin.swift index a8693945..f6ac2b03 100644 --- a/Plugins/AWSLambdaPackager/Plugin.swift +++ b/Plugins/AWSLambdaPackager/Plugin.swift @@ -249,11 +249,29 @@ struct AWSLambdaPackager: CommandPlugin { let resourcesDirectoryName = artifactURL.lastPathComponent let relocatedResourcesDirectory = workingDirectory.appending(path: resourcesDirectoryName) if FileManager.default.fileExists(atPath: artifactURL.path()) { - try FileManager.default.copyItem( - atPath: artifactURL.path(), - toPath: relocatedResourcesDirectory.path() - ) - arguments.append(resourcesDirectoryName) + do { + try FileManager.default.copyItem( + atPath: artifactURL.path(), + toPath: relocatedResourcesDirectory.path() + ) + arguments.append(resourcesDirectoryName) + } catch let error as CocoaError { + + // On Linux, when the build has been done with Docker, + // the source file are owned by root + // this causes a permission error **after** the files have been copied + // see https://github.com/swift-server/swift-aws-lambda-runtime/issues/449 + // see https://forums.swift.org/t/filemanager-copyitem-on-linux-fails-after-copying-the-files/77282 + + // because this error happens after the files have been copied, we can ignore it + // this code checks if the destination file exists + // if they do, just ignore error, otherwise throw it up to the caller. + if !(error.code == CocoaError.Code.fileWriteNoPermission + && FileManager.default.fileExists(atPath: relocatedResourcesDirectory.path())) + { + throw error + } // else just ignore it + } } } diff --git a/scripts/ubuntu-install-swift.sh b/scripts/ubuntu-install-swift.sh new file mode 100644 index 00000000..5ff58f46 --- /dev/null +++ b/scripts/ubuntu-install-swift.sh @@ -0,0 +1,70 @@ +#!/bin/bash +##===----------------------------------------------------------------------===## +## +## This source file is part of the SwiftAWSLambdaRuntime open source project +## +## Copyright (c) 2025 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 +## +##===----------------------------------------------------------------------===## + +sudo apt update && sudo apt -y upgrade + +# Install Swift 6.0.3 +sudo apt-get -y install \ + binutils \ + git \ + gnupg2 \ + libc6-dev \ + libcurl4-openssl-dev \ + libedit2 \ + libgcc-13-dev \ + libncurses-dev \ + libpython3-dev \ + libsqlite3-0 \ + libstdc++-13-dev \ + libxml2-dev \ + libz3-dev \ + pkg-config \ + tzdata \ + unzip \ + zip \ + zlib1g-dev + +wget https://download.swift.org/swift-6.0.3-release/ubuntu2404-aarch64/swift-6.0.3-RELEASE/swift-6.0.3-RELEASE-ubuntu24.04-aarch64.tar.gz + +tar xfvz swift-6.0.3-RELEASE-ubuntu24.04-aarch64.tar.gz + +export PATH=/home/ubuntu/swift-6.0.3-RELEASE-ubuntu24.04-aarch64/usr/bin:"${PATH}" + +swift --version + +# Install Docker +sudo apt-get update +sudo apt-get install -y ca-certificates curl +sudo install -m 0755 -d /etc/apt/keyrings +sudo curl -fsSL https://download.docker.com/linux/ubuntu/gpg -o /etc/apt/keyrings/docker.asc +sudo chmod a+r /etc/apt/keyrings/docker.asc + +# Add the repository to Apt sources: +# shellcheck source=/etc/os-release +# shellcheck disable=SC1091 +. /etc/os-release +echo \ + "deb [arch=$(dpkg --print-architecture) signed-by=/etc/apt/keyrings/docker.asc] https://download.docker.com/linux/ubuntu \ + $VERSION_CODENAME stable" | \ + sudo tee /etc/apt/sources.list.d/docker.list > /dev/null +sudo apt-get update + +sudo apt-get -y install docker-ce docker-ce-cli containerd.io docker-buildx-plugin docker-compose-plugin + +# Add the current user to the docker group +sudo usermod -aG docker "$USER" + +# LOGOUT and LOGIN to apply the changes +exit 0 diff --git a/scripts/ubuntu-test-plugin.sh b/scripts/ubuntu-test-plugin.sh new file mode 100644 index 00000000..19d74609 --- /dev/null +++ b/scripts/ubuntu-test-plugin.sh @@ -0,0 +1,28 @@ +#!/bin/bash +##===----------------------------------------------------------------------===## +## +## This source file is part of the SwiftAWSLambdaRuntime open source project +## +## Copyright (c) 2025 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 +## +##===----------------------------------------------------------------------===## + +# Connect with ssh + +export PATH=/home/ubuntu/swift-6.0.3-RELEASE-ubuntu24.04-aarch64/usr/bin:"${PATH}" + +# clone a project +git clone https://github.com/swift-server/swift-aws-lambda-runtime.git + +# be sure Swift is install. +# Youc an install swift with the following command: ./scripts/ubuntu-install-swift.sh + +# build the project +cd swift-aws-lambda-runtime/Examples/ResourcesPackaging/ || exit 1 +LAMBDA_USE_LOCAL_DEPS=../.. swift package archive --allow-network-connections docker From cda7c768dc453afb8a36bd0dd1a45d08ecbb0517 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A9bastien=20Stormacq?= Date: Tue, 21 Jan 2025 08:58:31 +0100 Subject: [PATCH 04/24] update CDK dependencies (#469) Just a `npm update` to remove dependabots warnings --- Examples/CDK/infra/package-lock.json | 22 +++++++++++----------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/Examples/CDK/infra/package-lock.json b/Examples/CDK/infra/package-lock.json index f91106b7..75710aae 100644 --- a/Examples/CDK/infra/package-lock.json +++ b/Examples/CDK/infra/package-lock.json @@ -23,9 +23,9 @@ } }, "node_modules/@aws-cdk/asset-awscli-v1": { - "version": "2.2.215", - "resolved": "https://registry.npmjs.org/@aws-cdk/asset-awscli-v1/-/asset-awscli-v1-2.2.215.tgz", - "integrity": "sha512-D+Jzwpl+zlBGjJf7nuRcz6JFNwqDQ+IzwIq0VSC4LMRRvrkhGE/ZE+zab3EnjmVkipcQqtQe+PVKefgmxETbvA==", + "version": "2.2.220", + "resolved": "https://registry.npmjs.org/@aws-cdk/asset-awscli-v1/-/asset-awscli-v1-2.2.220.tgz", + "integrity": "sha512-2eXZnnIgwWmXc7eRh8mRKPp6yHTKiQrLziRX/oVSfp4M6Jn2no0QFKJoHWqziF5MDQa5TF8qhD4FGsls/1nYPg==", "license": "Apache-2.0" }, "node_modules/@aws-cdk/asset-kubectl-v20": { @@ -41,16 +41,16 @@ "license": "Apache-2.0" }, "node_modules/@aws-cdk/cloud-assembly-schema": { - "version": "38.0.1", - "resolved": "https://registry.npmjs.org/@aws-cdk/cloud-assembly-schema/-/cloud-assembly-schema-38.0.1.tgz", - "integrity": "sha512-KvPe+NMWAulfNVwY7jenFhzhuLhLqJ/OPy5jx7wUstbjnYnjRVLpUHPU3yCjXFE0J8cuJVdx95BJ4rOs66Pi9w==", + "version": "39.2.1", + "resolved": "https://registry.npmjs.org/@aws-cdk/cloud-assembly-schema/-/cloud-assembly-schema-39.2.1.tgz", + "integrity": "sha512-cz18QG02p++ivQ5wdjLI8TyMk1p6kwQc1ExeL2A9RoUg1gCnwEBrkJMhiWgTroWBblT0GF0MqSvzkyeEdHqn2A==", "bundleDependencies": [ "jsonschema", "semver" ], "license": "Apache-2.0", "dependencies": { - "jsonschema": "^1.4.1", + "jsonschema": "~1.4.1", "semver": "^7.6.3" } }, @@ -356,9 +356,9 @@ } }, "node_modules/aws-cdk-lib": { - "version": "2.173.2", - "resolved": "https://registry.npmjs.org/aws-cdk-lib/-/aws-cdk-lib-2.173.2.tgz", - "integrity": "sha512-cL9+z8Pl3VZGoO7BwdsrFAOeud/vSl3at7OvmhihbNprMN15XuFUx/rViAU5OI1m92NbV4NBzYSLbSeCwYLNyw==", + "version": "2.176.0", + "resolved": "https://registry.npmjs.org/aws-cdk-lib/-/aws-cdk-lib-2.176.0.tgz", + "integrity": "sha512-6Gs2kBaq4elQ4fNAOiCgbD9oOLx/heb/Lp4OVE6Uf7FulYW0DikWJXxR5GWJslTJ4/sCf3UU91q415fc0bruLg==", "bundleDependencies": [ "@balena/dockerignore", "case", @@ -377,7 +377,7 @@ "@aws-cdk/asset-awscli-v1": "^2.2.208", "@aws-cdk/asset-kubectl-v20": "^2.1.3", "@aws-cdk/asset-node-proxy-agent-v6": "^2.1.0", - "@aws-cdk/cloud-assembly-schema": "^38.0.1", + "@aws-cdk/cloud-assembly-schema": "^39.0.1", "@balena/dockerignore": "^1.0.2", "case": "1.6.3", "fs-extra": "^11.2.0", From ed84609bc4d235ff24600457b9630661e851be9a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A9bastien=20Stormacq?= Date: Wed, 22 Jan 2025 12:59:10 +0100 Subject: [PATCH 05/24] [core] Update Local Server for Swift 6 compliance (#464) This PR updates the 4 years old Lambda Local Server for Swift 6 concurrency. This contributes to https://github.com/swift-server/swift-aws-lambda-runtime/issues/462 --- .../Lambda+LocalServer.swift | 610 ++++++++++-------- 1 file changed, 356 insertions(+), 254 deletions(-) diff --git a/Sources/AWSLambdaRuntimeCore/Lambda+LocalServer.swift b/Sources/AWSLambdaRuntimeCore/Lambda+LocalServer.swift index a23ef1cf..a3d98aa2 100644 --- a/Sources/AWSLambdaRuntimeCore/Lambda+LocalServer.swift +++ b/Sources/AWSLambdaRuntimeCore/Lambda+LocalServer.swift @@ -19,13 +19,22 @@ import NIOConcurrencyHelpers import NIOCore import NIOHTTP1 import NIOPosix +import Synchronization // This functionality is designed for local testing hence being a #if DEBUG flag. + // For example: -// // try Lambda.withLocalServer { -// Lambda.run { (context: LambdaContext, event: String, callback: @escaping (Result) -> Void) in -// callback(.success("Hello, \(event)!")) +// try await LambdaRuntimeClient.withRuntimeClient( +// configuration: .init(ip: "127.0.0.1", port: 7000), +// eventLoop: self.eventLoop, +// logger: self.logger +// ) { runtimeClient in +// try await Lambda.runLoop( +// runtimeClient: runtimeClient, +// handler: handler, +// logger: self.logger +// ) // } // } extension Lambda { @@ -36,290 +45,383 @@ extension Lambda { /// - body: Code to run within the context of the mock server. Typically this would be a Lambda.run function call. /// /// - note: This API is designed strictly for local testing and is behind a DEBUG flag - static func withLocalServer( + static func withLocalServer( invocationEndpoint: String? = nil, - _ body: @escaping () async throws -> Value - ) async throws -> Value { - let server = LocalLambda.Server(invocationEndpoint: invocationEndpoint) - try await server.start().get() - defer { try! server.stop() } - return try await body() + _ body: @escaping () async throws -> Void + ) async throws { + + // launch the local server and wait for it to be started before running the body + try await withThrowingTaskGroup(of: Void.self) { group in + // this call will return when the server calls continuation.resume() + try await withCheckedThrowingContinuation { (continuation: CheckedContinuation) in + group.addTask { + try await LambdaHttpServer(invocationEndpoint: invocationEndpoint).start(continuation: continuation) + } + } + // now that server is started, run the Lambda function itself + try await body() + } } } -// MARK: - Local Mock Server +// MARK: - Local HTTP Server -private enum LocalLambda { - struct Server { - private let logger: Logger - private let group: EventLoopGroup - private let host: String - private let port: Int - private let invocationEndpoint: String +/// An HTTP server that behaves like the AWS Lambda service for local testing. +/// This server is used to simulate the AWS Lambda service for local testing but also to accept invocation requests from the lambda client. +/// +/// It accepts three types of requests from the Lambda function (through the LambdaRuntimeClient): +/// 1. GET /next - the lambda function polls this endpoint to get the next invocation request +/// 2. POST /:requestID/response - the lambda function posts the response to the invocation request +/// 3. POST /:requestID/error - the lambda function posts an error response to the invocation request +/// +/// It also accepts one type of request from the client invoking the lambda function: +/// 1. POST /invoke - the client posts the event to the lambda function +/// +/// This server passes the data received from /invoke POST request to the lambda function (GET /next) and then forwards the response back to the client. +private struct LambdaHttpServer { + private let logger: Logger + private let group: EventLoopGroup + private let host: String + private let port: Int + private let invocationEndpoint: String - init(invocationEndpoint: String?) { - var logger = Logger(label: "LocalLambdaServer") - logger.logLevel = .info - self.logger = logger - self.group = MultiThreadedEventLoopGroup(numberOfThreads: 1) - self.host = "127.0.0.1" - self.port = 7000 - self.invocationEndpoint = invocationEndpoint ?? "/invoke" - } + private let invocationPool = Pool() + private let responsePool = Pool() - func start() -> EventLoopFuture { - let bootstrap = ServerBootstrap(group: group) - .serverChannelOption(ChannelOptions.socket(SocketOptionLevel(SOL_SOCKET), SO_REUSEADDR), value: 1) - .childChannelInitializer { channel in - channel.pipeline.configureHTTPServerPipeline(withErrorHandling: true).flatMap { _ in - channel.pipeline.addHandler( - HTTPHandler(logger: self.logger, invocationEndpoint: self.invocationEndpoint) + init(invocationEndpoint: String?) { + var logger = Logger(label: "LocalServer") + logger.logLevel = Lambda.env("LOG_LEVEL").flatMap(Logger.Level.init) ?? .info + self.logger = logger + self.group = MultiThreadedEventLoopGroup.singleton + self.host = "127.0.0.1" + self.port = 7000 + self.invocationEndpoint = invocationEndpoint ?? "/invoke" + } + + func start(continuation: CheckedContinuation) async throws { + let channel = try await ServerBootstrap(group: self.group) + .serverChannelOption(.backlog, value: 256) + .serverChannelOption(.socketOption(.so_reuseaddr), value: 1) + .childChannelOption(.maxMessagesPerRead, value: 1) + .bind( + host: self.host, + port: self.port + ) { channel in + channel.eventLoop.makeCompletedFuture { + + try channel.pipeline.syncOperations.configureHTTPServerPipeline( + withErrorHandling: true + ) + + return try NIOAsyncChannel( + wrappingChannelSynchronously: channel, + configuration: NIOAsyncChannel.Configuration( + inboundType: HTTPServerRequestPart.self, + outboundType: HTTPServerResponsePart.self ) + ) + } + } + + // notify the caller that the server is started + continuation.resume() + logger.info( + "Server started and listening", + metadata: [ + "host": "\(channel.channel.localAddress?.ipAddress?.debugDescription ?? "")", + "port": "\(channel.channel.localAddress?.port ?? 0)", + ] + ) + + // We are handling each incoming connection in a separate child task. It is important + // to use a discarding task group here which automatically discards finished child tasks. + // A normal task group retains all child tasks and their outputs in memory until they are + // consumed by iterating the group or by exiting the group. Since, we are never consuming + // the results of the group we need the group to automatically discard them; otherwise, this + // would result in a memory leak over time. + try await withThrowingDiscardingTaskGroup { group in + try await channel.executeThenClose { inbound in + for try await connectionChannel in inbound { + + group.addTask { + logger.trace("Handling a new connection") + await self.handleConnection(channel: connectionChannel) + logger.trace("Done handling the connection") } } - return bootstrap.bind(host: self.host, port: self.port).flatMap { channel -> EventLoopFuture in - guard channel.localAddress != nil else { - return channel.eventLoop.makeFailedFuture(ServerError.cantBind) - } - self.logger.info( - "LocalLambdaServer started and listening on \(self.host):\(self.port), receiving events on \(self.invocationEndpoint)" - ) - return channel.eventLoop.makeSucceededFuture(()) } } + logger.info("Server shutting down") + } - func stop() throws { - try self.group.syncShutdownGracefully() + /// This method handles individual TCP connections + private func handleConnection( + channel: NIOAsyncChannel + ) async { + + var requestHead: HTTPRequestHead! + var requestBody: ByteBuffer? + + // Note that this method is non-throwing and we are catching any error. + // We do this since we don't want to tear down the whole server when a single connection + // encounters an error. + do { + try await channel.executeThenClose { inbound, outbound in + for try await inboundData in inbound { + if case .head(let head) = inboundData { + requestHead = head + } + if case .body(let body) = inboundData { + requestBody = body + } + if case .end = inboundData { + precondition(requestHead != nil, "Received .end without .head") + // process the request + let response = try await self.processRequest( + head: requestHead, + body: requestBody + ) + // send the responses + try await self.sendResponse( + response: response, + outbound: outbound + ) + + requestHead = nil + requestBody = nil + } + } + } + } catch { + logger.error("Hit error: \(error)") } } - final class HTTPHandler: ChannelInboundHandler { - public typealias InboundIn = HTTPServerRequestPart - public typealias OutboundOut = HTTPServerResponsePart + /// This function process the URI request sent by the client and by the Lambda function + /// + /// It enqueues the client invocation and iterate over the invocation queue when the Lambda function sends /next request + /// It answers the /:requestID/response and /:requestID/error requests sent by the Lambda function but do not process the body + /// + /// - Parameters: + /// - head: the HTTP request head + /// - body: the HTTP request body + /// - Throws: + /// - Returns: the response to send back to the client or the Lambda function + private func processRequest(head: HTTPRequestHead, body: ByteBuffer?) async throws -> LocalServerResponse { - private var pending = CircularBuffer<(head: HTTPRequestHead, body: ByteBuffer?)>() - - private static var invocations = CircularBuffer() - private static var invocationState = InvocationState.waitingForLambdaRequest - - private let logger: Logger - private let invocationEndpoint: String - - init(logger: Logger, invocationEndpoint: String) { - self.logger = logger - self.invocationEndpoint = invocationEndpoint + if let body { + self.logger.trace( + "Processing request", + metadata: ["URI": "\(head.method) \(head.uri)", "Body": "\(String(buffer: body))"] + ) + } else { + self.logger.trace("Processing request", metadata: ["URI": "\(head.method) \(head.uri)"]) } - func channelRead(context: ChannelHandlerContext, data: NIOAny) { - let requestPart = unwrapInboundIn(data) + switch (head.method, head.uri) { - switch requestPart { - case .head(let head): - self.pending.append((head: head, body: nil)) - case .body(var buffer): - var request = self.pending.removeFirst() - if request.body == nil { - request.body = buffer + // + // client invocations + // + // client POST /invoke + case (.POST, let url) where url.hasSuffix(self.invocationEndpoint): + guard let body else { + return .init(status: .badRequest, headers: [], body: nil) + } + // we always accept the /invoke request and push them to the pool + let requestId = "\(DispatchTime.now().uptimeNanoseconds)" + logger.trace("/invoke received invocation", metadata: ["requestId": "\(requestId)"]) + await self.invocationPool.push(LocalServerInvocation(requestId: requestId, request: body)) + + // wait for the lambda function to process the request + for try await response in self.responsePool { + logger.trace( + "Received response to return to client", + metadata: ["requestId": "\(response.requestId ?? "")"] + ) + if response.requestId == requestId { + return response } else { - request.body!.writeBuffer(&buffer) - } - self.pending.prepend(request) - case .end: - let request = self.pending.removeFirst() - self.processRequest(context: context, request: request) - } - } - - func processRequest(context: ChannelHandlerContext, request: (head: HTTPRequestHead, body: ByteBuffer?)) { - - let eventLoop = context.eventLoop - let loopBoundContext = NIOLoopBound(context, eventLoop: eventLoop) - - switch (request.head.method, request.head.uri) { - // this endpoint is called by the client invoking the lambda - case (.POST, let url) where url.hasSuffix(self.invocationEndpoint): - guard let work = request.body else { - return self.writeResponse(context: context, response: .init(status: .badRequest)) - } - let requestID = "\(DispatchTime.now().uptimeNanoseconds)" // FIXME: - let promise = context.eventLoop.makePromise(of: Response.self) - promise.futureResult.whenComplete { result in - let context = loopBoundContext.value - switch result { - case .failure(let error): - self.logger.error("invocation error: \(error)") - self.writeResponse(context: context, response: .init(status: .internalServerError)) - case .success(let response): - self.writeResponse(context: context, response: response) - } - } - let invocation = Invocation(requestID: requestID, request: work, responsePromise: promise) - switch Self.invocationState { - case .waitingForInvocation(let promise): - promise.succeed(invocation) - case .waitingForLambdaRequest, .waitingForLambdaResponse: - Self.invocations.append(invocation) - } - - // lambda invocation using the wrong http method - case (_, let url) where url.hasSuffix(self.invocationEndpoint): - self.writeResponse(context: context, status: .methodNotAllowed) - - // /next endpoint is called by the lambda polling for work - case (.GET, let url) where url.hasSuffix(Consts.getNextInvocationURLSuffix): - // check if our server is in the correct state - guard case .waitingForLambdaRequest = Self.invocationState else { - self.logger.error("invalid invocation state \(Self.invocationState)") - self.writeResponse(context: context, response: .init(status: .unprocessableEntity)) - return - } - - // pop the first task from the queue - switch Self.invocations.popFirst() { - case .none: - // if there is nothing in the queue, - // create a promise that we can fullfill when we get a new task - let promise = context.eventLoop.makePromise(of: Invocation.self) - promise.futureResult.whenComplete { result in - let context = loopBoundContext.value - switch result { - case .failure(let error): - self.logger.error("invocation error: \(error)") - self.writeResponse(context: context, status: .internalServerError) - case .success(let invocation): - Self.invocationState = .waitingForLambdaResponse(invocation) - self.writeResponse(context: context, response: invocation.makeResponse()) - } - } - Self.invocationState = .waitingForInvocation(promise) - case .some(let invocation): - // if there is a task pending, we can immediately respond with it. - Self.invocationState = .waitingForLambdaResponse(invocation) - self.writeResponse(context: context, response: invocation.makeResponse()) - } - - // :requestID/response endpoint is called by the lambda posting the response - case (.POST, let url) where url.hasSuffix(Consts.postResponseURLSuffix): - let parts = request.head.uri.split(separator: "/") - guard let requestID = parts.count > 2 ? String(parts[parts.count - 2]) : nil else { - // the request is malformed, since we were expecting a requestId in the path - return self.writeResponse(context: context, status: .badRequest) - } - guard case .waitingForLambdaResponse(let invocation) = Self.invocationState else { - // a response was send, but we did not expect to receive one - self.logger.error("invalid invocation state \(Self.invocationState)") - return self.writeResponse(context: context, status: .unprocessableEntity) - } - guard requestID == invocation.requestID else { - // the request's requestId is not matching the one we are expecting - self.logger.error( - "invalid invocation state request ID \(requestID) does not match expected \(invocation.requestID)" + logger.error( + "Received response for a different request id", + metadata: ["response requestId": "\(response.requestId ?? "")", "requestId": "\(requestId)"] ) - return self.writeResponse(context: context, status: .badRequest) + // should we return an error here ? Or crash as this is probably a programming error? } - - invocation.responsePromise.succeed(.init(status: .ok, body: request.body)) - self.writeResponse(context: context, status: .accepted) - Self.invocationState = .waitingForLambdaRequest - - // :requestID/error endpoint is called by the lambda posting an error response - case (.POST, let url) where url.hasSuffix(Consts.postErrorURLSuffix): - let parts = request.head.uri.split(separator: "/") - guard let requestID = parts.count > 2 ? String(parts[parts.count - 2]) : nil else { - // the request is malformed, since we were expecting a requestId in the path - return self.writeResponse(context: context, status: .badRequest) - } - guard case .waitingForLambdaResponse(let invocation) = Self.invocationState else { - // a response was send, but we did not expect to receive one - self.logger.error("invalid invocation state \(Self.invocationState)") - return self.writeResponse(context: context, status: .unprocessableEntity) - } - guard requestID == invocation.requestID else { - // the request's requestId is not matching the one we are expecting - self.logger.error( - "invalid invocation state request ID \(requestID) does not match expected \(invocation.requestID)" - ) - return self.writeResponse(context: context, status: .badRequest) - } - - invocation.responsePromise.succeed(.init(status: .internalServerError, body: request.body)) - self.writeResponse(context: context, status: .accepted) - Self.invocationState = .waitingForLambdaRequest - - // unknown call - default: - self.writeResponse(context: context, status: .notFound) } - } + // What todo when there is no more responses to process? + // This should not happen as the async iterator blocks until there is a response to process + fatalError("No more responses to process - the async for loop should not return") - func writeResponse(context: ChannelHandlerContext, status: HTTPResponseStatus) { - self.writeResponse(context: context, response: .init(status: status)) - } + // client uses incorrect HTTP method + case (_, let url) where url.hasSuffix(self.invocationEndpoint): + return .init(status: .methodNotAllowed) - func writeResponse(context: ChannelHandlerContext, response: Response) { - var headers = HTTPHeaders(response.headers ?? []) - headers.add(name: "content-length", value: "\(response.body?.readableBytes ?? 0)") - let head = HTTPResponseHead( - version: HTTPVersion(major: 1, minor: 1), - status: response.status, - headers: headers + // + // lambda invocations + // + + // /next endpoint is called by the lambda polling for work + // this call only returns when there is a task to give to the lambda function + case (.GET, let url) where url.hasSuffix(Consts.getNextInvocationURLSuffix): + + // pop the tasks from the queue + self.logger.trace("/next waiting for /invoke") + for try await invocation in self.invocationPool { + self.logger.trace("/next retrieved invocation", metadata: ["requestId": "\(invocation.requestId)"]) + // this call also stores the invocation requestId into the response + return invocation.makeResponse(status: .accepted) + } + // What todo when there is no more tasks to process? + // This should not happen as the async iterator blocks until there is a task to process + fatalError("No more invocations to process - the async for loop should not return") + + // :requestID/response endpoint is called by the lambda posting the response + case (.POST, let url) where url.hasSuffix(Consts.postResponseURLSuffix): + let parts = head.uri.split(separator: "/") + guard let requestId = parts.count > 2 ? String(parts[parts.count - 2]) : nil else { + // the request is malformed, since we were expecting a requestId in the path + return .init(status: .badRequest) + } + // enqueue the lambda function response to be served as response to the client /invoke + logger.trace("/:requestID/response received response", metadata: ["requestId": "\(requestId)"]) + await self.responsePool.push( + LocalServerResponse( + id: requestId, + status: .ok, + headers: [("Content-Type", "application/json")], + body: body + ) ) - context.write(wrapOutboundOut(.head(head))).whenFailure { error in - self.logger.error("\(self) write error \(error)") + // tell the Lambda function we accepted the response + return .init(id: requestId, status: .accepted) + + // :requestID/error endpoint is called by the lambda posting an error response + // we accept all requestID and we do not handle the body, we just acknowledge the request + case (.POST, let url) where url.hasSuffix(Consts.postErrorURLSuffix): + let parts = head.uri.split(separator: "/") + guard let _ = parts.count > 2 ? String(parts[parts.count - 2]) : nil else { + // the request is malformed, since we were expecting a requestId in the path + return .init(status: .badRequest) } + return .init(status: .ok) - if let buffer = response.body { - context.write(wrapOutboundOut(.body(.byteBuffer(buffer)))).whenFailure { error in - self.logger.error("\(self) write error \(error)") - } - } - - context.writeAndFlush(wrapOutboundOut(.end(nil))).whenComplete { result in - if case .failure(let error) = result { - self.logger.error("\(self) write error \(error)") - } - } - } - - struct Response { - var status: HTTPResponseStatus = .ok - var headers: [(String, String)]? - var body: ByteBuffer? - } - - struct Invocation { - let requestID: String - let request: ByteBuffer - let responsePromise: EventLoopPromise - - func makeResponse() -> Response { - var response = Response() - response.body = self.request - // required headers - response.headers = [ - (AmazonHeaders.requestID, self.requestID), - ( - AmazonHeaders.invokedFunctionARN, - "arn:aws:lambda:us-east-1:\(Int16.random(in: Int16.min ... Int16.max)):function:custom-runtime" - ), - (AmazonHeaders.traceID, "Root=\(AmazonHeaders.generateXRayTraceID());Sampled=1"), - (AmazonHeaders.deadline, "\(DispatchWallTime.distantFuture.millisSinceEpoch)"), - ] - return response - } - } - - enum InvocationState { - case waitingForInvocation(EventLoopPromise) - case waitingForLambdaRequest - case waitingForLambdaResponse(Invocation) + // unknown call + default: + return .init(status: .notFound) } } - enum ServerError: Error { - case notReady - case cantBind + private func sendResponse( + response: LocalServerResponse, + outbound: NIOAsyncChannelOutboundWriter + ) async throws { + var headers = HTTPHeaders(response.headers ?? []) + headers.add(name: "Content-Length", value: "\(response.body?.readableBytes ?? 0)") + + self.logger.trace("Writing response", metadata: ["requestId": "\(response.requestId ?? "")"]) + try await outbound.write( + HTTPServerResponsePart.head( + HTTPResponseHead( + version: .init(major: 1, minor: 1), + status: response.status, + headers: headers + ) + ) + ) + if let body = response.body { + try await outbound.write(HTTPServerResponsePart.body(.byteBuffer(body))) + } + + try await outbound.write(HTTPServerResponsePart.end(nil)) + } + + /// A shared data structure to store the current invocation or response requests and the continuation objects. + /// This data structure is shared between instances of the HTTPHandler + /// (one instance to serve requests from the Lambda function and one instance to serve requests from the client invoking the lambda function). + private final class Pool: AsyncSequence, AsyncIteratorProtocol, Sendable where T: Sendable { + typealias Element = T + + private let _buffer = Mutex>(.init()) + private let _continuation = Mutex?>(nil) + + /// retrieve the first element from the buffer + public func popFirst() async -> T? { + self._buffer.withLock { $0.popFirst() } + } + + /// enqueue an element, or give it back immediately to the iterator if it is waiting for an element + public func push(_ invocation: T) async { + // if the iterator is waiting for an element, give it to it + // otherwise, enqueue the element + if let continuation = self._continuation.withLock({ $0 }) { + self._continuation.withLock { $0 = nil } + continuation.resume(returning: invocation) + } else { + self._buffer.withLock { $0.append(invocation) } + } + } + + func next() async throws -> T? { + + // exit the async for loop if the task is cancelled + guard !Task.isCancelled else { + return nil + } + + if let element = await self.popFirst() { + return element + } else { + // we can't return nil if there is nothing to dequeue otherwise the async for loop will stop + // wait for an element to be enqueued + return try await withCheckedThrowingContinuation { (continuation: CheckedContinuation) in + // store the continuation for later, when an element is enqueued + self._continuation.withLock { + $0 = continuation + } + } + } + } + + func makeAsyncIterator() -> Pool { + self + } + } + + private struct LocalServerResponse: Sendable { + let requestId: String? + let status: HTTPResponseStatus + let headers: [(String, String)]? + let body: ByteBuffer? + init(id: String? = nil, status: HTTPResponseStatus, headers: [(String, String)]? = nil, body: ByteBuffer? = nil) + { + self.requestId = id + self.status = status + self.headers = headers + self.body = body + } + } + + private struct LocalServerInvocation: Sendable { + let requestId: String + let request: ByteBuffer + + func makeResponse(status: HTTPResponseStatus) -> LocalServerResponse { + + // required headers + let headers = [ + (AmazonHeaders.requestID, self.requestId), + ( + AmazonHeaders.invokedFunctionARN, + "arn:aws:lambda:us-east-1:\(Int16.random(in: Int16.min ... Int16.max)):function:custom-runtime" + ), + (AmazonHeaders.traceID, "Root=\(AmazonHeaders.generateXRayTraceID());Sampled=1"), + (AmazonHeaders.deadline, "\(DispatchWallTime.distantFuture.millisSinceEpoch)"), + ] + + return LocalServerResponse(id: self.requestId, status: status, headers: headers, body: self.request) + } } } #endif From 02821fe8f7e2a5afa5f0168d3b321deb8b84c05e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A9bastien=20Stormacq?= Date: Wed, 22 Jan 2025 13:02:01 +0100 Subject: [PATCH 06/24] [test] Update mock server for Swift 6 compliance (#463) Rewrite to MockServer (used only for performance testing and part of a separate target) to comply with Swift 6 language mode and concurrency. The new MockServer now uses `NIOAsyncChannel` and structured concurrency. Instead of adding support to [MAX_REQUEST](https://github.com/swift-server/swift-aws-lambda-runtime/blob/11756b4e00ca75894826b41666bdae506b6eb496/Sources/AWSLambdaRuntimeCore/LambdaConfiguration.swift#L53) environment variable like v1 did, we implemented support for `MAX_REQUEST` environment variable in the MockServer itself. It closes the connection and shutdown the server after servicing MAX_INVOCATIONS Lambda requests). This allow to add the MAX_REQUEST penalty on the MockServer and not on the LambdaRuntimeClient. However, currently, the LambdaRuntimeClient does not shutdown when the MockServer ends. I created https://github.com/swift-server/swift-aws-lambda-runtime/issues/465 to track this issue. See https://github.com/swift-server/swift-aws-lambda-runtime/issues/377 --- Package.swift | 6 +- Sources/MockServer/MockHTTPServer.swift | 298 ++++++++++++++++++++++++ Sources/MockServer/main.swift | 177 -------------- 3 files changed, 301 insertions(+), 180 deletions(-) create mode 100644 Sources/MockServer/MockHTTPServer.swift delete mode 100644 Sources/MockServer/main.swift diff --git a/Package.swift b/Package.swift index d2c92fdc..96068884 100644 --- a/Package.swift +++ b/Package.swift @@ -17,7 +17,7 @@ let package = Package( .library(name: "AWSLambdaTesting", targets: ["AWSLambdaTesting"]), ], dependencies: [ - .package(url: "https://github.com/apple/swift-nio.git", from: "2.76.0"), + .package(url: "https://github.com/apple/swift-nio.git", from: "2.77.0"), .package(url: "https://github.com/apple/swift-log.git", from: "1.5.4"), ], targets: [ @@ -89,11 +89,11 @@ let package = Package( .executableTarget( name: "MockServer", dependencies: [ + .product(name: "Logging", package: "swift-log"), .product(name: "NIOHTTP1", package: "swift-nio"), .product(name: "NIOCore", package: "swift-nio"), .product(name: "NIOPosix", package: "swift-nio"), - ], - swiftSettings: [.swiftLanguageMode(.v5)] + ] ), ] ) diff --git a/Sources/MockServer/MockHTTPServer.swift b/Sources/MockServer/MockHTTPServer.swift new file mode 100644 index 00000000..0849e325 --- /dev/null +++ b/Sources/MockServer/MockHTTPServer.swift @@ -0,0 +1,298 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the SwiftAWSLambdaRuntime open source project +// +// Copyright (c) 2017-2025 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 Logging +import NIOCore +import NIOHTTP1 +import NIOPosix +import Synchronization + +// for UUID and Date +#if canImport(FoundationEssentials) +import FoundationEssentials +#else +import Foundation +#endif + +@main +struct HttpServer { + /// The server's host. (default: 127.0.0.1) + private let host: String + /// The server's port. (default: 7000) + private let port: Int + /// The server's event loop group. (default: MultiThreadedEventLoopGroup.singleton) + private let eventLoopGroup: MultiThreadedEventLoopGroup + /// the mode. Are we mocking a server for a Lambda function that expects a String or a JSON document? (default: string) + private let mode: Mode + /// the number of connections this server must accept before shutting down (default: 1) + private let maxInvocations: Int + /// the logger (control verbosity with LOG_LEVEL environment variable) + private let logger: Logger + + static func main() async throws { + var log = Logger(label: "MockServer") + log.logLevel = env("LOG_LEVEL").flatMap(Logger.Level.init) ?? .info + + let server = HttpServer( + host: env("HOST") ?? "127.0.0.1", + port: env("PORT").flatMap(Int.init) ?? 7000, + eventLoopGroup: .singleton, + mode: env("MODE").flatMap(Mode.init) ?? .string, + maxInvocations: env("MAX_INVOCATIONS").flatMap(Int.init) ?? 1, + logger: log + ) + try await server.run() + } + + /// This method starts the server and handles one unique incoming connections + /// The Lambda function will send two HTTP requests over this connection: one for the next invocation and one for the response. + private func run() async throws { + let channel = try await ServerBootstrap(group: self.eventLoopGroup) + .serverChannelOption(.backlog, value: 256) + .serverChannelOption(.socketOption(.so_reuseaddr), value: 1) + .childChannelOption(.maxMessagesPerRead, value: 1) + .bind( + host: self.host, + port: self.port + ) { channel in + channel.eventLoop.makeCompletedFuture { + + try channel.pipeline.syncOperations.configureHTTPServerPipeline( + withErrorHandling: true + ) + + return try NIOAsyncChannel( + wrappingChannelSynchronously: channel, + configuration: NIOAsyncChannel.Configuration( + inboundType: HTTPServerRequestPart.self, + outboundType: HTTPServerResponsePart.self + ) + ) + } + } + + logger.info( + "Server started and listening", + metadata: [ + "host": "\(channel.channel.localAddress?.ipAddress?.debugDescription ?? "")", + "port": "\(channel.channel.localAddress?.port ?? 0)", + "maxInvocations": "\(self.maxInvocations)", + ] + ) + + // This counter is used to track the number of incoming connections. + // This mock servers accepts n TCP connection then shutdowns + let connectionCounter = SharedCounter(maxValue: self.maxInvocations) + + // We are handling each incoming connection in a separate child task. It is important + // to use a discarding task group here which automatically discards finished child tasks. + // A normal task group retains all child tasks and their outputs in memory until they are + // consumed by iterating the group or by exiting the group. Since, we are never consuming + // the results of the group we need the group to automatically discard them; otherwise, this + // would result in a memory leak over time. + try await withThrowingDiscardingTaskGroup { group in + try await channel.executeThenClose { inbound in + for try await connectionChannel in inbound { + + let counter = connectionCounter.current() + logger.trace("Handling new connection", metadata: ["connectionNumber": "\(counter)"]) + + group.addTask { + await self.handleConnection(channel: connectionChannel) + logger.trace("Done handling connection", metadata: ["connectionNumber": "\(counter)"]) + } + + if connectionCounter.increment() { + logger.info( + "Maximum number of connections reached, shutting down after current connection", + metadata: ["maxConnections": "\(self.maxInvocations)"] + ) + break // this causes the server to shutdown after handling the connection + } + } + } + } + logger.info("Server shutting down") + } + + /// This method handles a single connection by responsing hard coded value to a Lambda function request. + /// It handles two requests: one for the next invocation and one for the response. + /// when the maximum number of requests is reached, it closes the connection. + private func handleConnection( + channel: NIOAsyncChannel + ) async { + + var requestHead: HTTPRequestHead! + var requestBody: ByteBuffer? + + // each Lambda invocation results in TWO HTTP requests (next and response) + let requestCount = SharedCounter(maxValue: 2) + + // Note that this method is non-throwing and we are catching any error. + // We do this since we don't want to tear down the whole server when a single connection + // encounters an error. + do { + try await channel.executeThenClose { inbound, outbound in + for try await inboundData in inbound { + let requestNumber = requestCount.current() + logger.trace("Handling request", metadata: ["requestNumber": "\(requestNumber)"]) + + if case .head(let head) = inboundData { + logger.trace("Received request head", metadata: ["head": "\(head)"]) + requestHead = head + } + if case .body(let body) = inboundData { + logger.trace("Received request body", metadata: ["body": "\(body)"]) + requestBody = body + } + if case .end(let end) = inboundData { + logger.trace("Received request end", metadata: ["end": "\(String(describing: end))"]) + + precondition(requestHead != nil, "Received .end without .head") + let (responseStatus, responseHeaders, responseBody) = self.processRequest( + requestHead: requestHead, + requestBody: requestBody + ) + + try await self.sendResponse( + responseStatus: responseStatus, + responseHeaders: responseHeaders, + responseBody: responseBody, + outbound: outbound + ) + + requestHead = nil + + if requestCount.increment() { + logger.info( + "Maximum number of requests reached, closing this connection", + metadata: ["maxRequest": "2"] + ) + break // this finishes handiling request on this connection + } + } + } + } + } catch { + logger.error("Hit error: \(error)") + } + } + /// This function process the requests and return an hard-coded response (string or JSON depending on the mode). + /// We ignore the requestBody. + private func processRequest( + requestHead: HTTPRequestHead, + requestBody: ByteBuffer? + ) -> (HTTPResponseStatus, [(String, String)], String) { + var responseStatus: HTTPResponseStatus = .ok + var responseBody: String = "" + var responseHeaders: [(String, String)] = [] + + logger.trace( + "Processing request", + metadata: ["VERB": "\(requestHead.method)", "URI": "\(requestHead.uri)"] + ) + + if requestHead.uri.hasSuffix("/next") { + responseStatus = .accepted + + let requestId = UUID().uuidString + switch self.mode { + case .string: + responseBody = "\"Seb\"" // must be a valid JSON document + case .json: + responseBody = "{ \"name\": \"Seb\", \"age\" : 52 }" + } + let deadline = Int64(Date(timeIntervalSinceNow: 60).timeIntervalSince1970 * 1000) + responseHeaders = [ + (AmazonHeaders.requestID, requestId), + (AmazonHeaders.invokedFunctionARN, "arn:aws:lambda:us-east-1:123456789012:function:custom-runtime"), + (AmazonHeaders.traceID, "Root=1-5bef4de7-ad49b0e87f6ef6c87fc2e700;Parent=9a9197af755a6419;Sampled=1"), + (AmazonHeaders.deadline, String(deadline)), + ] + } else if requestHead.uri.hasSuffix("/response") { + responseStatus = .accepted + } else if requestHead.uri.hasSuffix("/error") { + responseStatus = .ok + } else { + responseStatus = .notFound + } + logger.trace("Returning response: \(responseStatus), \(responseHeaders), \(responseBody)") + return (responseStatus, responseHeaders, responseBody) + } + + private func sendResponse( + responseStatus: HTTPResponseStatus, + responseHeaders: [(String, String)], + responseBody: String, + outbound: NIOAsyncChannelOutboundWriter + ) async throws { + var headers = HTTPHeaders(responseHeaders) + headers.add(name: "Content-Length", value: "\(responseBody.utf8.count)") + headers.add(name: "KeepAlive", value: "timeout=1, max=2") + + logger.trace("Writing response head") + try await outbound.write( + HTTPServerResponsePart.head( + HTTPResponseHead( + version: .init(major: 1, minor: 1), // use HTTP 1.1 it keeps connection alive between requests + status: responseStatus, + headers: headers + ) + ) + ) + logger.trace("Writing response body") + try await outbound.write(HTTPServerResponsePart.body(.byteBuffer(ByteBuffer(string: responseBody)))) + logger.trace("Writing response end") + try await outbound.write(HTTPServerResponsePart.end(nil)) + } + + private enum Mode: String { + case string + case json + } + + private static func env(_ name: String) -> String? { + guard let value = getenv(name) else { + return nil + } + return String(cString: value) + } + + private enum AmazonHeaders { + static let requestID = "Lambda-Runtime-Aws-Request-Id" + static let traceID = "Lambda-Runtime-Trace-Id" + static let clientContext = "X-Amz-Client-Context" + static let cognitoIdentity = "X-Amz-Cognito-Identity" + static let deadline = "Lambda-Runtime-Deadline-Ms" + static let invokedFunctionARN = "Lambda-Runtime-Invoked-Function-Arn" + } + + private final class SharedCounter: Sendable { + private let counterMutex = Mutex(0) + private let maxValue: Int + + init(maxValue: Int) { + self.maxValue = maxValue + } + func current() -> Int { + counterMutex.withLock { $0 } + } + func increment() -> Bool { + counterMutex.withLock { + $0 += 1 + return $0 >= maxValue + } + } + } +} diff --git a/Sources/MockServer/main.swift b/Sources/MockServer/main.swift deleted file mode 100644 index 1b8466f9..00000000 --- a/Sources/MockServer/main.swift +++ /dev/null @@ -1,177 +0,0 @@ -//===----------------------------------------------------------------------===// -// -// This source file is part of the SwiftAWSLambdaRuntime open source project -// -// Copyright (c) 2017-2018 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 Dispatch -import NIOCore -import NIOHTTP1 -import NIOPosix - -#if canImport(FoundationEssentials) -import FoundationEssentials -#else -import Foundation -#endif - -struct MockServer { - private let group: EventLoopGroup - private let host: String - private let port: Int - private let mode: Mode - - public init() { - self.group = MultiThreadedEventLoopGroup(numberOfThreads: System.coreCount) - self.host = env("HOST") ?? "127.0.0.1" - self.port = env("PORT").flatMap(Int.init) ?? 7000 - self.mode = env("MODE").flatMap(Mode.init) ?? .string - } - - func start() throws { - let bootstrap = ServerBootstrap(group: group) - .serverChannelOption(ChannelOptions.socket(SocketOptionLevel(SOL_SOCKET), SO_REUSEADDR), value: 1) - .childChannelInitializer { channel in - channel.pipeline.configureHTTPServerPipeline(withErrorHandling: true).flatMap { _ in - channel.pipeline.addHandler(HTTPHandler(mode: self.mode)) - } - } - try bootstrap.bind(host: self.host, port: self.port).flatMap { channel -> EventLoopFuture in - guard let localAddress = channel.localAddress else { - return channel.eventLoop.makeFailedFuture(ServerError.cantBind) - } - print("\(self) started and listening on \(localAddress)") - return channel.eventLoop.makeSucceededFuture(()) - }.wait() - } -} - -final class HTTPHandler: ChannelInboundHandler { - public typealias InboundIn = HTTPServerRequestPart - public typealias OutboundOut = HTTPServerResponsePart - - private let mode: Mode - - private var pending = CircularBuffer<(head: HTTPRequestHead, body: ByteBuffer?)>() - - public init(mode: Mode) { - self.mode = mode - } - - func channelRead(context: ChannelHandlerContext, data: NIOAny) { - let requestPart = unwrapInboundIn(data) - - switch requestPart { - case .head(let head): - self.pending.append((head: head, body: nil)) - case .body(var buffer): - var request = self.pending.removeFirst() - if request.body == nil { - request.body = buffer - } else { - request.body!.writeBuffer(&buffer) - } - self.pending.prepend(request) - case .end: - let request = self.pending.removeFirst() - self.processRequest(context: context, request: request) - } - } - - func processRequest(context: ChannelHandlerContext, request: (head: HTTPRequestHead, body: ByteBuffer?)) { - var responseStatus: HTTPResponseStatus - var responseBody: String? - var responseHeaders: [(String, String)]? - - if request.head.uri.hasSuffix("/next") { - let requestId = UUID().uuidString - responseStatus = .ok - switch self.mode { - case .string: - responseBody = requestId - case .json: - responseBody = "{ \"body\": \"\(requestId)\" }" - } - let deadline = Int64(Date(timeIntervalSinceNow: 60).timeIntervalSince1970 * 1000) - responseHeaders = [ - (AmazonHeaders.requestID, requestId), - (AmazonHeaders.invokedFunctionARN, "arn:aws:lambda:us-east-1:123456789012:function:custom-runtime"), - (AmazonHeaders.traceID, "Root=1-5bef4de7-ad49b0e87f6ef6c87fc2e700;Parent=9a9197af755a6419;Sampled=1"), - (AmazonHeaders.deadline, String(deadline)), - ] - } else if request.head.uri.hasSuffix("/response") { - responseStatus = .accepted - } else { - responseStatus = .notFound - } - self.writeResponse(context: context, status: responseStatus, headers: responseHeaders, body: responseBody) - } - - func writeResponse( - context: ChannelHandlerContext, - status: HTTPResponseStatus, - headers: [(String, String)]? = nil, - body: String? = nil - ) { - var headers = HTTPHeaders(headers ?? []) - headers.add(name: "content-length", value: "\(body?.utf8.count ?? 0)") - let head = HTTPResponseHead(version: HTTPVersion(major: 1, minor: 1), status: status, headers: headers) - - context.write(wrapOutboundOut(.head(head))).whenFailure { error in - print("\(self) write error \(error)") - } - - if let b = body { - var buffer = context.channel.allocator.buffer(capacity: b.utf8.count) - buffer.writeString(b) - context.write(wrapOutboundOut(.body(.byteBuffer(buffer)))).whenFailure { error in - print("\(self) write error \(error)") - } - } - - context.writeAndFlush(wrapOutboundOut(.end(nil))).whenComplete { result in - if case .failure(let error) = result { - print("\(self) write error \(error)") - } - } - } -} - -enum ServerError: Error { - case notReady - case cantBind -} - -enum AmazonHeaders { - static let requestID = "Lambda-Runtime-Aws-Request-Id" - static let traceID = "Lambda-Runtime-Trace-Id" - static let clientContext = "X-Amz-Client-Context" - static let cognitoIdentity = "X-Amz-Cognito-Identity" - static let deadline = "Lambda-Runtime-Deadline-Ms" - static let invokedFunctionARN = "Lambda-Runtime-Invoked-Function-Arn" -} - -enum Mode: String { - case string - case json -} - -func env(_ name: String) -> String? { - guard let value = getenv(name) else { - return nil - } - return String(cString: value) -} - -// main -let server = MockServer() -try! server.start() -dispatchMain() From 7aa746f07e0ec76c39f2581f0250d9c8d17b0a4b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A9bastien=20Stormacq?= Date: Wed, 22 Jan 2025 18:53:57 +0100 Subject: [PATCH 07/24] [core] LocalServer - report the error to the continuation (#471) fix https://github.com/swift-server/swift-aws-lambda-runtime/issues/470 --- Sources/AWSLambdaRuntimeCore/Lambda+LocalServer.swift | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/Sources/AWSLambdaRuntimeCore/Lambda+LocalServer.swift b/Sources/AWSLambdaRuntimeCore/Lambda+LocalServer.swift index a3d98aa2..fa907def 100644 --- a/Sources/AWSLambdaRuntimeCore/Lambda+LocalServer.swift +++ b/Sources/AWSLambdaRuntimeCore/Lambda+LocalServer.swift @@ -55,7 +55,13 @@ extension Lambda { // this call will return when the server calls continuation.resume() try await withCheckedThrowingContinuation { (continuation: CheckedContinuation) in group.addTask { - try await LambdaHttpServer(invocationEndpoint: invocationEndpoint).start(continuation: continuation) + do { + try await LambdaHttpServer(invocationEndpoint: invocationEndpoint).start( + continuation: continuation + ) + } catch { + continuation.resume(throwing: error) + } } } // now that server is started, run the Lambda function itself From dbc2f98b628264bca533b8acd8703e90c19aae81 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A9bastien=20Stormacq?= Date: Wed, 29 Jan 2025 13:26:56 +0100 Subject: [PATCH 08/24] add local testing instructions to hello world example (#474) The local testing instruction are missing from the hello world example. The format of the payload is not intuitive or trivial to guess. We need to make it explicit --- Examples/HelloWorld/README.md | 29 +++++++++++++++++++++++++++++ 1 file changed, 29 insertions(+) diff --git a/Examples/HelloWorld/README.md b/Examples/HelloWorld/README.md index 73b51595..245233c3 100644 --- a/Examples/HelloWorld/README.md +++ b/Examples/HelloWorld/README.md @@ -12,6 +12,35 @@ The handler is `(event: String, context: LambdaContext)`. The function takes two The function return value will be encoded as your Lambda function response. +## Test locally + +You can test your function locally before deploying it to AWS Lambda. + +To start the local function, type the following commands: + +```bash +swift run +``` + +It will compile your code and start the local server. You know the local server is ready to accept connections when you see this message. + +```txt +Building for debugging... +[1/1] Write swift-version--644A47CB88185983.txt +Build of product 'MyLambda' complete! (0.31s) +2025-01-29T12:44:48+0100 info LocalServer : host="127.0.0.1" port=7000 [AWSLambdaRuntimeCore] Server started and listening +``` + +Then, from another Terminal, send your payload with `curl`. Note that the payload must be a valid JSON string. In the case of this function that accepts a simple String, it means the String must be wrapped in between double quotes. + +```bash +curl -d '"seb"' http://127.0.0.1:7000/invoke +"Hello seb" +``` + +> [!IMPORTANT] +> The local server is only available in `DEBUG` mode. It will not start with `swift -c release run`. + ## Build & Package To build & archive the package, type the following commands. From 4d7d23b9c1a6e055d52979a689e77e1004fd5dd4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A9bastien=20Stormacq?= Date: Wed, 29 Jan 2025 23:12:47 +0100 Subject: [PATCH 09/24] [examples] Update to Swift Lambda Events 1.0.0 (#475) Now that the swift Lambda event library has been tagged 1.0.0, update all the examples to use `from: "1.0.0"` instead of `main` branch --- Examples/APIGateway+LambdaAuthorizer/Package.swift | 2 +- Examples/APIGateway/Package.swift | 2 +- Examples/CDK/Package.swift | 2 +- Examples/S3_AWSSDK/Package.swift | 2 +- Examples/S3_Soto/Package.swift | 2 +- Examples/Testing/Package.swift | 2 +- 6 files changed, 6 insertions(+), 6 deletions(-) diff --git a/Examples/APIGateway+LambdaAuthorizer/Package.swift b/Examples/APIGateway+LambdaAuthorizer/Package.swift index 574bdbbe..1373758e 100644 --- a/Examples/APIGateway+LambdaAuthorizer/Package.swift +++ b/Examples/APIGateway+LambdaAuthorizer/Package.swift @@ -21,7 +21,7 @@ let package = Package( 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"), - .package(url: "https://github.com/swift-server/swift-aws-lambda-events.git", branch: "main"), + .package(url: "https://github.com/swift-server/swift-aws-lambda-events.git", from: "1.0.0"), ], targets: [ .executableTarget( diff --git a/Examples/APIGateway/Package.swift b/Examples/APIGateway/Package.swift index df13380d..b2373801 100644 --- a/Examples/APIGateway/Package.swift +++ b/Examples/APIGateway/Package.swift @@ -14,7 +14,7 @@ let package = Package( 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"), - .package(url: "https://github.com/swift-server/swift-aws-lambda-events.git", branch: "main"), + .package(url: "https://github.com/swift-server/swift-aws-lambda-events.git", from: "1.0.0"), ], targets: [ .executableTarget( diff --git a/Examples/CDK/Package.swift b/Examples/CDK/Package.swift index df13380d..b2373801 100644 --- a/Examples/CDK/Package.swift +++ b/Examples/CDK/Package.swift @@ -14,7 +14,7 @@ let package = Package( 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"), - .package(url: "https://github.com/swift-server/swift-aws-lambda-events.git", branch: "main"), + .package(url: "https://github.com/swift-server/swift-aws-lambda-events.git", from: "1.0.0"), ], targets: [ .executableTarget( diff --git a/Examples/S3_AWSSDK/Package.swift b/Examples/S3_AWSSDK/Package.swift index fa1426fd..0eec7c6b 100644 --- a/Examples/S3_AWSSDK/Package.swift +++ b/Examples/S3_AWSSDK/Package.swift @@ -14,7 +14,7 @@ let package = Package( 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"), - .package(url: "https://github.com/swift-server/swift-aws-lambda-events", branch: "main"), + .package(url: "https://github.com/swift-server/swift-aws-lambda-events", from: "1.0.0"), .package(url: "https://github.com/awslabs/aws-sdk-swift", from: "1.0.0"), ], targets: [ diff --git a/Examples/S3_Soto/Package.swift b/Examples/S3_Soto/Package.swift index 25b891a8..97e5a9fb 100644 --- a/Examples/S3_Soto/Package.swift +++ b/Examples/S3_Soto/Package.swift @@ -16,7 +16,7 @@ let package = Package( // 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"), - .package(url: "https://github.com/swift-server/swift-aws-lambda-events", branch: "main"), + .package(url: "https://github.com/swift-server/swift-aws-lambda-events", from: "1.0.0"), ], targets: [ .executableTarget( diff --git a/Examples/Testing/Package.swift b/Examples/Testing/Package.swift index 79aab087..db196325 100644 --- a/Examples/Testing/Package.swift +++ b/Examples/Testing/Package.swift @@ -14,7 +14,7 @@ let package = Package( 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"), - .package(url: "https://github.com/swift-server/swift-aws-lambda-events.git", branch: "main"), + .package(url: "https://github.com/swift-server/swift-aws-lambda-events.git", from: "1.0.0"), ], targets: [ .executableTarget( From bd0ec626464f1c1232e7baf2cabf4138e1f770a7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A9bastien=20Stormacq?= Date: Fri, 31 Jan 2025 17:03:29 +0100 Subject: [PATCH 10/24] Remove platform dance in some examples (#476) Remove `#if os` on some examples --- Examples/APIGateway+LambdaAuthorizer/Package.swift | 6 ------ Examples/HelloJSON/Package.swift | 8 +------- 2 files changed, 1 insertion(+), 13 deletions(-) diff --git a/Examples/APIGateway+LambdaAuthorizer/Package.swift b/Examples/APIGateway+LambdaAuthorizer/Package.swift index 1373758e..03117835 100644 --- a/Examples/APIGateway+LambdaAuthorizer/Package.swift +++ b/Examples/APIGateway+LambdaAuthorizer/Package.swift @@ -5,12 +5,6 @@ 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: [.macOS(.v15)], diff --git a/Examples/HelloJSON/Package.swift b/Examples/HelloJSON/Package.swift index 506f0678..4ec236fd 100644 --- a/Examples/HelloJSON/Package.swift +++ b/Examples/HelloJSON/Package.swift @@ -5,15 +5,9 @@ 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, + platforms: [.macOS(.v15)], products: [ .executable(name: "HelloJSON", targets: ["HelloJSON"]) ], From 0777c801726a1c55dac6913977ca11e411c4cbf9 Mon Sep 17 00:00:00 2001 From: Paul Toffoloni <69189821+ptoffy@users.noreply.github.com> Date: Tue, 11 Feb 2025 17:22:12 +0100 Subject: [PATCH 11/24] Add `S3EventNotifier` example (#477) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Add `S3EventNotifier` example ### Motivation: There is currently no example regarding an AWS event triggered lambda, such as a lambda that gets invoked on an S3 object upload. I've had to look for a while to figure out that the `AWSLambdaEvents` exists and that it can be used for that (had to find out via a Java example! 😜) so I think this could be useful and a somewhat common use case ### Modifications: Added an example of a lambda that gets triggered when an object gets uploaded to an S3 bucket. I have not described how to set up the actual S3 event in AWS because I figured if you needed such a lambda you'd know, but I can describe that too in the README if needed --------- Co-authored-by: Sébastien Stormacq --- .github/workflows/pull_request.yml | 2 +- Examples/README.md | 4 +- Examples/S3EventNotifier/.gitignore | 9 ++ Examples/S3EventNotifier/Package.swift | 50 +++++++++++ Examples/S3EventNotifier/README.md | 94 +++++++++++++++++++++ Examples/S3EventNotifier/Sources/main.swift | 33 ++++++++ 6 files changed, 190 insertions(+), 2 deletions(-) create mode 100644 Examples/S3EventNotifier/.gitignore create mode 100644 Examples/S3EventNotifier/Package.swift create mode 100644 Examples/S3EventNotifier/README.md create mode 100644 Examples/S3EventNotifier/Sources/main.swift diff --git a/.github/workflows/pull_request.yml b/.github/workflows/pull_request.yml index 72c2a24c..9c1e88a3 100644 --- a/.github/workflows/pull_request.yml +++ b/.github/workflows/pull_request.yml @@ -36,7 +36,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: "[ 'APIGateway', 'APIGateway+LambdaAuthorizer', 'BackgroundTasks', 'HelloJSON', 'HelloWorld', 'ResourcesPackaging', 'S3_AWSSDK', 'S3_Soto', 'Streaming', 'Testing', 'Tutorial' ]" + examples: "[ 'APIGateway', 'APIGateway+LambdaAuthorizer', 'BackgroundTasks', 'HelloJSON', 'HelloWorld', 'ResourcesPackaging', 'S3EventNotifier', 'S3_AWSSDK', 'S3_Soto', 'Streaming', 'Testing', 'Tutorial' ]" archive_plugin_examples: "[ 'HelloWorld', 'ResourcesPackaging' ]" archive_plugin_enabled: true diff --git a/Examples/README.md b/Examples/README.md index 53ccc8bd..973df897 100644 --- a/Examples/README.md +++ b/Examples/README.md @@ -28,6 +28,8 @@ This directory contains example code for Lambda functions. - **[HelloWorld](HelloWorld/README.md)**: a simple Lambda function (requires [AWS CLI](https://docs.aws.amazon.com/cli/latest/userguide/getting-started-install.html)). +- **[S3EventNotifier](S3EventNotifier/README.md)**: a Lambda function that receives object-upload notifications from an [Amazon S3](https://docs.aws.amazon.com/AmazonS3/latest/userguide/Welcome.html) bucket. + - **[S3_AWSSDK](S3_AWSSDK/README.md)**: a Lambda function that uses the [AWS SDK for Swift](https://docs.aws.amazon.com/sdk-for-swift/latest/developer-guide/getting-started.html) to invoke an [Amazon S3](https://docs.aws.amazon.com/AmazonS3/latest/userguide/Welcome.html) API (requires [AWS SAM](https://aws.amazon.com/serverless/sam/)). - **[S3_Soto](S3_Soto/README.md)**: a Lambda function that uses [Soto](https://github.com/soto-project/soto) to invoke an [Amazon S3](https://docs.aws.amazon.com/AmazonS3/latest/userguide/Welcome.html) API (requires [AWS SAM](https://aws.amazon.com/serverless/sam/)). @@ -64,4 +66,4 @@ To obtain these keys, you need an AWS account: 4. **(Optional) Generate Temporary Security Credentials**: If you’re using temporary credentials (which are more secure for short-term access), use AWS Security Token Service (STS). You can call the `GetSessionToken` or `AssumeRole` API to generate temporary credentials, including a session token. -With these in hand, you can use AWS SigV4 to securely sign your requests and interact with AWS services from your Swift app. \ No newline at end of file +With these in hand, you can use AWS SigV4 to securely sign your requests and interact with AWS services from your Swift app. diff --git a/Examples/S3EventNotifier/.gitignore b/Examples/S3EventNotifier/.gitignore new file mode 100644 index 00000000..10edc03d --- /dev/null +++ b/Examples/S3EventNotifier/.gitignore @@ -0,0 +1,9 @@ +.DS_Store +/.build +/.index-build +/Packages +xcuserdata/ +DerivedData/ +.swiftpm/configuration/registries.json +.swiftpm/xcode/package.xcworkspace/contents.xcworkspacedata +.netrc diff --git a/Examples/S3EventNotifier/Package.swift b/Examples/S3EventNotifier/Package.swift new file mode 100644 index 00000000..6554b385 --- /dev/null +++ b/Examples/S3EventNotifier/Package.swift @@ -0,0 +1,50 @@ +// swift-tools-version: 6.0 +import PackageDescription + +// needed for CI to test the local version of the library +import struct Foundation.URL + +let package = Package( + name: "S3EventNotifier", + platforms: [.macOS(.v15)], + dependencies: [ + .package(url: "https://github.com/swift-server/swift-aws-lambda-runtime.git", branch: "main"), + .package(url: "https://github.com/swift-server/swift-aws-lambda-events", branch: "main"), + ], + targets: [ + .executableTarget( + name: "S3EventNotifier", + dependencies: [ + .product(name: "AWSLambdaRuntime", package: "swift-aws-lambda-runtime"), + .product(name: "AWSLambdaEvents", package: "swift-aws-lambda-events"), + ] + ) + ] +) + +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/S3EventNotifier/README.md b/Examples/S3EventNotifier/README.md new file mode 100644 index 00000000..3ccee239 --- /dev/null +++ b/Examples/S3EventNotifier/README.md @@ -0,0 +1,94 @@ +# S3 Event Notifier + +This example demonstrates how to write a Lambda that is invoked by an event originating from Amazon S3, such as a new object being uploaded to a bucket. + +## Code + +In this example the Lambda function receives an `S3Event` object defined in the `AWSLambdaEvents` library as input object. The `S3Event` object contains all the information about the S3 event that triggered the function, but what we are interested in is the bucket name and the object key, which are inside of a notification `Record`. The object contains an array of records, however since the Lambda function is triggered by a single event, we can safely assume that there is only one record in the array: the first one. Inside of this record, we can find the bucket name and the object key: + +```swift +guard let s3NotificationRecord = event.records.first else { + throw LambdaError.noNotificationRecord +} + +let bucket = s3NotificationRecord.s3.bucket.name +let key = s3NotificationRecord.s3.object.key.replacingOccurrences(of: "+", with: " ") +``` + +The key is URL encoded, so we replace the `+` with a space. + +## Build & Package + +To build & archive the package you can use the following commands: + +```bash +swift build +swift package archive --allow-network-connections docker +``` + +If there are no errors, a ZIP file should be ready to deploy, located at `.build/plugins/AWSLambdaPackager/outputs/AWSLambdaPackager/S3EventNotifier/S3EventNotifier.zip`. + +## Deploy + +> [!IMPORTANT] +> The Lambda function and the S3 bucket must be located in the same AWS Region. In the code below, we use `eu-west-1` (Ireland). + +To deploy the Lambda function, you can use the `aws` command line: + +```bash +REGION=eu-west-1 +aws lambda create-function \ + --region "${REGION}" \ + --function-name S3EventNotifier \ + --zip-file fileb://.build/plugins/AWSLambdaPackager/outputs/AWSLambdaPackager/S3EventNotifier/S3EventNotifier.zip \ + --runtime provided.al2 \ + --handler provided \ + --architectures arm64 \ + --role arn:aws:iam:::role/lambda_basic_execution +``` + +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 define `REGION` with the region where you want to deploy your Lambda function and replace `` with your actual AWS account ID (for example: 012345678901). + +Besides deploying the Lambda function you also need to create the S3 bucket and configure it to send events to the Lambda function. You can do this using the following commands: + +```bash +REGION=eu-west-1 + +aws s3api create-bucket \ + --region "${REGION}" \ + --bucket my-test-bucket \ + --create-bucket-configuration LocationConstraint="${REGION}" + +aws lambda add-permission \ + --region "${REGION}" \ + --function-name S3EventNotifier \ + --statement-id S3InvokeFunction \ + --action lambda:InvokeFunction \ + --principal s3.amazonaws.com \ + --source-arn arn:aws:s3:::my-test-bucket + +aws s3api put-bucket-notification-configuration \ + --region "${REGION}" \ + --bucket my-test-bucket \ + --notification-configuration '{ + "LambdaFunctionConfigurations": [{ + "LambdaFunctionArn": "arn:aws:lambda:${REGION}::function:S3EventNotifier", + "Events": ["s3:ObjectCreated:*"] + }] + }' + +touch testfile.txt && aws s3 cp testfile.txt s3://my-test-bucket/ +``` + +This will: + - create a bucket named `my-test-bucket` in the `$REGION` region; + - add a permission to the Lambda function to be invoked by Amazon S3; + - configure the bucket to send `s3:ObjectCreated:*` events to the Lambda function named `S3EventNotifier`; + - upload a file named `testfile.txt` to the bucket. + +Replace `my-test-bucket` with your bucket name (bucket names are unique globaly and this one is already taken). Also replace `REGION` environment variable with the AWS Region where you deployed the Lambda function and `` with your actual AWS account ID. + +> [!IMPORTANT] +> The Lambda function and the S3 bucket must be located in the same AWS Region. Adjust the code above according to your closest AWS Region. \ No newline at end of file diff --git a/Examples/S3EventNotifier/Sources/main.swift b/Examples/S3EventNotifier/Sources/main.swift new file mode 100644 index 00000000..9a55974e --- /dev/null +++ b/Examples/S3EventNotifier/Sources/main.swift @@ -0,0 +1,33 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the SwiftAWSLambdaRuntime open source project +// +// Copyright (c) 2025 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 AWSLambdaEvents +import AWSLambdaRuntime +import Foundation + +let runtime = LambdaRuntime { (event: S3Event, context: LambdaContext) async throws in + guard let s3NotificationRecord = event.records.first else { + context.logger.error("No S3 notification record found in the event") + return + } + + let bucket = s3NotificationRecord.s3.bucket.name + let key = s3NotificationRecord.s3.object.key.replacingOccurrences(of: "+", with: " ") + + context.logger.info("Received notification from S3 bucket '\(bucket)' for object with key '\(key)'") + + // Here you could, for example, notify an API or a messaging service +} + +try await runtime.run() From 40e2291532fdc0cf2e54f285bf33dc37b740646f Mon Sep 17 00:00:00 2001 From: Josh <29730338+mr-j-tree@users.noreply.github.com> Date: Sun, 16 Feb 2025 12:40:46 +0000 Subject: [PATCH 12/24] Quick typo fix. (#478) Fixed a glaring typo in the opening section. _[One line description of your change]_ ### Motivation: First thing I noticed when I started reading. ### Modifications: Swapped two letters around. ### Result: Peace and love for all humankind. --- readme.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/readme.md b/readme.md index e844dd0a..f945b1e1 100644 --- a/readme.md +++ b/readme.md @@ -1,7 +1,7 @@ > [!IMPORTANT] > The documentation included here refers to the Swift AWS Lambda Runtime v2 (code from the main branch). If you're developing for the runtime v1.x, check this [readme](https://github.com/swift-server/swift-aws-lambda-runtime/blob/v1/readme.md) instead. -This guide contains the follwoing sections: +This guide contains the following sections: - [The Swift AWS Lambda Runtime](#the-swift-aws-lambda-runtime) - [Pre-requisites](#pre-requisites) From d7780480f2f32c0dc90adbc14a5acbe4e6e700aa Mon Sep 17 00:00:00 2001 From: Fabian Fett Date: Thu, 20 Feb 2025 16:17:23 +0100 Subject: [PATCH 13/24] Enable Swift 6 mode! (#482) --- Package.swift | 3 +-- Sources/AWSLambdaRuntime/Lambda+Codable.swift | 2 +- .../AWSLambdaRuntimeCore/Lambda+Codable.swift | 6 +++--- .../Lambda+LocalServer.swift | 11 ++++++----- .../AWSLambdaRuntimeCore/LambdaHandlers.swift | 16 +++++++++------- .../LambdaRuntimeClient.swift | 14 +++++++++----- 6 files changed, 29 insertions(+), 23 deletions(-) diff --git a/Package.swift b/Package.swift index 96068884..0652d53b 100644 --- a/Package.swift +++ b/Package.swift @@ -36,8 +36,7 @@ let package = Package( .product(name: "NIOCore", package: "swift-nio"), .product(name: "NIOConcurrencyHelpers", package: "swift-nio"), .product(name: "NIOPosix", package: "swift-nio"), - ], - swiftSettings: [.swiftLanguageMode(.v5)] + ] ), .plugin( name: "AWSLambdaPackager", diff --git a/Sources/AWSLambdaRuntime/Lambda+Codable.swift b/Sources/AWSLambdaRuntime/Lambda+Codable.swift index fb6d2ca7..ccb37dd8 100644 --- a/Sources/AWSLambdaRuntime/Lambda+Codable.swift +++ b/Sources/AWSLambdaRuntime/Lambda+Codable.swift @@ -66,7 +66,7 @@ extension LambdaCodableAdapter { public init( encoder: JSONEncoder = JSONEncoder(), decoder: JSONDecoder = JSONDecoder(), - handler: Handler + handler: sending Handler ) where Output: Encodable, diff --git a/Sources/AWSLambdaRuntimeCore/Lambda+Codable.swift b/Sources/AWSLambdaRuntimeCore/Lambda+Codable.swift index 7a0a9a22..a77c0542 100644 --- a/Sources/AWSLambdaRuntimeCore/Lambda+Codable.swift +++ b/Sources/AWSLambdaRuntimeCore/Lambda+Codable.swift @@ -58,7 +58,7 @@ public struct LambdaHandlerAdapter< /// Initializes an instance given a concrete handler. /// - Parameter handler: The ``LambdaHandler`` conforming handler that is to be adapted to ``LambdaWithBackgroundProcessingHandler``. @inlinable - public init(handler: Handler) { + public init(handler: sending Handler) { self.handler = handler } @@ -98,7 +98,7 @@ public struct LambdaCodableAdapter< /// - decoder: The decoder object that will be used to decode the received `ByteBuffer` event into the generic `Event` type served to the `handler`. /// - handler: The handler object. @inlinable - public init(encoder: Encoder, decoder: Decoder, handler: Handler) where Output: Encodable { + public init(encoder: sending Encoder, decoder: sending Decoder, handler: sending Handler) where Output: Encodable { self.encoder = encoder self.decoder = decoder self.handler = handler @@ -109,7 +109,7 @@ public struct LambdaCodableAdapter< /// - decoder: The decoder object that will be used to decode the received `ByteBuffer` event into the generic `Event` type served to the `handler`. /// - handler: The handler object. @inlinable - public init(decoder: Decoder, handler: Handler) where Output == Void, Encoder == VoidEncoder { + public init(decoder: sending Decoder, handler: Handler) where Output == Void, Encoder == VoidEncoder { self.encoder = VoidEncoder() self.decoder = decoder self.handler = handler diff --git a/Sources/AWSLambdaRuntimeCore/Lambda+LocalServer.swift b/Sources/AWSLambdaRuntimeCore/Lambda+LocalServer.swift index fa907def..64a9acb7 100644 --- a/Sources/AWSLambdaRuntimeCore/Lambda+LocalServer.swift +++ b/Sources/AWSLambdaRuntimeCore/Lambda+LocalServer.swift @@ -174,13 +174,14 @@ private struct LambdaHttpServer { do { try await channel.executeThenClose { inbound, outbound in for try await inboundData in inbound { - if case .head(let head) = inboundData { + switch inboundData { + case .head(let head): requestHead = head - } - if case .body(let body) = inboundData { + + case .body(let body): requestBody = body - } - if case .end = inboundData { + + case .end: precondition(requestHead != nil, "Received .end without .head") // process the request let response = try await self.processRequest( diff --git a/Sources/AWSLambdaRuntimeCore/LambdaHandlers.swift b/Sources/AWSLambdaRuntimeCore/LambdaHandlers.swift index b76b453d..82a6eef3 100644 --- a/Sources/AWSLambdaRuntimeCore/LambdaHandlers.swift +++ b/Sources/AWSLambdaRuntimeCore/LambdaHandlers.swift @@ -154,7 +154,7 @@ public struct ClosureHandler: LambdaHandler { /// Initialize with a closure handler over generic `Input` and `Output` types. /// - Parameter body: The handler function written as a closure. - public init(body: @escaping (Event, LambdaContext) async throws -> Output) where Output: Encodable { + public init(body: sending @escaping (Event, LambdaContext) async throws -> Output) where Output: Encodable { self.body = body } @@ -192,8 +192,8 @@ extension LambdaRuntime { Encoder: LambdaOutputEncoder, Decoder: LambdaEventDecoder >( - encoder: Encoder, - decoder: Decoder, + encoder: sending Encoder, + decoder: sending Decoder, body: sending @escaping (Event, LambdaContext) async throws -> Output ) where @@ -205,13 +205,15 @@ extension LambdaRuntime { Encoder > { - let handler = LambdaCodableAdapter( + let closureHandler = ClosureHandler(body: body) + let streamingAdapter = LambdaHandlerAdapter(handler: closureHandler) + let codableWrapper = LambdaCodableAdapter( encoder: encoder, decoder: decoder, - handler: LambdaHandlerAdapter(handler: ClosureHandler(body: body)) + handler: streamingAdapter ) - self.init(handler: handler) + self.init(handler: codableWrapper) } /// Initialize an instance with a ``LambdaHandler`` defined in the form of a closure **with a `Void` return type**, an encoder, and a decoder. @@ -219,7 +221,7 @@ extension LambdaRuntime { /// - decoder: The decoder object that will be used to decode the incoming `ByteBuffer` event into the generic `Event` type. /// - body: The handler in the form of a closure. public convenience init( - decoder: Decoder, + decoder: sending Decoder, body: sending @escaping (Event, LambdaContext) async throws -> Void ) where diff --git a/Sources/AWSLambdaRuntimeCore/LambdaRuntimeClient.swift b/Sources/AWSLambdaRuntimeCore/LambdaRuntimeClient.swift index bbd16efa..228dc471 100644 --- a/Sources/AWSLambdaRuntimeCore/LambdaRuntimeClient.swift +++ b/Sources/AWSLambdaRuntimeCore/LambdaRuntimeClient.swift @@ -49,9 +49,13 @@ final actor LambdaRuntimeClient: LambdaRuntimeClientProtocol { } } + private typealias ConnectionContinuation = CheckedContinuation< + NIOLoopBound>, any Error + > + private enum ConnectionState { case disconnected - case connecting([CheckedContinuation, any Error>]) + case connecting([ConnectionContinuation]) case connected(Channel, LambdaChannelHandler) } @@ -158,7 +162,6 @@ final actor LambdaRuntimeClient: LambdaRuntimeClientProtocol { .sentResponse: fatalError("Invalid state: \(self.lambdaState)") } - } private func write(_ buffer: NIOCore.ByteBuffer) async throws { @@ -284,11 +287,11 @@ final actor LambdaRuntimeClient: LambdaRuntimeClientProtocol { case .connecting(var array): // Since we do get sequential invocations this case normally should never be hit. // We'll support it anyway. - return try await withCheckedThrowingContinuation { - (continuation: CheckedContinuation, any Error>) in + let loopBound = try await withCheckedThrowingContinuation { (continuation: ConnectionContinuation) in array.append(continuation) self.connectionState = .connecting(array) } + return loopBound.value case .connected(_, let handler): return handler } @@ -339,8 +342,9 @@ final actor LambdaRuntimeClient: LambdaRuntimeClientProtocol { case .connecting(let array): self.connectionState = .connected(channel, handler) defer { + let loopBound = NIOLoopBound(handler, eventLoop: self.eventLoop) for continuation in array { - continuation.resume(returning: handler) + continuation.resume(returning: loopBound) } } return handler From 230eeb505cf8e621a0597ae7fbfc9257fe7bfb84 Mon Sep 17 00:00:00 2001 From: Adam Fowler Date: Fri, 21 Feb 2025 14:54:53 +0000 Subject: [PATCH 14/24] Update swift-nio to 2.81.0 (#483) Update swift-nio to 2.81.0 to fix Swift 6 errors --- Package.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Package.swift b/Package.swift index 0652d53b..bcc45444 100644 --- a/Package.swift +++ b/Package.swift @@ -17,7 +17,7 @@ let package = Package( .library(name: "AWSLambdaTesting", targets: ["AWSLambdaTesting"]), ], dependencies: [ - .package(url: "https://github.com/apple/swift-nio.git", from: "2.77.0"), + .package(url: "https://github.com/apple/swift-nio.git", from: "2.81.0"), .package(url: "https://github.com/apple/swift-log.git", from: "1.5.4"), ], targets: [ From 64b417926d3ef3d2e08a1b12402d44b2a05c3446 Mon Sep 17 00:00:00 2001 From: mattcorey Date: Wed, 26 Feb 2025 17:42:27 -0500 Subject: [PATCH 15/24] Instructions for customizing the output path are incorrect (#484) Instructions for customizing the output path are incorrect ### Motivation: `displayHelpMessage` was incorrect - it claimed that the output path was customizable via a `--output-directory` flag, but the code actually looks for `--output-path` ### Modifications: Changing the help message here, instead of the code, in case existing projects already depend on `--output-path` ### Result: `displayHelpMessage` will show the correct instructions --- Plugins/AWSLambdaPackager/Plugin.swift | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Plugins/AWSLambdaPackager/Plugin.swift b/Plugins/AWSLambdaPackager/Plugin.swift index f6ac2b03..01e47b3d 100644 --- a/Plugins/AWSLambdaPackager/Plugin.swift +++ b/Plugins/AWSLambdaPackager/Plugin.swift @@ -307,7 +307,7 @@ struct AWSLambdaPackager: CommandPlugin { USAGE: swift package --allow-network-connections docker archive [--help] [--verbose] - [--output-directory ] + [--output-path ] [--products ] [--configuration debug | release] [--swift-version ] @@ -317,7 +317,7 @@ struct AWSLambdaPackager: CommandPlugin { OPTIONS: --verbose Produce verbose output for debugging. - --output-directory The path of the binary package. + --output-path The path of the binary package. (default is `.build/plugins/AWSLambdaPackager/outputs/...`) --products The list of executable targets to build. (default is taken from Package.swift) From 5de00c96c8e5cdb75be33ee6827dbe39adcdbff3 Mon Sep 17 00:00:00 2001 From: Fabian Fett Date: Thu, 27 Feb 2025 16:08:39 +0100 Subject: [PATCH 16/24] Fixes for Local Lambda Server (#486) --- Package.swift | 3 +- .../Lambda+LocalServer.swift | 253 ++++++++++++------ 2 files changed, 175 insertions(+), 81 deletions(-) diff --git a/Package.swift b/Package.swift index bcc45444..9eac9771 100644 --- a/Package.swift +++ b/Package.swift @@ -19,6 +19,7 @@ let package = Package( dependencies: [ .package(url: "https://github.com/apple/swift-nio.git", from: "2.81.0"), .package(url: "https://github.com/apple/swift-log.git", from: "1.5.4"), + .package(url: "https://github.com/apple/swift-collections.git", from: "1.1.4"), ], targets: [ .target( @@ -31,10 +32,10 @@ let package = Package( .target( name: "AWSLambdaRuntimeCore", dependencies: [ + .product(name: "DequeModule", package: "swift-collections"), .product(name: "Logging", package: "swift-log"), .product(name: "NIOHTTP1", package: "swift-nio"), .product(name: "NIOCore", package: "swift-nio"), - .product(name: "NIOConcurrencyHelpers", package: "swift-nio"), .product(name: "NIOPosix", package: "swift-nio"), ] ), diff --git a/Sources/AWSLambdaRuntimeCore/Lambda+LocalServer.swift b/Sources/AWSLambdaRuntimeCore/Lambda+LocalServer.swift index 64a9acb7..4bb113a7 100644 --- a/Sources/AWSLambdaRuntimeCore/Lambda+LocalServer.swift +++ b/Sources/AWSLambdaRuntimeCore/Lambda+LocalServer.swift @@ -13,9 +13,9 @@ //===----------------------------------------------------------------------===// #if DEBUG +import DequeModule import Dispatch import Logging -import NIOConcurrencyHelpers import NIOCore import NIOHTTP1 import NIOPosix @@ -47,24 +47,15 @@ extension Lambda { /// - note: This API is designed strictly for local testing and is behind a DEBUG flag static func withLocalServer( invocationEndpoint: String? = nil, - _ body: @escaping () async throws -> Void + _ body: sending @escaping () async throws -> Void ) async throws { + var logger = Logger(label: "LocalServer") + logger.logLevel = Lambda.env("LOG_LEVEL").flatMap(Logger.Level.init) ?? .info - // launch the local server and wait for it to be started before running the body - try await withThrowingTaskGroup(of: Void.self) { group in - // this call will return when the server calls continuation.resume() - try await withCheckedThrowingContinuation { (continuation: CheckedContinuation) in - group.addTask { - do { - try await LambdaHttpServer(invocationEndpoint: invocationEndpoint).start( - continuation: continuation - ) - } catch { - continuation.resume(throwing: error) - } - } - } - // now that server is started, run the Lambda function itself + try await LambdaHTTPServer.withLocalServer( + invocationEndpoint: invocationEndpoint, + logger: logger + ) { try await body() } } @@ -84,34 +75,46 @@ extension Lambda { /// 1. POST /invoke - the client posts the event to the lambda function /// /// This server passes the data received from /invoke POST request to the lambda function (GET /next) and then forwards the response back to the client. -private struct LambdaHttpServer { - private let logger: Logger - private let group: EventLoopGroup - private let host: String - private let port: Int +private struct LambdaHTTPServer { private let invocationEndpoint: String private let invocationPool = Pool() private let responsePool = Pool() - init(invocationEndpoint: String?) { - var logger = Logger(label: "LocalServer") - logger.logLevel = Lambda.env("LOG_LEVEL").flatMap(Logger.Level.init) ?? .info - self.logger = logger - self.group = MultiThreadedEventLoopGroup.singleton - self.host = "127.0.0.1" - self.port = 7000 + private init( + invocationEndpoint: String? + ) { self.invocationEndpoint = invocationEndpoint ?? "/invoke" } - func start(continuation: CheckedContinuation) async throws { - let channel = try await ServerBootstrap(group: self.group) + private enum TaskResult: Sendable { + case closureResult(Swift.Result) + case serverReturned(Swift.Result) + } + + struct UnsafeTransferBox: @unchecked Sendable { + let value: Value + + init(value: sending Value) { + self.value = value + } + } + + static func withLocalServer( + invocationEndpoint: String?, + host: String = "127.0.0.1", + port: Int = 7000, + eventLoopGroup: MultiThreadedEventLoopGroup = .singleton, + logger: Logger, + _ closure: sending @escaping () async throws -> Result + ) async throws -> Result { + let channel = try await ServerBootstrap(group: eventLoopGroup) .serverChannelOption(.backlog, value: 256) .serverChannelOption(.socketOption(.so_reuseaddr), value: 1) .childChannelOption(.maxMessagesPerRead, value: 1) .bind( - host: self.host, - port: self.port + host: host, + port: port ) { channel in channel.eventLoop.makeCompletedFuture { @@ -129,8 +132,6 @@ private struct LambdaHttpServer { } } - // notify the caller that the server is started - continuation.resume() logger.info( "Server started and listening", metadata: [ @@ -139,30 +140,87 @@ private struct LambdaHttpServer { ] ) - // We are handling each incoming connection in a separate child task. It is important - // to use a discarding task group here which automatically discards finished child tasks. - // A normal task group retains all child tasks and their outputs in memory until they are - // consumed by iterating the group or by exiting the group. Since, we are never consuming - // the results of the group we need the group to automatically discard them; otherwise, this - // would result in a memory leak over time. - try await withThrowingDiscardingTaskGroup { group in - try await channel.executeThenClose { inbound in - for try await connectionChannel in inbound { + let server = LambdaHTTPServer(invocationEndpoint: invocationEndpoint) - group.addTask { - logger.trace("Handling a new connection") - await self.handleConnection(channel: connectionChannel) - logger.trace("Done handling the connection") + // Sadly the Swift compiler does not understand that the passed in closure will only be + // invoked once. Because of this we need an unsafe transfer box here. Buuuh! + let closureBox = UnsafeTransferBox(value: closure) + let result = await withTaskGroup(of: TaskResult.self, returning: Swift.Result.self) { + group in + group.addTask { + let c = closureBox.value + do { + let result = try await c() + return .closureResult(.success(result)) + } catch { + return .closureResult(.failure(error)) + } + } + + group.addTask { + do { + // We are handling each incoming connection in a separate child task. It is important + // to use a discarding task group here which automatically discards finished child tasks. + // A normal task group retains all child tasks and their outputs in memory until they are + // consumed by iterating the group or by exiting the group. Since, we are never consuming + // the results of the group we need the group to automatically discard them; otherwise, this + // would result in a memory leak over time. + try await withThrowingDiscardingTaskGroup { taskGroup in + try await channel.executeThenClose { inbound in + for try await connectionChannel in inbound { + + taskGroup.addTask { + logger.trace("Handling a new connection") + await server.handleConnection(channel: connectionChannel, logger: logger) + logger.trace("Done handling the connection") + } + } + } } + return .serverReturned(.success(())) + } catch { + return .serverReturned(.failure(error)) + } + } + + // Now that the local HTTP server and LambdaHandler tasks are started, wait for the + // first of the two that will terminate. + // When the first task terminates, cancel the group and collect the result of the + // second task. + + // collect and return the result of the LambdaHandler + let serverOrHandlerResult1 = await group.next()! + group.cancelAll() + + switch serverOrHandlerResult1 { + case .closureResult(let result): + return result + + case .serverReturned(let result): + logger.error( + "Server shutdown before closure completed", + metadata: [ + "error": "\(result.maybeError != nil ? "\(result.maybeError!)" : "none")" + ] + ) + switch await group.next()! { + case .closureResult(let result): + return result + + case .serverReturned: + fatalError("Only one task is a server, and only one can return `serverReturned`") } } } + logger.info("Server shutting down") + return try result.get() } /// This method handles individual TCP connections private func handleConnection( - channel: NIOAsyncChannel + channel: NIOAsyncChannel, + logger: Logger ) async { var requestHead: HTTPRequestHead! @@ -186,12 +244,14 @@ private struct LambdaHttpServer { // process the request let response = try await self.processRequest( head: requestHead, - body: requestBody + body: requestBody, + logger: logger ) // send the responses try await self.sendResponse( response: response, - outbound: outbound + outbound: outbound, + logger: logger ) requestHead = nil @@ -214,15 +274,19 @@ private struct LambdaHttpServer { /// - body: the HTTP request body /// - Throws: /// - Returns: the response to send back to the client or the Lambda function - private func processRequest(head: HTTPRequestHead, body: ByteBuffer?) async throws -> LocalServerResponse { + private func processRequest( + head: HTTPRequestHead, + body: ByteBuffer?, + logger: Logger + ) async throws -> LocalServerResponse { if let body { - self.logger.trace( + logger.trace( "Processing request", metadata: ["URI": "\(head.method) \(head.uri)", "Body": "\(String(buffer: body))"] ) } else { - self.logger.trace("Processing request", metadata: ["URI": "\(head.method) \(head.uri)"]) + logger.trace("Processing request", metadata: ["URI": "\(head.method) \(head.uri)"]) } switch (head.method, head.uri) { @@ -237,7 +301,9 @@ private struct LambdaHttpServer { } // we always accept the /invoke request and push them to the pool let requestId = "\(DispatchTime.now().uptimeNanoseconds)" - logger.trace("/invoke received invocation", metadata: ["requestId": "\(requestId)"]) + var logger = logger + logger[metadataKey: "requestID"] = "\(requestId)" + logger.trace("/invoke received invocation") await self.invocationPool.push(LocalServerInvocation(requestId: requestId, request: body)) // wait for the lambda function to process the request @@ -273,9 +339,9 @@ private struct LambdaHttpServer { case (.GET, let url) where url.hasSuffix(Consts.getNextInvocationURLSuffix): // pop the tasks from the queue - self.logger.trace("/next waiting for /invoke") + logger.trace("/next waiting for /invoke") for try await invocation in self.invocationPool { - self.logger.trace("/next retrieved invocation", metadata: ["requestId": "\(invocation.requestId)"]) + logger.trace("/next retrieved invocation", metadata: ["requestId": "\(invocation.requestId)"]) // this call also stores the invocation requestId into the response return invocation.makeResponse(status: .accepted) } @@ -322,12 +388,13 @@ private struct LambdaHttpServer { private func sendResponse( response: LocalServerResponse, - outbound: NIOAsyncChannelOutboundWriter + outbound: NIOAsyncChannelOutboundWriter, + logger: Logger ) async throws { var headers = HTTPHeaders(response.headers ?? []) headers.add(name: "Content-Length", value: "\(response.body?.readableBytes ?? 0)") - self.logger.trace("Writing response", metadata: ["requestId": "\(response.requestId ?? "")"]) + logger.trace("Writing response", metadata: ["requestId": "\(response.requestId ?? "")"]) try await outbound.write( HTTPServerResponsePart.head( HTTPResponseHead( @@ -350,44 +417,59 @@ private struct LambdaHttpServer { private final class Pool: AsyncSequence, AsyncIteratorProtocol, Sendable where T: Sendable { typealias Element = T - private let _buffer = Mutex>(.init()) - private let _continuation = Mutex?>(nil) - - /// retrieve the first element from the buffer - public func popFirst() async -> T? { - self._buffer.withLock { $0.popFirst() } + enum State: ~Copyable { + case buffer(Deque) + case continuation(CheckedContinuation?) } + private let lock = Mutex(.buffer([])) + /// enqueue an element, or give it back immediately to the iterator if it is waiting for an element public func push(_ invocation: T) async { // if the iterator is waiting for an element, give it to it // otherwise, enqueue the element - if let continuation = self._continuation.withLock({ $0 }) { - self._continuation.withLock { $0 = nil } - continuation.resume(returning: invocation) - } else { - self._buffer.withLock { $0.append(invocation) } + let maybeContinuation = self.lock.withLock { state -> CheckedContinuation? in + switch consume state { + case .continuation(let continuation): + state = .buffer([]) + return continuation + + case .buffer(var buffer): + buffer.append(invocation) + state = .buffer(buffer) + return nil + } } + + maybeContinuation?.resume(returning: invocation) } func next() async throws -> T? { - // exit the async for loop if the task is cancelled guard !Task.isCancelled else { return nil } - if let element = await self.popFirst() { - return element - } else { - // we can't return nil if there is nothing to dequeue otherwise the async for loop will stop - // wait for an element to be enqueued - return try await withCheckedThrowingContinuation { (continuation: CheckedContinuation) in - // store the continuation for later, when an element is enqueued - self._continuation.withLock { - $0 = continuation + return try await withCheckedThrowingContinuation { (continuation: CheckedContinuation) in + let nextAction = self.lock.withLock { state -> T? in + switch consume state { + case .buffer(var buffer): + if let first = buffer.popFirst() { + state = .buffer(buffer) + return first + } else { + state = .continuation(continuation) + return nil + } + + case .continuation: + fatalError("Concurrent invocations to next(). This is illegal.") } } + + guard let nextAction else { return } + + continuation.resume(returning: nextAction) } } @@ -432,3 +514,14 @@ private struct LambdaHttpServer { } } #endif + +extension Result { + var maybeError: Failure? { + switch self { + case .success: + return nil + case .failure(let error): + return error + } + } +} From 61cd5d54da5490b1047c43839edd52cf27363468 Mon Sep 17 00:00:00 2001 From: Adam Fowler Date: Thu, 27 Feb 2025 16:03:41 +0000 Subject: [PATCH 17/24] [core] Add cancellation handling for nextInvocation() (#459) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Allow `LambdaChannelHandler.nextInvocation` to be cancelled. ### Motivation: If we want to use ServiceLifecycle with the lambda runtime the lambda runtime needs to be cancellable either via a ServiceLifecycle graceful shutdown or via Task cancellation. To avoid bringing in the ServiceLifecycle dependency this PR adds cancellation via Task cancellation handler. ### Modifications: Add `withTaskCancellationHandler` to nextInvocation which calls close on cancel. In `LambdaChannelHandler.channelInactive` resume continuation if state is `waitingForNextInvocation` Added `LambdaRuntimeClientTests.testCancellation` ### Result: You can now cancel the runtime while it is waiting for the next invocation. --------- Co-authored-by: Fabian Fett Co-authored-by: Sébastien Stormacq --- Sources/AWSLambdaRuntimeCore/Lambda.swift | 38 ++++++++------ .../LambdaRuntimeClient.swift | 40 +++++++++----- .../LambdaRuntimeClientTests.swift | 52 +++++++++++++++++++ 3 files changed, 100 insertions(+), 30 deletions(-) diff --git a/Sources/AWSLambdaRuntimeCore/Lambda.swift b/Sources/AWSLambdaRuntimeCore/Lambda.swift index 3ba90e9c..4634fca0 100644 --- a/Sources/AWSLambdaRuntimeCore/Lambda.swift +++ b/Sources/AWSLambdaRuntimeCore/Lambda.swift @@ -37,25 +37,31 @@ public enum Lambda { ) async throws where Handler: StreamingLambdaHandler { var handler = handler - while !Task.isCancelled { - let (invocation, writer) = try await runtimeClient.nextInvocation() + do { + while !Task.isCancelled { + let (invocation, writer) = try await runtimeClient.nextInvocation() - do { - try await handler.handle( - invocation.event, - responseWriter: writer, - context: LambdaContext( - requestID: invocation.metadata.requestID, - traceID: invocation.metadata.traceID, - invokedFunctionARN: invocation.metadata.invokedFunctionARN, - deadline: DispatchWallTime(millisSinceEpoch: invocation.metadata.deadlineInMillisSinceEpoch), - logger: logger + do { + try await handler.handle( + invocation.event, + responseWriter: writer, + context: LambdaContext( + requestID: invocation.metadata.requestID, + traceID: invocation.metadata.traceID, + invokedFunctionARN: invocation.metadata.invokedFunctionARN, + deadline: DispatchWallTime( + millisSinceEpoch: invocation.metadata.deadlineInMillisSinceEpoch + ), + logger: logger + ) ) - ) - } catch { - try await writer.reportError(error) - continue + } catch { + try await writer.reportError(error) + continue + } } + } catch is CancellationError { + // don't allow cancellation error to propagate further } } diff --git a/Sources/AWSLambdaRuntimeCore/LambdaRuntimeClient.swift b/Sources/AWSLambdaRuntimeCore/LambdaRuntimeClient.swift index 228dc471..809a6a0e 100644 --- a/Sources/AWSLambdaRuntimeCore/LambdaRuntimeClient.swift +++ b/Sources/AWSLambdaRuntimeCore/LambdaRuntimeClient.swift @@ -145,22 +145,28 @@ final actor LambdaRuntimeClient: LambdaRuntimeClientProtocol { } func nextInvocation() async throws -> (Invocation, Writer) { - switch self.lambdaState { - case .idle: - self.lambdaState = .waitingForNextInvocation - let handler = try await self.makeOrGetConnection() - let invocation = try await handler.nextInvocation() - guard case .waitingForNextInvocation = self.lambdaState else { + try await withTaskCancellationHandler { + switch self.lambdaState { + case .idle: + self.lambdaState = .waitingForNextInvocation + let handler = try await self.makeOrGetConnection() + let invocation = try await handler.nextInvocation() + guard case .waitingForNextInvocation = self.lambdaState else { + fatalError("Invalid state: \(self.lambdaState)") + } + self.lambdaState = .waitingForResponse(requestID: invocation.metadata.requestID) + return (invocation, Writer(runtimeClient: self)) + + case .waitingForNextInvocation, + .waitingForResponse, + .sendingResponse, + .sentResponse: fatalError("Invalid state: \(self.lambdaState)") } - self.lambdaState = .waitingForResponse(requestID: invocation.metadata.requestID) - return (invocation, Writer(runtimeClient: self)) - - case .waitingForNextInvocation, - .waitingForResponse, - .sendingResponse, - .sentResponse: - fatalError("Invalid state: \(self.lambdaState)") + } onCancel: { + Task { + await self.close() + } } } @@ -819,6 +825,12 @@ extension LambdaChannelHandler: ChannelInboundHandler { func channelInactive(context: ChannelHandlerContext) { // fail any pending responses with last error or assume peer disconnected + switch self.state { + case .connected(_, .waitingForNextInvocation(let continuation)): + continuation.resume(throwing: self.lastError ?? ChannelError.ioOnClosedChannel) + default: + break + } // we don't need to forward channelInactive to the delegate, as the delegate observes the // closeFuture diff --git a/Tests/AWSLambdaRuntimeCoreTests/LambdaRuntimeClientTests.swift b/Tests/AWSLambdaRuntimeCoreTests/LambdaRuntimeClientTests.swift index e779b931..5d430a0f 100644 --- a/Tests/AWSLambdaRuntimeCoreTests/LambdaRuntimeClientTests.swift +++ b/Tests/AWSLambdaRuntimeCoreTests/LambdaRuntimeClientTests.swift @@ -86,4 +86,56 @@ struct LambdaRuntimeClientTests { } } } + + @Test + func testCancellation() async throws { + struct HappyBehavior: LambdaServerBehavior { + let requestId = UUID().uuidString + let event = "hello" + + func getInvocation() -> GetInvocationResult { + .success((self.requestId, self.event)) + } + + func processResponse(requestId: String, response: String?) -> Result { + #expect(self.requestId == requestId) + #expect(self.event == response) + return .success(()) + } + + func processError(requestId: String, error: ErrorResponse) -> Result { + Issue.record("should not report error") + return .failure(.internalServerError) + } + + func processInitError(error: ErrorResponse) -> Result { + Issue.record("should not report init error") + return .failure(.internalServerError) + } + } + + try await withMockServer(behaviour: HappyBehavior()) { port in + try await LambdaRuntimeClient.withRuntimeClient( + configuration: .init(ip: "127.0.0.1", port: port), + eventLoop: NIOSingletons.posixEventLoopGroup.next(), + logger: self.logger + ) { runtimeClient in + try await withThrowingTaskGroup(of: Void.self) { group in + group.addTask { + while true { + let (_, writer) = try await runtimeClient.nextInvocation() + // Wrap this is a task so cancellation isn't propagated to the write calls + try await Task { + try await writer.write(ByteBuffer(string: "hello")) + try await writer.finish() + }.value + } + } + // wait a small amount to ensure we are waiting for continuation + try await Task.sleep(for: .milliseconds(100)) + group.cancelAll() + } + } + } + } } From 03876f6eed50cd1703998278091838306ef3466f Mon Sep 17 00:00:00 2001 From: Fabian Fett Date: Tue, 4 Mar 2025 08:20:06 +0100 Subject: [PATCH 18/24] [TestServer] Fix 488 and add simple test lambda (#489) Fixes: #488 --- .../Lambda+LocalServer.swift | 23 ++++++++++++++----- 1 file changed, 17 insertions(+), 6 deletions(-) diff --git a/Sources/AWSLambdaRuntimeCore/Lambda+LocalServer.swift b/Sources/AWSLambdaRuntimeCore/Lambda+LocalServer.swift index 4bb113a7..bee880cc 100644 --- a/Sources/AWSLambdaRuntimeCore/Lambda+LocalServer.swift +++ b/Sources/AWSLambdaRuntimeCore/Lambda+LocalServer.swift @@ -352,15 +352,15 @@ private struct LambdaHTTPServer { // :requestID/response endpoint is called by the lambda posting the response case (.POST, let url) where url.hasSuffix(Consts.postResponseURLSuffix): let parts = head.uri.split(separator: "/") - guard let requestId = parts.count > 2 ? String(parts[parts.count - 2]) : nil else { + guard let requestID = parts.count > 2 ? String(parts[parts.count - 2]) : nil else { // the request is malformed, since we were expecting a requestId in the path return .init(status: .badRequest) } // enqueue the lambda function response to be served as response to the client /invoke - logger.trace("/:requestID/response received response", metadata: ["requestId": "\(requestId)"]) + logger.trace("/:requestID/response received response", metadata: ["requestId": "\(requestID)"]) await self.responsePool.push( LocalServerResponse( - id: requestId, + id: requestID, status: .ok, headers: [("Content-Type", "application/json")], body: body @@ -368,17 +368,28 @@ private struct LambdaHTTPServer { ) // tell the Lambda function we accepted the response - return .init(id: requestId, status: .accepted) + return .init(id: requestID, status: .accepted) // :requestID/error endpoint is called by the lambda posting an error response // we accept all requestID and we do not handle the body, we just acknowledge the request case (.POST, let url) where url.hasSuffix(Consts.postErrorURLSuffix): let parts = head.uri.split(separator: "/") - guard let _ = parts.count > 2 ? String(parts[parts.count - 2]) : nil else { + guard let requestID = parts.count > 2 ? String(parts[parts.count - 2]) : nil else { // the request is malformed, since we were expecting a requestId in the path return .init(status: .badRequest) } - return .init(status: .ok) + // enqueue the lambda function response to be served as response to the client /invoke + logger.trace("/:requestID/response received response", metadata: ["requestId": "\(requestID)"]) + await self.responsePool.push( + LocalServerResponse( + id: requestID, + status: .internalServerError, + headers: [("Content-Type", "application/json")], + body: body + ) + ) + + return .init(status: .accepted) // unknown call default: From 0a75e0f9cc1387cdf69f8d796889ce20712cfa8b Mon Sep 17 00:00:00 2001 From: Fabian Fett Date: Thu, 6 Mar 2025 09:52:23 +0100 Subject: [PATCH 19/24] Use package traits when using Swift 6.1 (#490) --- .github/workflows/integration_tests.yml | 2 +- .github/workflows/pull_request.yml | 4 +- .spi.yml | 2 +- Examples/HelloJSON/Package.swift | 10 +- Package.swift | 35 +- Package@swift-6.0.swift | 95 +++++ .../ControlPlaneRequest.swift | 0 .../ControlPlaneRequestEncoder.swift | 0 .../Docs.docc}/Deployment.md | 0 .../Docs.docc}/Documentation.md | 2 +- .../Docs.docc}/Proposals/0001-v2-api.md | 2 +- .../Docs.docc}/Resources/code/.shellcheckrc | 0 .../Resources/code/03-01-01-package-init.sh | 0 .../Resources/code/03-01-02-package-init.sh | 0 .../Resources/code/03-01-03-package-init.sh | 0 .../Resources/code/03-01-04-package-init.sh | 0 .../Resources/code/03-02-01-package.swift | 0 .../Resources/code/03-02-02-package.swift | 0 .../Resources/code/03-02-03-package.swift | 0 .../Resources/code/03-02-04-package.swift | 0 .../Resources/code/03-02-05-package.swift | 0 .../Resources/code/03-03-01-main.swift | 0 .../Resources/code/03-03-02-main.swift | 0 .../Resources/code/03-03-03-main.swift | 0 .../Resources/code/03-03-04-main.swift | 0 .../Resources/code/03-03-05-main.swift | 0 .../Resources/code/03-03-06-main.swift | 0 .../Resources/code/03-03-07-main.swift | 0 .../Resources/code/03-04-02-console-output.sh | 2 +- .../Resources/code/03-04-03-curl.sh | 0 .../Resources/code/03-04-04-curl.sh | 0 .../Resources/code/03-04-06-terminal.sh | 0 .../Resources/code/03-04-07-terminal.sh | 2 +- .../Resources/code/04-01-02-plugin-archive.sh | 0 .../Resources/code/04-01-03-plugin-archive.sh | 0 .../Resources/code/04-01-04-plugin-archive.sh | 0 .../Resources/code/04-03-01-aws-cli.sh | 0 .../code/04-03-02-lambda-invoke-hidden.sh | 0 .../Resources/code/04-03-02-lambda-invoke.sh | 0 .../Resources/code/04-03-03-lambda-invoke.sh | 0 .../Resources/code/04-03-04-lambda-invoke.sh | 0 .../Resources/code/04-03-05-lambda-invoke.sh | 0 .../deployment/console-10-regions.png | Bin .../deployment/console-20-dashboard.png | Bin .../deployment/console-30-create-function.png | Bin .../deployment/console-40-select-zip-file.png | Bin .../deployment/console-50-upload-zip.png | Bin .../console-60-prepare-test-event.png | Bin .../console-70-view-invocation-response.png | Bin .../deployment/console-80-delete-function.png | Bin .../deployment/console-80-delete-role.png | Bin .../tutorials/00-swift_on_lambda.png | Bin .../tutorials/01-swift_on_lambda.png | Bin .../tutorials/03-01-terminal-package-init.png | Bin .../Resources/tutorials/03-01-xcode@2x.png | Bin .../tutorials/03-01-xcode~dark@2x.png | Bin .../tutorials/03-02-swift-package-manager.png | Bin .../tutorials/03-03-swift-code-xcode.png | Bin .../tutorials/03-04-01-compile-run@2x.png.png | Bin .../03-04-01-compile-run~dark@2x.png | Bin .../tutorials/03-04-test-locally.png | Bin .../tutorials/03-swift_on_lambda.png | Bin .../tutorials/04-01-01-docker-started@2x.png | Bin .../tutorials/04-01-compile-for-linux.png | Bin .../tutorials/04-02-01-console-login@2x.png | Bin .../tutorials/04-02-02-console-login@2x.png | Bin .../tutorials/04-02-03-select-region@2x.png | Bin .../tutorials/04-02-04-select-lambda@2x.png | Bin .../04-02-04-select-lambda~dark@2x.png | Bin .../tutorials/04-02-05-create-function@2x.png | Bin .../04-02-05-create-function~dark@2x.png | Bin .../tutorials/04-02-06-create-function@2x.png | Bin .../04-02-06-create-function~dark@2x.png | Bin .../tutorials/04-02-07-upload-zip@2x.png | Bin .../tutorials/04-02-07-upload-zip~dark@2x.png | Bin .../tutorials/04-02-08-upload-zip@2x.png | Bin .../tutorials/04-02-08-upload-zip~dark@2x.png | Bin .../tutorials/04-02-09-test-lambda@2x.png | Bin .../04-02-09-test-lambda~dark@2x.png | Bin .../04-02-10-test-lambda-result@2x.png | Bin .../04-02-10-test-lambda-result~dark@2x.png | Bin .../tutorials/04-02-create-lambda.png | Bin .../tutorials/04-03-invoke-lambda.png | Bin .../tutorials/04-swift_on_lambda.png | Bin Sources/AWSLambdaRuntime/Docs.docc/index.md | 344 ------------------ .../Docs.docc}/quick-setup.md | 0 .../Docs.docc}/tutorials/01-overview.tutorial | 0 .../tutorials/02-what-is-lambda.tutorial | 0 .../tutorials/03-prerequisites.tutorial | 0 .../tutorials/03-write-function.tutorial | 0 .../tutorials/04-deploy-function.tutorial | 0 .../tutorials/table-of-content.tutorial | 0 .../Context+Foundation.swift | 4 +- .../FoundationSupport/Lambda+JSON.swift | 138 +++++++ .../Vendored/ByteBuffer-foundation.swift | 2 + .../Vendored/JSON+ByteBuffer.swift | 2 + Sources/AWSLambdaRuntime/Lambda+Codable.swift | 253 +++++++------ .../Lambda+LocalServer.swift | 4 +- .../Lambda.swift | 0 .../LambdaContext.swift | 0 .../LambdaHandlers.swift | 0 .../LambdaRequestID.swift | 0 .../LambdaRuntime+ServiceLifecycle.swift | 19 + .../LambdaRuntime.swift | 2 +- .../LambdaRuntimeClient.swift | 0 .../LambdaRuntimeClientProtocol.swift | 0 .../LambdaRuntimeError.swift | 0 .../Utils.swift | 0 .../AWSLambdaRuntimeCore/Lambda+Codable.swift | 162 --------- .../ControlPlaneRequestEncoderTests.swift | 2 +- .../InvocationTests.swift | 2 +- .../LambdaMockClient.swift | 2 +- .../LambdaRequestIDTests.swift | 2 +- .../LambdaRunLoopTests.swift | 2 +- .../LambdaRuntimeClientTests.swift | 2 +- .../MockLambdaServer.swift | 2 +- .../AWSLambdaRuntimeCoreTests/UtilsTest.swift | 2 +- readme.md | 8 +- 118 files changed, 449 insertions(+), 661 deletions(-) create mode 100644 Package@swift-6.0.swift rename Sources/{AWSLambdaRuntimeCore => AWSLambdaRuntime}/ControlPlaneRequest.swift (100%) rename Sources/{AWSLambdaRuntimeCore => AWSLambdaRuntime}/ControlPlaneRequestEncoder.swift (100%) rename Sources/{AWSLambdaRuntimeCore/Documentation.docc => AWSLambdaRuntime/Docs.docc}/Deployment.md (100%) rename Sources/{AWSLambdaRuntimeCore/Documentation.docc => AWSLambdaRuntime/Docs.docc}/Documentation.md (98%) rename Sources/{AWSLambdaRuntimeCore/Documentation.docc => AWSLambdaRuntime/Docs.docc}/Proposals/0001-v2-api.md (99%) rename Sources/{AWSLambdaRuntimeCore/Documentation.docc => AWSLambdaRuntime/Docs.docc}/Resources/code/.shellcheckrc (100%) rename Sources/{AWSLambdaRuntimeCore/Documentation.docc => AWSLambdaRuntime/Docs.docc}/Resources/code/03-01-01-package-init.sh (100%) rename Sources/{AWSLambdaRuntimeCore/Documentation.docc => AWSLambdaRuntime/Docs.docc}/Resources/code/03-01-02-package-init.sh (100%) rename Sources/{AWSLambdaRuntimeCore/Documentation.docc => AWSLambdaRuntime/Docs.docc}/Resources/code/03-01-03-package-init.sh (100%) rename Sources/{AWSLambdaRuntimeCore/Documentation.docc => AWSLambdaRuntime/Docs.docc}/Resources/code/03-01-04-package-init.sh (100%) rename Sources/{AWSLambdaRuntimeCore/Documentation.docc => AWSLambdaRuntime/Docs.docc}/Resources/code/03-02-01-package.swift (100%) rename Sources/{AWSLambdaRuntimeCore/Documentation.docc => AWSLambdaRuntime/Docs.docc}/Resources/code/03-02-02-package.swift (100%) rename Sources/{AWSLambdaRuntimeCore/Documentation.docc => AWSLambdaRuntime/Docs.docc}/Resources/code/03-02-03-package.swift (100%) rename Sources/{AWSLambdaRuntimeCore/Documentation.docc => AWSLambdaRuntime/Docs.docc}/Resources/code/03-02-04-package.swift (100%) rename Sources/{AWSLambdaRuntimeCore/Documentation.docc => AWSLambdaRuntime/Docs.docc}/Resources/code/03-02-05-package.swift (100%) rename Sources/{AWSLambdaRuntimeCore/Documentation.docc => AWSLambdaRuntime/Docs.docc}/Resources/code/03-03-01-main.swift (100%) rename Sources/{AWSLambdaRuntimeCore/Documentation.docc => AWSLambdaRuntime/Docs.docc}/Resources/code/03-03-02-main.swift (100%) rename Sources/{AWSLambdaRuntimeCore/Documentation.docc => AWSLambdaRuntime/Docs.docc}/Resources/code/03-03-03-main.swift (100%) rename Sources/{AWSLambdaRuntimeCore/Documentation.docc => AWSLambdaRuntime/Docs.docc}/Resources/code/03-03-04-main.swift (100%) rename Sources/{AWSLambdaRuntimeCore/Documentation.docc => AWSLambdaRuntime/Docs.docc}/Resources/code/03-03-05-main.swift (100%) rename Sources/{AWSLambdaRuntimeCore/Documentation.docc => AWSLambdaRuntime/Docs.docc}/Resources/code/03-03-06-main.swift (100%) rename Sources/{AWSLambdaRuntimeCore/Documentation.docc => AWSLambdaRuntime/Docs.docc}/Resources/code/03-03-07-main.swift (100%) rename Sources/{AWSLambdaRuntimeCore/Documentation.docc => AWSLambdaRuntime/Docs.docc}/Resources/code/03-04-02-console-output.sh (93%) rename Sources/{AWSLambdaRuntimeCore/Documentation.docc => AWSLambdaRuntime/Docs.docc}/Resources/code/03-04-03-curl.sh (100%) rename Sources/{AWSLambdaRuntimeCore/Documentation.docc => AWSLambdaRuntime/Docs.docc}/Resources/code/03-04-04-curl.sh (100%) rename Sources/{AWSLambdaRuntimeCore/Documentation.docc => AWSLambdaRuntime/Docs.docc}/Resources/code/03-04-06-terminal.sh (100%) rename Sources/{AWSLambdaRuntimeCore/Documentation.docc => AWSLambdaRuntime/Docs.docc}/Resources/code/03-04-07-terminal.sh (67%) rename Sources/{AWSLambdaRuntimeCore/Documentation.docc => AWSLambdaRuntime/Docs.docc}/Resources/code/04-01-02-plugin-archive.sh (100%) rename Sources/{AWSLambdaRuntimeCore/Documentation.docc => AWSLambdaRuntime/Docs.docc}/Resources/code/04-01-03-plugin-archive.sh (100%) rename Sources/{AWSLambdaRuntimeCore/Documentation.docc => AWSLambdaRuntime/Docs.docc}/Resources/code/04-01-04-plugin-archive.sh (100%) rename Sources/{AWSLambdaRuntimeCore/Documentation.docc => AWSLambdaRuntime/Docs.docc}/Resources/code/04-03-01-aws-cli.sh (100%) rename Sources/{AWSLambdaRuntimeCore/Documentation.docc => AWSLambdaRuntime/Docs.docc}/Resources/code/04-03-02-lambda-invoke-hidden.sh (100%) rename Sources/{AWSLambdaRuntimeCore/Documentation.docc => AWSLambdaRuntime/Docs.docc}/Resources/code/04-03-02-lambda-invoke.sh (100%) rename Sources/{AWSLambdaRuntimeCore/Documentation.docc => AWSLambdaRuntime/Docs.docc}/Resources/code/04-03-03-lambda-invoke.sh (100%) rename Sources/{AWSLambdaRuntimeCore/Documentation.docc => AWSLambdaRuntime/Docs.docc}/Resources/code/04-03-04-lambda-invoke.sh (100%) rename Sources/{AWSLambdaRuntimeCore/Documentation.docc => AWSLambdaRuntime/Docs.docc}/Resources/code/04-03-05-lambda-invoke.sh (100%) rename Sources/{AWSLambdaRuntimeCore/Documentation.docc => AWSLambdaRuntime/Docs.docc}/Resources/deployment/console-10-regions.png (100%) rename Sources/{AWSLambdaRuntimeCore/Documentation.docc => AWSLambdaRuntime/Docs.docc}/Resources/deployment/console-20-dashboard.png (100%) rename Sources/{AWSLambdaRuntimeCore/Documentation.docc => AWSLambdaRuntime/Docs.docc}/Resources/deployment/console-30-create-function.png (100%) rename Sources/{AWSLambdaRuntimeCore/Documentation.docc => AWSLambdaRuntime/Docs.docc}/Resources/deployment/console-40-select-zip-file.png (100%) rename Sources/{AWSLambdaRuntimeCore/Documentation.docc => AWSLambdaRuntime/Docs.docc}/Resources/deployment/console-50-upload-zip.png (100%) rename Sources/{AWSLambdaRuntimeCore/Documentation.docc => AWSLambdaRuntime/Docs.docc}/Resources/deployment/console-60-prepare-test-event.png (100%) rename Sources/{AWSLambdaRuntimeCore/Documentation.docc => AWSLambdaRuntime/Docs.docc}/Resources/deployment/console-70-view-invocation-response.png (100%) rename Sources/{AWSLambdaRuntimeCore/Documentation.docc => AWSLambdaRuntime/Docs.docc}/Resources/deployment/console-80-delete-function.png (100%) rename Sources/{AWSLambdaRuntimeCore/Documentation.docc => AWSLambdaRuntime/Docs.docc}/Resources/deployment/console-80-delete-role.png (100%) rename Sources/{AWSLambdaRuntimeCore/Documentation.docc => AWSLambdaRuntime/Docs.docc}/Resources/tutorials/00-swift_on_lambda.png (100%) rename Sources/{AWSLambdaRuntimeCore/Documentation.docc => AWSLambdaRuntime/Docs.docc}/Resources/tutorials/01-swift_on_lambda.png (100%) rename Sources/{AWSLambdaRuntimeCore/Documentation.docc => AWSLambdaRuntime/Docs.docc}/Resources/tutorials/03-01-terminal-package-init.png (100%) rename Sources/{AWSLambdaRuntimeCore/Documentation.docc => AWSLambdaRuntime/Docs.docc}/Resources/tutorials/03-01-xcode@2x.png (100%) rename Sources/{AWSLambdaRuntimeCore/Documentation.docc => AWSLambdaRuntime/Docs.docc}/Resources/tutorials/03-01-xcode~dark@2x.png (100%) rename Sources/{AWSLambdaRuntimeCore/Documentation.docc => AWSLambdaRuntime/Docs.docc}/Resources/tutorials/03-02-swift-package-manager.png (100%) rename Sources/{AWSLambdaRuntimeCore/Documentation.docc => AWSLambdaRuntime/Docs.docc}/Resources/tutorials/03-03-swift-code-xcode.png (100%) rename Sources/{AWSLambdaRuntimeCore/Documentation.docc => AWSLambdaRuntime/Docs.docc}/Resources/tutorials/03-04-01-compile-run@2x.png.png (100%) rename Sources/{AWSLambdaRuntimeCore/Documentation.docc => AWSLambdaRuntime/Docs.docc}/Resources/tutorials/03-04-01-compile-run~dark@2x.png (100%) rename Sources/{AWSLambdaRuntimeCore/Documentation.docc => AWSLambdaRuntime/Docs.docc}/Resources/tutorials/03-04-test-locally.png (100%) rename Sources/{AWSLambdaRuntimeCore/Documentation.docc => AWSLambdaRuntime/Docs.docc}/Resources/tutorials/03-swift_on_lambda.png (100%) rename Sources/{AWSLambdaRuntimeCore/Documentation.docc => AWSLambdaRuntime/Docs.docc}/Resources/tutorials/04-01-01-docker-started@2x.png (100%) rename Sources/{AWSLambdaRuntimeCore/Documentation.docc => AWSLambdaRuntime/Docs.docc}/Resources/tutorials/04-01-compile-for-linux.png (100%) rename Sources/{AWSLambdaRuntimeCore/Documentation.docc => AWSLambdaRuntime/Docs.docc}/Resources/tutorials/04-02-01-console-login@2x.png (100%) rename Sources/{AWSLambdaRuntimeCore/Documentation.docc => AWSLambdaRuntime/Docs.docc}/Resources/tutorials/04-02-02-console-login@2x.png (100%) rename Sources/{AWSLambdaRuntimeCore/Documentation.docc => AWSLambdaRuntime/Docs.docc}/Resources/tutorials/04-02-03-select-region@2x.png (100%) rename Sources/{AWSLambdaRuntimeCore/Documentation.docc => AWSLambdaRuntime/Docs.docc}/Resources/tutorials/04-02-04-select-lambda@2x.png (100%) rename Sources/{AWSLambdaRuntimeCore/Documentation.docc => AWSLambdaRuntime/Docs.docc}/Resources/tutorials/04-02-04-select-lambda~dark@2x.png (100%) rename Sources/{AWSLambdaRuntimeCore/Documentation.docc => AWSLambdaRuntime/Docs.docc}/Resources/tutorials/04-02-05-create-function@2x.png (100%) rename Sources/{AWSLambdaRuntimeCore/Documentation.docc => AWSLambdaRuntime/Docs.docc}/Resources/tutorials/04-02-05-create-function~dark@2x.png (100%) rename Sources/{AWSLambdaRuntimeCore/Documentation.docc => AWSLambdaRuntime/Docs.docc}/Resources/tutorials/04-02-06-create-function@2x.png (100%) rename Sources/{AWSLambdaRuntimeCore/Documentation.docc => AWSLambdaRuntime/Docs.docc}/Resources/tutorials/04-02-06-create-function~dark@2x.png (100%) rename Sources/{AWSLambdaRuntimeCore/Documentation.docc => AWSLambdaRuntime/Docs.docc}/Resources/tutorials/04-02-07-upload-zip@2x.png (100%) rename Sources/{AWSLambdaRuntimeCore/Documentation.docc => AWSLambdaRuntime/Docs.docc}/Resources/tutorials/04-02-07-upload-zip~dark@2x.png (100%) rename Sources/{AWSLambdaRuntimeCore/Documentation.docc => AWSLambdaRuntime/Docs.docc}/Resources/tutorials/04-02-08-upload-zip@2x.png (100%) rename Sources/{AWSLambdaRuntimeCore/Documentation.docc => AWSLambdaRuntime/Docs.docc}/Resources/tutorials/04-02-08-upload-zip~dark@2x.png (100%) rename Sources/{AWSLambdaRuntimeCore/Documentation.docc => AWSLambdaRuntime/Docs.docc}/Resources/tutorials/04-02-09-test-lambda@2x.png (100%) rename Sources/{AWSLambdaRuntimeCore/Documentation.docc => AWSLambdaRuntime/Docs.docc}/Resources/tutorials/04-02-09-test-lambda~dark@2x.png (100%) rename Sources/{AWSLambdaRuntimeCore/Documentation.docc => AWSLambdaRuntime/Docs.docc}/Resources/tutorials/04-02-10-test-lambda-result@2x.png (100%) rename Sources/{AWSLambdaRuntimeCore/Documentation.docc => AWSLambdaRuntime/Docs.docc}/Resources/tutorials/04-02-10-test-lambda-result~dark@2x.png (100%) rename Sources/{AWSLambdaRuntimeCore/Documentation.docc => AWSLambdaRuntime/Docs.docc}/Resources/tutorials/04-02-create-lambda.png (100%) rename Sources/{AWSLambdaRuntimeCore/Documentation.docc => AWSLambdaRuntime/Docs.docc}/Resources/tutorials/04-03-invoke-lambda.png (100%) rename Sources/{AWSLambdaRuntimeCore/Documentation.docc => AWSLambdaRuntime/Docs.docc}/Resources/tutorials/04-swift_on_lambda.png (100%) delete mode 100644 Sources/AWSLambdaRuntime/Docs.docc/index.md rename Sources/{AWSLambdaRuntimeCore/Documentation.docc => AWSLambdaRuntime/Docs.docc}/quick-setup.md (100%) rename Sources/{AWSLambdaRuntimeCore/Documentation.docc => AWSLambdaRuntime/Docs.docc}/tutorials/01-overview.tutorial (100%) rename Sources/{AWSLambdaRuntimeCore/Documentation.docc => AWSLambdaRuntime/Docs.docc}/tutorials/02-what-is-lambda.tutorial (100%) rename Sources/{AWSLambdaRuntimeCore/Documentation.docc => AWSLambdaRuntime/Docs.docc}/tutorials/03-prerequisites.tutorial (100%) rename Sources/{AWSLambdaRuntimeCore/Documentation.docc => AWSLambdaRuntime/Docs.docc}/tutorials/03-write-function.tutorial (100%) rename Sources/{AWSLambdaRuntimeCore/Documentation.docc => AWSLambdaRuntime/Docs.docc}/tutorials/04-deploy-function.tutorial (100%) rename Sources/{AWSLambdaRuntimeCore/Documentation.docc => AWSLambdaRuntime/Docs.docc}/tutorials/table-of-content.tutorial (100%) rename Sources/AWSLambdaRuntime/{ => FoundationSupport}/Context+Foundation.swift (92%) create mode 100644 Sources/AWSLambdaRuntime/FoundationSupport/Lambda+JSON.swift rename Sources/AWSLambdaRuntime/{ => FoundationSupport}/Vendored/ByteBuffer-foundation.swift (98%) rename Sources/AWSLambdaRuntime/{ => FoundationSupport}/Vendored/JSON+ByteBuffer.swift (98%) rename Sources/{AWSLambdaRuntimeCore => AWSLambdaRuntime}/Lambda+LocalServer.swift (99%) rename Sources/{AWSLambdaRuntimeCore => AWSLambdaRuntime}/Lambda.swift (100%) rename Sources/{AWSLambdaRuntimeCore => AWSLambdaRuntime}/LambdaContext.swift (100%) rename Sources/{AWSLambdaRuntimeCore => AWSLambdaRuntime}/LambdaHandlers.swift (100%) rename Sources/{AWSLambdaRuntimeCore => AWSLambdaRuntime}/LambdaRequestID.swift (100%) create mode 100644 Sources/AWSLambdaRuntime/LambdaRuntime+ServiceLifecycle.swift rename Sources/{AWSLambdaRuntimeCore => AWSLambdaRuntime}/LambdaRuntime.swift (99%) rename Sources/{AWSLambdaRuntimeCore => AWSLambdaRuntime}/LambdaRuntimeClient.swift (100%) rename Sources/{AWSLambdaRuntimeCore => AWSLambdaRuntime}/LambdaRuntimeClientProtocol.swift (100%) rename Sources/{AWSLambdaRuntimeCore => AWSLambdaRuntime}/LambdaRuntimeError.swift (100%) rename Sources/{AWSLambdaRuntimeCore => AWSLambdaRuntime}/Utils.swift (100%) delete mode 100644 Sources/AWSLambdaRuntimeCore/Lambda+Codable.swift diff --git a/.github/workflows/integration_tests.yml b/.github/workflows/integration_tests.yml index 1a67f637..af02f0d4 100644 --- a/.github/workflows/integration_tests.yml +++ b/.github/workflows/integration_tests.yml @@ -39,7 +39,7 @@ on: matrix_linux_swift_container_image: type: string description: "Container image for the matrix job. Defaults to matching latest Swift Ubuntu image." - default: "swift:amazonlinux2" + default: "swiftlang/swift:nightly-6.1-jammy" ## We are cancelling previously triggered workflow runs concurrency: diff --git a/.github/workflows/pull_request.yml b/.github/workflows/pull_request.yml index 9c1e88a3..520c539d 100644 --- a/.github/workflows/pull_request.yml +++ b/.github/workflows/pull_request.yml @@ -12,9 +12,9 @@ jobs: license_header_check_project_name: "SwiftAWSLambdaRuntime" shell_check_enabled: true python_lint_check_enabled: true - api_breakage_check_container_image: "swift:6.0-noble" + api_breakage_check_container_image: "swiftlang/swift:nightly-6.1-jammy" docs_check_container_image: "swift:6.0-noble" - format_check_container_image: "swiftlang/swift:nightly-6.0-jammy" + format_check_container_image: "swiftlang/swift:nightly-6.1-jammy" yamllint_check_enabled: true unit-tests: diff --git a/.spi.yml b/.spi.yml index 16792833..9c13e3e4 100644 --- a/.spi.yml +++ b/.spi.yml @@ -1,4 +1,4 @@ version: 1 builder: configs: - - documentation_targets: [AWSLambdaRuntimeCore, AWSLambdaRuntime] + - documentation_targets: [AWSLambdaRuntime] diff --git a/Examples/HelloJSON/Package.swift b/Examples/HelloJSON/Package.swift index 4ec236fd..9f26ff9d 100644 --- a/Examples/HelloJSON/Package.swift +++ b/Examples/HelloJSON/Package.swift @@ -1,4 +1,4 @@ -// swift-tools-version:6.0 +// swift-tools-version:6.1 import PackageDescription @@ -13,7 +13,13 @@ let package = Package( ], 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") + .package( + url: "https://github.com/swift-server/swift-aws-lambda-runtime.git", + branch: "ff-package-traits", + traits: [ + .trait(name: "FoundationJSONSupport") + ] + ) ], targets: [ .executableTarget( diff --git a/Package.swift b/Package.swift index 9eac9771..3bd026f9 100644 --- a/Package.swift +++ b/Package.swift @@ -1,4 +1,4 @@ -// swift-tools-version:6.0 +// swift-tools-version:6.1 import PackageDescription @@ -6,37 +6,45 @@ let package = Package( name: "swift-aws-lambda-runtime", platforms: [.macOS(.v15)], products: [ - // this library exports `AWSLambdaRuntimeCore` and adds Foundation convenience methods .library(name: "AWSLambdaRuntime", targets: ["AWSLambdaRuntime"]), - // this has all the main functionality for lambda and it does not link Foundation - .library(name: "AWSLambdaRuntimeCore", targets: ["AWSLambdaRuntimeCore"]), // plugin to package the lambda, creating an archive that can be uploaded to AWS // requires Linux or at least macOS v15 .plugin(name: "AWSLambdaPackager", targets: ["AWSLambdaPackager"]), // for testing only .library(name: "AWSLambdaTesting", targets: ["AWSLambdaTesting"]), ], + traits: [ + "FoundationJSONSupport", + "ServiceLifecycleSupport", + "LocalServerSupport", + .default( + enabledTraits: [ + "FoundationJSONSupport", + "ServiceLifecycleSupport", + "LocalServerSupport", + ] + ), + ], dependencies: [ .package(url: "https://github.com/apple/swift-nio.git", from: "2.81.0"), .package(url: "https://github.com/apple/swift-log.git", from: "1.5.4"), .package(url: "https://github.com/apple/swift-collections.git", from: "1.1.4"), + .package(url: "https://github.com/swift-server/swift-service-lifecycle.git", from: "2.6.3"), ], targets: [ .target( name: "AWSLambdaRuntime", dependencies: [ - .byName(name: "AWSLambdaRuntimeCore"), .product(name: "NIOCore", package: "swift-nio"), - ] - ), - .target( - name: "AWSLambdaRuntimeCore", - dependencies: [ .product(name: "DequeModule", package: "swift-collections"), .product(name: "Logging", package: "swift-log"), .product(name: "NIOHTTP1", package: "swift-nio"), - .product(name: "NIOCore", package: "swift-nio"), .product(name: "NIOPosix", package: "swift-nio"), + .product( + name: "ServiceLifecycle", + package: "swift-service-lifecycle", + condition: .when(traits: ["ServiceLifecycleSupport"]) + ), ] ), .plugin( @@ -58,7 +66,7 @@ let package = Package( .testTarget( name: "AWSLambdaRuntimeCoreTests", dependencies: [ - .byName(name: "AWSLambdaRuntimeCore"), + .byName(name: "AWSLambdaRuntime"), .product(name: "NIOTestUtils", package: "swift-nio"), .product(name: "NIOFoundationCompat", package: "swift-nio"), ] @@ -66,8 +74,7 @@ let package = Package( .testTarget( name: "AWSLambdaRuntimeTests", dependencies: [ - .byName(name: "AWSLambdaRuntimeCore"), - .byName(name: "AWSLambdaRuntime"), + .byName(name: "AWSLambdaRuntime") ] ), // testing helper diff --git a/Package@swift-6.0.swift b/Package@swift-6.0.swift new file mode 100644 index 00000000..ff45ec88 --- /dev/null +++ b/Package@swift-6.0.swift @@ -0,0 +1,95 @@ +// swift-tools-version:6.0 + +import PackageDescription + +let package = Package( + name: "swift-aws-lambda-runtime", + platforms: [.macOS(.v15)], + products: [ + .library(name: "AWSLambdaRuntime", targets: ["AWSLambdaRuntime"]), + // plugin to package the lambda, creating an archive that can be uploaded to AWS + // requires Linux or at least macOS v15 + .plugin(name: "AWSLambdaPackager", targets: ["AWSLambdaPackager"]), + // for testing only + .library(name: "AWSLambdaTesting", targets: ["AWSLambdaTesting"]), + ], + dependencies: [ + .package(url: "https://github.com/apple/swift-nio.git", from: "2.81.0"), + .package(url: "https://github.com/apple/swift-log.git", from: "1.5.4"), + .package(url: "https://github.com/apple/swift-collections.git", from: "1.1.4"), + .package(url: "https://github.com/swift-server/swift-service-lifecycle.git", from: "2.6.3"), + ], + targets: [ + .target( + name: "AWSLambdaRuntime", + dependencies: [ + .product(name: "NIOCore", package: "swift-nio"), + .product(name: "DequeModule", package: "swift-collections"), + .product(name: "Logging", package: "swift-log"), + .product(name: "NIOHTTP1", package: "swift-nio"), + .product(name: "NIOPosix", package: "swift-nio"), + .product(name: "ServiceLifecycle", package: "swift-service-lifecycle"), + ], + swiftSettings: [ + .define("FoundationJSONSupport"), + .define("ServiceLifecycleSupport"), + .define("LocalServerSupport"), + ] + ), + .plugin( + name: "AWSLambdaPackager", + capability: .command( + intent: .custom( + verb: "archive", + description: + "Archive the Lambda binary and prepare it for uploading to AWS. Requires docker on macOS or non Amazonlinux 2 distributions." + ), + permissions: [ + .allowNetworkConnections( + scope: .docker, + reason: "This plugin uses Docker to create the AWS Lambda ZIP package." + ) + ] + ) + ), + .testTarget( + name: "AWSLambdaRuntimeCoreTests", + dependencies: [ + .byName(name: "AWSLambdaRuntime"), + .product(name: "NIOTestUtils", package: "swift-nio"), + .product(name: "NIOFoundationCompat", package: "swift-nio"), + ] + ), + .testTarget( + name: "AWSLambdaRuntimeTests", + dependencies: [ + .byName(name: "AWSLambdaRuntime") + ] + ), + // testing helper + .target( + name: "AWSLambdaTesting", + dependencies: [ + .byName(name: "AWSLambdaRuntime"), + .product(name: "NIOCore", package: "swift-nio"), + .product(name: "NIOPosix", package: "swift-nio"), + ] + ), + .testTarget( + name: "AWSLambdaTestingTests", + dependencies: [ + .byName(name: "AWSLambdaTesting") + ] + ), + // for perf testing + .executableTarget( + name: "MockServer", + dependencies: [ + .product(name: "Logging", package: "swift-log"), + .product(name: "NIOHTTP1", package: "swift-nio"), + .product(name: "NIOCore", package: "swift-nio"), + .product(name: "NIOPosix", package: "swift-nio"), + ] + ), + ] +) diff --git a/Sources/AWSLambdaRuntimeCore/ControlPlaneRequest.swift b/Sources/AWSLambdaRuntime/ControlPlaneRequest.swift similarity index 100% rename from Sources/AWSLambdaRuntimeCore/ControlPlaneRequest.swift rename to Sources/AWSLambdaRuntime/ControlPlaneRequest.swift diff --git a/Sources/AWSLambdaRuntimeCore/ControlPlaneRequestEncoder.swift b/Sources/AWSLambdaRuntime/ControlPlaneRequestEncoder.swift similarity index 100% rename from Sources/AWSLambdaRuntimeCore/ControlPlaneRequestEncoder.swift rename to Sources/AWSLambdaRuntime/ControlPlaneRequestEncoder.swift diff --git a/Sources/AWSLambdaRuntimeCore/Documentation.docc/Deployment.md b/Sources/AWSLambdaRuntime/Docs.docc/Deployment.md similarity index 100% rename from Sources/AWSLambdaRuntimeCore/Documentation.docc/Deployment.md rename to Sources/AWSLambdaRuntime/Docs.docc/Deployment.md diff --git a/Sources/AWSLambdaRuntimeCore/Documentation.docc/Documentation.md b/Sources/AWSLambdaRuntime/Docs.docc/Documentation.md similarity index 98% rename from Sources/AWSLambdaRuntimeCore/Documentation.docc/Documentation.md rename to Sources/AWSLambdaRuntime/Docs.docc/Documentation.md index 4016bcbd..e820ce26 100644 --- a/Sources/AWSLambdaRuntimeCore/Documentation.docc/Documentation.md +++ b/Sources/AWSLambdaRuntime/Docs.docc/Documentation.md @@ -1,4 +1,4 @@ -# ``AWSLambdaRuntimeCore`` +# ``AWSLambdaRuntime`` An AWS Lambda runtime for the Swift programming language diff --git a/Sources/AWSLambdaRuntimeCore/Documentation.docc/Proposals/0001-v2-api.md b/Sources/AWSLambdaRuntime/Docs.docc/Proposals/0001-v2-api.md similarity index 99% rename from Sources/AWSLambdaRuntimeCore/Documentation.docc/Proposals/0001-v2-api.md rename to Sources/AWSLambdaRuntime/Docs.docc/Proposals/0001-v2-api.md index e4ff259b..624182d0 100644 --- a/Sources/AWSLambdaRuntimeCore/Documentation.docc/Proposals/0001-v2-api.md +++ b/Sources/AWSLambdaRuntime/Docs.docc/Proposals/0001-v2-api.md @@ -40,7 +40,7 @@ Server ecosystem is to shift to newer, more readable, Swift concurrency construc #### No ownership of the main() function A Lambda function can currently be implemented through conformance to the various handler protocols defined in -`AWSLambdaRuntimeCore/LambdaHandler`. Each of these protocols have an extension which implements a `static func main()`. +``AWSLambdaRuntime/LambdaHandler``. Each of these protocols have an extension which implements a `static func main()`. This allows users to annotate their `LambdaHandler` conforming object with `@main`. The `static func main()` calls the internal `Lambda.run()` function, which starts the Lambda function. Since the `Lambda.run()` method is internal, users cannot override the default implementation. This has proven challenging for users who want to diff --git a/Sources/AWSLambdaRuntimeCore/Documentation.docc/Resources/code/.shellcheckrc b/Sources/AWSLambdaRuntime/Docs.docc/Resources/code/.shellcheckrc similarity index 100% rename from Sources/AWSLambdaRuntimeCore/Documentation.docc/Resources/code/.shellcheckrc rename to Sources/AWSLambdaRuntime/Docs.docc/Resources/code/.shellcheckrc diff --git a/Sources/AWSLambdaRuntimeCore/Documentation.docc/Resources/code/03-01-01-package-init.sh b/Sources/AWSLambdaRuntime/Docs.docc/Resources/code/03-01-01-package-init.sh similarity index 100% rename from Sources/AWSLambdaRuntimeCore/Documentation.docc/Resources/code/03-01-01-package-init.sh rename to Sources/AWSLambdaRuntime/Docs.docc/Resources/code/03-01-01-package-init.sh diff --git a/Sources/AWSLambdaRuntimeCore/Documentation.docc/Resources/code/03-01-02-package-init.sh b/Sources/AWSLambdaRuntime/Docs.docc/Resources/code/03-01-02-package-init.sh similarity index 100% rename from Sources/AWSLambdaRuntimeCore/Documentation.docc/Resources/code/03-01-02-package-init.sh rename to Sources/AWSLambdaRuntime/Docs.docc/Resources/code/03-01-02-package-init.sh diff --git a/Sources/AWSLambdaRuntimeCore/Documentation.docc/Resources/code/03-01-03-package-init.sh b/Sources/AWSLambdaRuntime/Docs.docc/Resources/code/03-01-03-package-init.sh similarity index 100% rename from Sources/AWSLambdaRuntimeCore/Documentation.docc/Resources/code/03-01-03-package-init.sh rename to Sources/AWSLambdaRuntime/Docs.docc/Resources/code/03-01-03-package-init.sh diff --git a/Sources/AWSLambdaRuntimeCore/Documentation.docc/Resources/code/03-01-04-package-init.sh b/Sources/AWSLambdaRuntime/Docs.docc/Resources/code/03-01-04-package-init.sh similarity index 100% rename from Sources/AWSLambdaRuntimeCore/Documentation.docc/Resources/code/03-01-04-package-init.sh rename to Sources/AWSLambdaRuntime/Docs.docc/Resources/code/03-01-04-package-init.sh diff --git a/Sources/AWSLambdaRuntimeCore/Documentation.docc/Resources/code/03-02-01-package.swift b/Sources/AWSLambdaRuntime/Docs.docc/Resources/code/03-02-01-package.swift similarity index 100% rename from Sources/AWSLambdaRuntimeCore/Documentation.docc/Resources/code/03-02-01-package.swift rename to Sources/AWSLambdaRuntime/Docs.docc/Resources/code/03-02-01-package.swift diff --git a/Sources/AWSLambdaRuntimeCore/Documentation.docc/Resources/code/03-02-02-package.swift b/Sources/AWSLambdaRuntime/Docs.docc/Resources/code/03-02-02-package.swift similarity index 100% rename from Sources/AWSLambdaRuntimeCore/Documentation.docc/Resources/code/03-02-02-package.swift rename to Sources/AWSLambdaRuntime/Docs.docc/Resources/code/03-02-02-package.swift diff --git a/Sources/AWSLambdaRuntimeCore/Documentation.docc/Resources/code/03-02-03-package.swift b/Sources/AWSLambdaRuntime/Docs.docc/Resources/code/03-02-03-package.swift similarity index 100% rename from Sources/AWSLambdaRuntimeCore/Documentation.docc/Resources/code/03-02-03-package.swift rename to Sources/AWSLambdaRuntime/Docs.docc/Resources/code/03-02-03-package.swift diff --git a/Sources/AWSLambdaRuntimeCore/Documentation.docc/Resources/code/03-02-04-package.swift b/Sources/AWSLambdaRuntime/Docs.docc/Resources/code/03-02-04-package.swift similarity index 100% rename from Sources/AWSLambdaRuntimeCore/Documentation.docc/Resources/code/03-02-04-package.swift rename to Sources/AWSLambdaRuntime/Docs.docc/Resources/code/03-02-04-package.swift diff --git a/Sources/AWSLambdaRuntimeCore/Documentation.docc/Resources/code/03-02-05-package.swift b/Sources/AWSLambdaRuntime/Docs.docc/Resources/code/03-02-05-package.swift similarity index 100% rename from Sources/AWSLambdaRuntimeCore/Documentation.docc/Resources/code/03-02-05-package.swift rename to Sources/AWSLambdaRuntime/Docs.docc/Resources/code/03-02-05-package.swift diff --git a/Sources/AWSLambdaRuntimeCore/Documentation.docc/Resources/code/03-03-01-main.swift b/Sources/AWSLambdaRuntime/Docs.docc/Resources/code/03-03-01-main.swift similarity index 100% rename from Sources/AWSLambdaRuntimeCore/Documentation.docc/Resources/code/03-03-01-main.swift rename to Sources/AWSLambdaRuntime/Docs.docc/Resources/code/03-03-01-main.swift diff --git a/Sources/AWSLambdaRuntimeCore/Documentation.docc/Resources/code/03-03-02-main.swift b/Sources/AWSLambdaRuntime/Docs.docc/Resources/code/03-03-02-main.swift similarity index 100% rename from Sources/AWSLambdaRuntimeCore/Documentation.docc/Resources/code/03-03-02-main.swift rename to Sources/AWSLambdaRuntime/Docs.docc/Resources/code/03-03-02-main.swift diff --git a/Sources/AWSLambdaRuntimeCore/Documentation.docc/Resources/code/03-03-03-main.swift b/Sources/AWSLambdaRuntime/Docs.docc/Resources/code/03-03-03-main.swift similarity index 100% rename from Sources/AWSLambdaRuntimeCore/Documentation.docc/Resources/code/03-03-03-main.swift rename to Sources/AWSLambdaRuntime/Docs.docc/Resources/code/03-03-03-main.swift diff --git a/Sources/AWSLambdaRuntimeCore/Documentation.docc/Resources/code/03-03-04-main.swift b/Sources/AWSLambdaRuntime/Docs.docc/Resources/code/03-03-04-main.swift similarity index 100% rename from Sources/AWSLambdaRuntimeCore/Documentation.docc/Resources/code/03-03-04-main.swift rename to Sources/AWSLambdaRuntime/Docs.docc/Resources/code/03-03-04-main.swift diff --git a/Sources/AWSLambdaRuntimeCore/Documentation.docc/Resources/code/03-03-05-main.swift b/Sources/AWSLambdaRuntime/Docs.docc/Resources/code/03-03-05-main.swift similarity index 100% rename from Sources/AWSLambdaRuntimeCore/Documentation.docc/Resources/code/03-03-05-main.swift rename to Sources/AWSLambdaRuntime/Docs.docc/Resources/code/03-03-05-main.swift diff --git a/Sources/AWSLambdaRuntimeCore/Documentation.docc/Resources/code/03-03-06-main.swift b/Sources/AWSLambdaRuntime/Docs.docc/Resources/code/03-03-06-main.swift similarity index 100% rename from Sources/AWSLambdaRuntimeCore/Documentation.docc/Resources/code/03-03-06-main.swift rename to Sources/AWSLambdaRuntime/Docs.docc/Resources/code/03-03-06-main.swift diff --git a/Sources/AWSLambdaRuntimeCore/Documentation.docc/Resources/code/03-03-07-main.swift b/Sources/AWSLambdaRuntime/Docs.docc/Resources/code/03-03-07-main.swift similarity index 100% rename from Sources/AWSLambdaRuntimeCore/Documentation.docc/Resources/code/03-03-07-main.swift rename to Sources/AWSLambdaRuntime/Docs.docc/Resources/code/03-03-07-main.swift diff --git a/Sources/AWSLambdaRuntimeCore/Documentation.docc/Resources/code/03-04-02-console-output.sh b/Sources/AWSLambdaRuntime/Docs.docc/Resources/code/03-04-02-console-output.sh similarity index 93% rename from Sources/AWSLambdaRuntimeCore/Documentation.docc/Resources/code/03-04-02-console-output.sh rename to Sources/AWSLambdaRuntime/Docs.docc/Resources/code/03-04-02-console-output.sh index 3d862f5d..11c59fb9 100644 --- a/Sources/AWSLambdaRuntimeCore/Documentation.docc/Resources/code/03-04-02-console-output.sh +++ b/Sources/AWSLambdaRuntime/Docs.docc/Resources/code/03-04-02-console-output.sh @@ -1,2 +1,2 @@ -2025-01-02T14:59:29+0100 info LocalLambdaServer : [AWSLambdaRuntimeCore] +2025-01-02T14:59:29+0100 info LocalLambdaServer : [AWSLambdaRuntime] LocalLambdaServer started and listening on 127.0.0.1:7000, receiving events on /invoke diff --git a/Sources/AWSLambdaRuntimeCore/Documentation.docc/Resources/code/03-04-03-curl.sh b/Sources/AWSLambdaRuntime/Docs.docc/Resources/code/03-04-03-curl.sh similarity index 100% rename from Sources/AWSLambdaRuntimeCore/Documentation.docc/Resources/code/03-04-03-curl.sh rename to Sources/AWSLambdaRuntime/Docs.docc/Resources/code/03-04-03-curl.sh diff --git a/Sources/AWSLambdaRuntimeCore/Documentation.docc/Resources/code/03-04-04-curl.sh b/Sources/AWSLambdaRuntime/Docs.docc/Resources/code/03-04-04-curl.sh similarity index 100% rename from Sources/AWSLambdaRuntimeCore/Documentation.docc/Resources/code/03-04-04-curl.sh rename to Sources/AWSLambdaRuntime/Docs.docc/Resources/code/03-04-04-curl.sh diff --git a/Sources/AWSLambdaRuntimeCore/Documentation.docc/Resources/code/03-04-06-terminal.sh b/Sources/AWSLambdaRuntime/Docs.docc/Resources/code/03-04-06-terminal.sh similarity index 100% rename from Sources/AWSLambdaRuntimeCore/Documentation.docc/Resources/code/03-04-06-terminal.sh rename to Sources/AWSLambdaRuntime/Docs.docc/Resources/code/03-04-06-terminal.sh diff --git a/Sources/AWSLambdaRuntimeCore/Documentation.docc/Resources/code/03-04-07-terminal.sh b/Sources/AWSLambdaRuntime/Docs.docc/Resources/code/03-04-07-terminal.sh similarity index 67% rename from Sources/AWSLambdaRuntimeCore/Documentation.docc/Resources/code/03-04-07-terminal.sh rename to Sources/AWSLambdaRuntime/Docs.docc/Resources/code/03-04-07-terminal.sh index 6e4d6a3d..1348bddc 100644 --- a/Sources/AWSLambdaRuntimeCore/Documentation.docc/Resources/code/03-04-07-terminal.sh +++ b/Sources/AWSLambdaRuntime/Docs.docc/Resources/code/03-04-07-terminal.sh @@ -3,4 +3,4 @@ swift run Building for debugging... [1/1] Write swift-version--58304C5D6DBC2206.txt Build of product 'PalindromeLambda' complete! (0.11s) -2025-01-02T15:12:49+0100 info LocalLambdaServer : [AWSLambdaRuntimeCore] LocalLambdaServer started and listening on 127.0.0.1:7000, receiving events on /invoke +2025-01-02T15:12:49+0100 info LocalLambdaServer : [AWSLambdaRuntime] LocalLambdaServer started and listening on 127.0.0.1:7000, receiving events on /invoke diff --git a/Sources/AWSLambdaRuntimeCore/Documentation.docc/Resources/code/04-01-02-plugin-archive.sh b/Sources/AWSLambdaRuntime/Docs.docc/Resources/code/04-01-02-plugin-archive.sh similarity index 100% rename from Sources/AWSLambdaRuntimeCore/Documentation.docc/Resources/code/04-01-02-plugin-archive.sh rename to Sources/AWSLambdaRuntime/Docs.docc/Resources/code/04-01-02-plugin-archive.sh diff --git a/Sources/AWSLambdaRuntimeCore/Documentation.docc/Resources/code/04-01-03-plugin-archive.sh b/Sources/AWSLambdaRuntime/Docs.docc/Resources/code/04-01-03-plugin-archive.sh similarity index 100% rename from Sources/AWSLambdaRuntimeCore/Documentation.docc/Resources/code/04-01-03-plugin-archive.sh rename to Sources/AWSLambdaRuntime/Docs.docc/Resources/code/04-01-03-plugin-archive.sh diff --git a/Sources/AWSLambdaRuntimeCore/Documentation.docc/Resources/code/04-01-04-plugin-archive.sh b/Sources/AWSLambdaRuntime/Docs.docc/Resources/code/04-01-04-plugin-archive.sh similarity index 100% rename from Sources/AWSLambdaRuntimeCore/Documentation.docc/Resources/code/04-01-04-plugin-archive.sh rename to Sources/AWSLambdaRuntime/Docs.docc/Resources/code/04-01-04-plugin-archive.sh diff --git a/Sources/AWSLambdaRuntimeCore/Documentation.docc/Resources/code/04-03-01-aws-cli.sh b/Sources/AWSLambdaRuntime/Docs.docc/Resources/code/04-03-01-aws-cli.sh similarity index 100% rename from Sources/AWSLambdaRuntimeCore/Documentation.docc/Resources/code/04-03-01-aws-cli.sh rename to Sources/AWSLambdaRuntime/Docs.docc/Resources/code/04-03-01-aws-cli.sh diff --git a/Sources/AWSLambdaRuntimeCore/Documentation.docc/Resources/code/04-03-02-lambda-invoke-hidden.sh b/Sources/AWSLambdaRuntime/Docs.docc/Resources/code/04-03-02-lambda-invoke-hidden.sh similarity index 100% rename from Sources/AWSLambdaRuntimeCore/Documentation.docc/Resources/code/04-03-02-lambda-invoke-hidden.sh rename to Sources/AWSLambdaRuntime/Docs.docc/Resources/code/04-03-02-lambda-invoke-hidden.sh diff --git a/Sources/AWSLambdaRuntimeCore/Documentation.docc/Resources/code/04-03-02-lambda-invoke.sh b/Sources/AWSLambdaRuntime/Docs.docc/Resources/code/04-03-02-lambda-invoke.sh similarity index 100% rename from Sources/AWSLambdaRuntimeCore/Documentation.docc/Resources/code/04-03-02-lambda-invoke.sh rename to Sources/AWSLambdaRuntime/Docs.docc/Resources/code/04-03-02-lambda-invoke.sh diff --git a/Sources/AWSLambdaRuntimeCore/Documentation.docc/Resources/code/04-03-03-lambda-invoke.sh b/Sources/AWSLambdaRuntime/Docs.docc/Resources/code/04-03-03-lambda-invoke.sh similarity index 100% rename from Sources/AWSLambdaRuntimeCore/Documentation.docc/Resources/code/04-03-03-lambda-invoke.sh rename to Sources/AWSLambdaRuntime/Docs.docc/Resources/code/04-03-03-lambda-invoke.sh diff --git a/Sources/AWSLambdaRuntimeCore/Documentation.docc/Resources/code/04-03-04-lambda-invoke.sh b/Sources/AWSLambdaRuntime/Docs.docc/Resources/code/04-03-04-lambda-invoke.sh similarity index 100% rename from Sources/AWSLambdaRuntimeCore/Documentation.docc/Resources/code/04-03-04-lambda-invoke.sh rename to Sources/AWSLambdaRuntime/Docs.docc/Resources/code/04-03-04-lambda-invoke.sh diff --git a/Sources/AWSLambdaRuntimeCore/Documentation.docc/Resources/code/04-03-05-lambda-invoke.sh b/Sources/AWSLambdaRuntime/Docs.docc/Resources/code/04-03-05-lambda-invoke.sh similarity index 100% rename from Sources/AWSLambdaRuntimeCore/Documentation.docc/Resources/code/04-03-05-lambda-invoke.sh rename to Sources/AWSLambdaRuntime/Docs.docc/Resources/code/04-03-05-lambda-invoke.sh diff --git a/Sources/AWSLambdaRuntimeCore/Documentation.docc/Resources/deployment/console-10-regions.png b/Sources/AWSLambdaRuntime/Docs.docc/Resources/deployment/console-10-regions.png similarity index 100% rename from Sources/AWSLambdaRuntimeCore/Documentation.docc/Resources/deployment/console-10-regions.png rename to Sources/AWSLambdaRuntime/Docs.docc/Resources/deployment/console-10-regions.png diff --git a/Sources/AWSLambdaRuntimeCore/Documentation.docc/Resources/deployment/console-20-dashboard.png b/Sources/AWSLambdaRuntime/Docs.docc/Resources/deployment/console-20-dashboard.png similarity index 100% rename from Sources/AWSLambdaRuntimeCore/Documentation.docc/Resources/deployment/console-20-dashboard.png rename to Sources/AWSLambdaRuntime/Docs.docc/Resources/deployment/console-20-dashboard.png diff --git a/Sources/AWSLambdaRuntimeCore/Documentation.docc/Resources/deployment/console-30-create-function.png b/Sources/AWSLambdaRuntime/Docs.docc/Resources/deployment/console-30-create-function.png similarity index 100% rename from Sources/AWSLambdaRuntimeCore/Documentation.docc/Resources/deployment/console-30-create-function.png rename to Sources/AWSLambdaRuntime/Docs.docc/Resources/deployment/console-30-create-function.png diff --git a/Sources/AWSLambdaRuntimeCore/Documentation.docc/Resources/deployment/console-40-select-zip-file.png b/Sources/AWSLambdaRuntime/Docs.docc/Resources/deployment/console-40-select-zip-file.png similarity index 100% rename from Sources/AWSLambdaRuntimeCore/Documentation.docc/Resources/deployment/console-40-select-zip-file.png rename to Sources/AWSLambdaRuntime/Docs.docc/Resources/deployment/console-40-select-zip-file.png diff --git a/Sources/AWSLambdaRuntimeCore/Documentation.docc/Resources/deployment/console-50-upload-zip.png b/Sources/AWSLambdaRuntime/Docs.docc/Resources/deployment/console-50-upload-zip.png similarity index 100% rename from Sources/AWSLambdaRuntimeCore/Documentation.docc/Resources/deployment/console-50-upload-zip.png rename to Sources/AWSLambdaRuntime/Docs.docc/Resources/deployment/console-50-upload-zip.png diff --git a/Sources/AWSLambdaRuntimeCore/Documentation.docc/Resources/deployment/console-60-prepare-test-event.png b/Sources/AWSLambdaRuntime/Docs.docc/Resources/deployment/console-60-prepare-test-event.png similarity index 100% rename from Sources/AWSLambdaRuntimeCore/Documentation.docc/Resources/deployment/console-60-prepare-test-event.png rename to Sources/AWSLambdaRuntime/Docs.docc/Resources/deployment/console-60-prepare-test-event.png diff --git a/Sources/AWSLambdaRuntimeCore/Documentation.docc/Resources/deployment/console-70-view-invocation-response.png b/Sources/AWSLambdaRuntime/Docs.docc/Resources/deployment/console-70-view-invocation-response.png similarity index 100% rename from Sources/AWSLambdaRuntimeCore/Documentation.docc/Resources/deployment/console-70-view-invocation-response.png rename to Sources/AWSLambdaRuntime/Docs.docc/Resources/deployment/console-70-view-invocation-response.png diff --git a/Sources/AWSLambdaRuntimeCore/Documentation.docc/Resources/deployment/console-80-delete-function.png b/Sources/AWSLambdaRuntime/Docs.docc/Resources/deployment/console-80-delete-function.png similarity index 100% rename from Sources/AWSLambdaRuntimeCore/Documentation.docc/Resources/deployment/console-80-delete-function.png rename to Sources/AWSLambdaRuntime/Docs.docc/Resources/deployment/console-80-delete-function.png diff --git a/Sources/AWSLambdaRuntimeCore/Documentation.docc/Resources/deployment/console-80-delete-role.png b/Sources/AWSLambdaRuntime/Docs.docc/Resources/deployment/console-80-delete-role.png similarity index 100% rename from Sources/AWSLambdaRuntimeCore/Documentation.docc/Resources/deployment/console-80-delete-role.png rename to Sources/AWSLambdaRuntime/Docs.docc/Resources/deployment/console-80-delete-role.png diff --git a/Sources/AWSLambdaRuntimeCore/Documentation.docc/Resources/tutorials/00-swift_on_lambda.png b/Sources/AWSLambdaRuntime/Docs.docc/Resources/tutorials/00-swift_on_lambda.png similarity index 100% rename from Sources/AWSLambdaRuntimeCore/Documentation.docc/Resources/tutorials/00-swift_on_lambda.png rename to Sources/AWSLambdaRuntime/Docs.docc/Resources/tutorials/00-swift_on_lambda.png diff --git a/Sources/AWSLambdaRuntimeCore/Documentation.docc/Resources/tutorials/01-swift_on_lambda.png b/Sources/AWSLambdaRuntime/Docs.docc/Resources/tutorials/01-swift_on_lambda.png similarity index 100% rename from Sources/AWSLambdaRuntimeCore/Documentation.docc/Resources/tutorials/01-swift_on_lambda.png rename to Sources/AWSLambdaRuntime/Docs.docc/Resources/tutorials/01-swift_on_lambda.png diff --git a/Sources/AWSLambdaRuntimeCore/Documentation.docc/Resources/tutorials/03-01-terminal-package-init.png b/Sources/AWSLambdaRuntime/Docs.docc/Resources/tutorials/03-01-terminal-package-init.png similarity index 100% rename from Sources/AWSLambdaRuntimeCore/Documentation.docc/Resources/tutorials/03-01-terminal-package-init.png rename to Sources/AWSLambdaRuntime/Docs.docc/Resources/tutorials/03-01-terminal-package-init.png diff --git a/Sources/AWSLambdaRuntimeCore/Documentation.docc/Resources/tutorials/03-01-xcode@2x.png b/Sources/AWSLambdaRuntime/Docs.docc/Resources/tutorials/03-01-xcode@2x.png similarity index 100% rename from Sources/AWSLambdaRuntimeCore/Documentation.docc/Resources/tutorials/03-01-xcode@2x.png rename to Sources/AWSLambdaRuntime/Docs.docc/Resources/tutorials/03-01-xcode@2x.png diff --git a/Sources/AWSLambdaRuntimeCore/Documentation.docc/Resources/tutorials/03-01-xcode~dark@2x.png b/Sources/AWSLambdaRuntime/Docs.docc/Resources/tutorials/03-01-xcode~dark@2x.png similarity index 100% rename from Sources/AWSLambdaRuntimeCore/Documentation.docc/Resources/tutorials/03-01-xcode~dark@2x.png rename to Sources/AWSLambdaRuntime/Docs.docc/Resources/tutorials/03-01-xcode~dark@2x.png diff --git a/Sources/AWSLambdaRuntimeCore/Documentation.docc/Resources/tutorials/03-02-swift-package-manager.png b/Sources/AWSLambdaRuntime/Docs.docc/Resources/tutorials/03-02-swift-package-manager.png similarity index 100% rename from Sources/AWSLambdaRuntimeCore/Documentation.docc/Resources/tutorials/03-02-swift-package-manager.png rename to Sources/AWSLambdaRuntime/Docs.docc/Resources/tutorials/03-02-swift-package-manager.png diff --git a/Sources/AWSLambdaRuntimeCore/Documentation.docc/Resources/tutorials/03-03-swift-code-xcode.png b/Sources/AWSLambdaRuntime/Docs.docc/Resources/tutorials/03-03-swift-code-xcode.png similarity index 100% rename from Sources/AWSLambdaRuntimeCore/Documentation.docc/Resources/tutorials/03-03-swift-code-xcode.png rename to Sources/AWSLambdaRuntime/Docs.docc/Resources/tutorials/03-03-swift-code-xcode.png diff --git a/Sources/AWSLambdaRuntimeCore/Documentation.docc/Resources/tutorials/03-04-01-compile-run@2x.png.png b/Sources/AWSLambdaRuntime/Docs.docc/Resources/tutorials/03-04-01-compile-run@2x.png.png similarity index 100% rename from Sources/AWSLambdaRuntimeCore/Documentation.docc/Resources/tutorials/03-04-01-compile-run@2x.png.png rename to Sources/AWSLambdaRuntime/Docs.docc/Resources/tutorials/03-04-01-compile-run@2x.png.png diff --git a/Sources/AWSLambdaRuntimeCore/Documentation.docc/Resources/tutorials/03-04-01-compile-run~dark@2x.png b/Sources/AWSLambdaRuntime/Docs.docc/Resources/tutorials/03-04-01-compile-run~dark@2x.png similarity index 100% rename from Sources/AWSLambdaRuntimeCore/Documentation.docc/Resources/tutorials/03-04-01-compile-run~dark@2x.png rename to Sources/AWSLambdaRuntime/Docs.docc/Resources/tutorials/03-04-01-compile-run~dark@2x.png diff --git a/Sources/AWSLambdaRuntimeCore/Documentation.docc/Resources/tutorials/03-04-test-locally.png b/Sources/AWSLambdaRuntime/Docs.docc/Resources/tutorials/03-04-test-locally.png similarity index 100% rename from Sources/AWSLambdaRuntimeCore/Documentation.docc/Resources/tutorials/03-04-test-locally.png rename to Sources/AWSLambdaRuntime/Docs.docc/Resources/tutorials/03-04-test-locally.png diff --git a/Sources/AWSLambdaRuntimeCore/Documentation.docc/Resources/tutorials/03-swift_on_lambda.png b/Sources/AWSLambdaRuntime/Docs.docc/Resources/tutorials/03-swift_on_lambda.png similarity index 100% rename from Sources/AWSLambdaRuntimeCore/Documentation.docc/Resources/tutorials/03-swift_on_lambda.png rename to Sources/AWSLambdaRuntime/Docs.docc/Resources/tutorials/03-swift_on_lambda.png diff --git a/Sources/AWSLambdaRuntimeCore/Documentation.docc/Resources/tutorials/04-01-01-docker-started@2x.png b/Sources/AWSLambdaRuntime/Docs.docc/Resources/tutorials/04-01-01-docker-started@2x.png similarity index 100% rename from Sources/AWSLambdaRuntimeCore/Documentation.docc/Resources/tutorials/04-01-01-docker-started@2x.png rename to Sources/AWSLambdaRuntime/Docs.docc/Resources/tutorials/04-01-01-docker-started@2x.png diff --git a/Sources/AWSLambdaRuntimeCore/Documentation.docc/Resources/tutorials/04-01-compile-for-linux.png b/Sources/AWSLambdaRuntime/Docs.docc/Resources/tutorials/04-01-compile-for-linux.png similarity index 100% rename from Sources/AWSLambdaRuntimeCore/Documentation.docc/Resources/tutorials/04-01-compile-for-linux.png rename to Sources/AWSLambdaRuntime/Docs.docc/Resources/tutorials/04-01-compile-for-linux.png diff --git a/Sources/AWSLambdaRuntimeCore/Documentation.docc/Resources/tutorials/04-02-01-console-login@2x.png b/Sources/AWSLambdaRuntime/Docs.docc/Resources/tutorials/04-02-01-console-login@2x.png similarity index 100% rename from Sources/AWSLambdaRuntimeCore/Documentation.docc/Resources/tutorials/04-02-01-console-login@2x.png rename to Sources/AWSLambdaRuntime/Docs.docc/Resources/tutorials/04-02-01-console-login@2x.png diff --git a/Sources/AWSLambdaRuntimeCore/Documentation.docc/Resources/tutorials/04-02-02-console-login@2x.png b/Sources/AWSLambdaRuntime/Docs.docc/Resources/tutorials/04-02-02-console-login@2x.png similarity index 100% rename from Sources/AWSLambdaRuntimeCore/Documentation.docc/Resources/tutorials/04-02-02-console-login@2x.png rename to Sources/AWSLambdaRuntime/Docs.docc/Resources/tutorials/04-02-02-console-login@2x.png diff --git a/Sources/AWSLambdaRuntimeCore/Documentation.docc/Resources/tutorials/04-02-03-select-region@2x.png b/Sources/AWSLambdaRuntime/Docs.docc/Resources/tutorials/04-02-03-select-region@2x.png similarity index 100% rename from Sources/AWSLambdaRuntimeCore/Documentation.docc/Resources/tutorials/04-02-03-select-region@2x.png rename to Sources/AWSLambdaRuntime/Docs.docc/Resources/tutorials/04-02-03-select-region@2x.png diff --git a/Sources/AWSLambdaRuntimeCore/Documentation.docc/Resources/tutorials/04-02-04-select-lambda@2x.png b/Sources/AWSLambdaRuntime/Docs.docc/Resources/tutorials/04-02-04-select-lambda@2x.png similarity index 100% rename from Sources/AWSLambdaRuntimeCore/Documentation.docc/Resources/tutorials/04-02-04-select-lambda@2x.png rename to Sources/AWSLambdaRuntime/Docs.docc/Resources/tutorials/04-02-04-select-lambda@2x.png diff --git a/Sources/AWSLambdaRuntimeCore/Documentation.docc/Resources/tutorials/04-02-04-select-lambda~dark@2x.png b/Sources/AWSLambdaRuntime/Docs.docc/Resources/tutorials/04-02-04-select-lambda~dark@2x.png similarity index 100% rename from Sources/AWSLambdaRuntimeCore/Documentation.docc/Resources/tutorials/04-02-04-select-lambda~dark@2x.png rename to Sources/AWSLambdaRuntime/Docs.docc/Resources/tutorials/04-02-04-select-lambda~dark@2x.png diff --git a/Sources/AWSLambdaRuntimeCore/Documentation.docc/Resources/tutorials/04-02-05-create-function@2x.png b/Sources/AWSLambdaRuntime/Docs.docc/Resources/tutorials/04-02-05-create-function@2x.png similarity index 100% rename from Sources/AWSLambdaRuntimeCore/Documentation.docc/Resources/tutorials/04-02-05-create-function@2x.png rename to Sources/AWSLambdaRuntime/Docs.docc/Resources/tutorials/04-02-05-create-function@2x.png diff --git a/Sources/AWSLambdaRuntimeCore/Documentation.docc/Resources/tutorials/04-02-05-create-function~dark@2x.png b/Sources/AWSLambdaRuntime/Docs.docc/Resources/tutorials/04-02-05-create-function~dark@2x.png similarity index 100% rename from Sources/AWSLambdaRuntimeCore/Documentation.docc/Resources/tutorials/04-02-05-create-function~dark@2x.png rename to Sources/AWSLambdaRuntime/Docs.docc/Resources/tutorials/04-02-05-create-function~dark@2x.png diff --git a/Sources/AWSLambdaRuntimeCore/Documentation.docc/Resources/tutorials/04-02-06-create-function@2x.png b/Sources/AWSLambdaRuntime/Docs.docc/Resources/tutorials/04-02-06-create-function@2x.png similarity index 100% rename from Sources/AWSLambdaRuntimeCore/Documentation.docc/Resources/tutorials/04-02-06-create-function@2x.png rename to Sources/AWSLambdaRuntime/Docs.docc/Resources/tutorials/04-02-06-create-function@2x.png diff --git a/Sources/AWSLambdaRuntimeCore/Documentation.docc/Resources/tutorials/04-02-06-create-function~dark@2x.png b/Sources/AWSLambdaRuntime/Docs.docc/Resources/tutorials/04-02-06-create-function~dark@2x.png similarity index 100% rename from Sources/AWSLambdaRuntimeCore/Documentation.docc/Resources/tutorials/04-02-06-create-function~dark@2x.png rename to Sources/AWSLambdaRuntime/Docs.docc/Resources/tutorials/04-02-06-create-function~dark@2x.png diff --git a/Sources/AWSLambdaRuntimeCore/Documentation.docc/Resources/tutorials/04-02-07-upload-zip@2x.png b/Sources/AWSLambdaRuntime/Docs.docc/Resources/tutorials/04-02-07-upload-zip@2x.png similarity index 100% rename from Sources/AWSLambdaRuntimeCore/Documentation.docc/Resources/tutorials/04-02-07-upload-zip@2x.png rename to Sources/AWSLambdaRuntime/Docs.docc/Resources/tutorials/04-02-07-upload-zip@2x.png diff --git a/Sources/AWSLambdaRuntimeCore/Documentation.docc/Resources/tutorials/04-02-07-upload-zip~dark@2x.png b/Sources/AWSLambdaRuntime/Docs.docc/Resources/tutorials/04-02-07-upload-zip~dark@2x.png similarity index 100% rename from Sources/AWSLambdaRuntimeCore/Documentation.docc/Resources/tutorials/04-02-07-upload-zip~dark@2x.png rename to Sources/AWSLambdaRuntime/Docs.docc/Resources/tutorials/04-02-07-upload-zip~dark@2x.png diff --git a/Sources/AWSLambdaRuntimeCore/Documentation.docc/Resources/tutorials/04-02-08-upload-zip@2x.png b/Sources/AWSLambdaRuntime/Docs.docc/Resources/tutorials/04-02-08-upload-zip@2x.png similarity index 100% rename from Sources/AWSLambdaRuntimeCore/Documentation.docc/Resources/tutorials/04-02-08-upload-zip@2x.png rename to Sources/AWSLambdaRuntime/Docs.docc/Resources/tutorials/04-02-08-upload-zip@2x.png diff --git a/Sources/AWSLambdaRuntimeCore/Documentation.docc/Resources/tutorials/04-02-08-upload-zip~dark@2x.png b/Sources/AWSLambdaRuntime/Docs.docc/Resources/tutorials/04-02-08-upload-zip~dark@2x.png similarity index 100% rename from Sources/AWSLambdaRuntimeCore/Documentation.docc/Resources/tutorials/04-02-08-upload-zip~dark@2x.png rename to Sources/AWSLambdaRuntime/Docs.docc/Resources/tutorials/04-02-08-upload-zip~dark@2x.png diff --git a/Sources/AWSLambdaRuntimeCore/Documentation.docc/Resources/tutorials/04-02-09-test-lambda@2x.png b/Sources/AWSLambdaRuntime/Docs.docc/Resources/tutorials/04-02-09-test-lambda@2x.png similarity index 100% rename from Sources/AWSLambdaRuntimeCore/Documentation.docc/Resources/tutorials/04-02-09-test-lambda@2x.png rename to Sources/AWSLambdaRuntime/Docs.docc/Resources/tutorials/04-02-09-test-lambda@2x.png diff --git a/Sources/AWSLambdaRuntimeCore/Documentation.docc/Resources/tutorials/04-02-09-test-lambda~dark@2x.png b/Sources/AWSLambdaRuntime/Docs.docc/Resources/tutorials/04-02-09-test-lambda~dark@2x.png similarity index 100% rename from Sources/AWSLambdaRuntimeCore/Documentation.docc/Resources/tutorials/04-02-09-test-lambda~dark@2x.png rename to Sources/AWSLambdaRuntime/Docs.docc/Resources/tutorials/04-02-09-test-lambda~dark@2x.png diff --git a/Sources/AWSLambdaRuntimeCore/Documentation.docc/Resources/tutorials/04-02-10-test-lambda-result@2x.png b/Sources/AWSLambdaRuntime/Docs.docc/Resources/tutorials/04-02-10-test-lambda-result@2x.png similarity index 100% rename from Sources/AWSLambdaRuntimeCore/Documentation.docc/Resources/tutorials/04-02-10-test-lambda-result@2x.png rename to Sources/AWSLambdaRuntime/Docs.docc/Resources/tutorials/04-02-10-test-lambda-result@2x.png diff --git a/Sources/AWSLambdaRuntimeCore/Documentation.docc/Resources/tutorials/04-02-10-test-lambda-result~dark@2x.png b/Sources/AWSLambdaRuntime/Docs.docc/Resources/tutorials/04-02-10-test-lambda-result~dark@2x.png similarity index 100% rename from Sources/AWSLambdaRuntimeCore/Documentation.docc/Resources/tutorials/04-02-10-test-lambda-result~dark@2x.png rename to Sources/AWSLambdaRuntime/Docs.docc/Resources/tutorials/04-02-10-test-lambda-result~dark@2x.png diff --git a/Sources/AWSLambdaRuntimeCore/Documentation.docc/Resources/tutorials/04-02-create-lambda.png b/Sources/AWSLambdaRuntime/Docs.docc/Resources/tutorials/04-02-create-lambda.png similarity index 100% rename from Sources/AWSLambdaRuntimeCore/Documentation.docc/Resources/tutorials/04-02-create-lambda.png rename to Sources/AWSLambdaRuntime/Docs.docc/Resources/tutorials/04-02-create-lambda.png diff --git a/Sources/AWSLambdaRuntimeCore/Documentation.docc/Resources/tutorials/04-03-invoke-lambda.png b/Sources/AWSLambdaRuntime/Docs.docc/Resources/tutorials/04-03-invoke-lambda.png similarity index 100% rename from Sources/AWSLambdaRuntimeCore/Documentation.docc/Resources/tutorials/04-03-invoke-lambda.png rename to Sources/AWSLambdaRuntime/Docs.docc/Resources/tutorials/04-03-invoke-lambda.png diff --git a/Sources/AWSLambdaRuntimeCore/Documentation.docc/Resources/tutorials/04-swift_on_lambda.png b/Sources/AWSLambdaRuntime/Docs.docc/Resources/tutorials/04-swift_on_lambda.png similarity index 100% rename from Sources/AWSLambdaRuntimeCore/Documentation.docc/Resources/tutorials/04-swift_on_lambda.png rename to Sources/AWSLambdaRuntime/Docs.docc/Resources/tutorials/04-swift_on_lambda.png diff --git a/Sources/AWSLambdaRuntime/Docs.docc/index.md b/Sources/AWSLambdaRuntime/Docs.docc/index.md deleted file mode 100644 index 4d7e1607..00000000 --- a/Sources/AWSLambdaRuntime/Docs.docc/index.md +++ /dev/null @@ -1,344 +0,0 @@ -# ``AWSLambdaRuntime`` - -An implementation of the AWS Lambda Runtime API in Swift. - -## Overview - -Many modern systems have client components like iOS, macOS or watchOS applications as well as server components that those clients interact with. Serverless functions are often the easiest and most efficient way for client application developers to extend their applications into the cloud. - -Serverless functions are increasingly becoming a popular choice for running event-driven or otherwise ad-hoc compute tasks in the cloud. They power mission critical microservices and data intensive workloads. In many cases, serverless functions allow developers to more easily scale and control compute costs given their on-demand nature. - -When using serverless functions, attention must be given to resource utilization as it directly impacts the costs of the system. This is where Swift shines! With its low memory footprint, deterministic performance, and quick start time, Swift is a fantastic match for the serverless functions architecture. - -Combine this with Swift's developer friendliness, expressiveness, and emphasis on safety, and we have a solution that is great for developers at all skill levels, scalable, and cost effective. - -Swift AWS Lambda Runtime was designed to make building Lambda functions in Swift simple and safe. The library is an implementation of the [AWS Lambda Runtime API](https://docs.aws.amazon.com/lambda/latest/dg/runtimes-custom.html) and uses an embedded asynchronous HTTP Client based on [SwiftNIO](http://github.com/apple/swift-nio) that is fine-tuned for performance in the AWS Runtime context. The library provides a multi-tier API that allows building a range of Lambda functions: From quick and simple closures to complex, performance-sensitive event handlers. - -## Getting started - -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 - - ```swift - // swift-tools-version:5.6 - - import PackageDescription - - let package = Package( - name: "my-lambda", - products: [ - .executable(name: "MyLambda", targets: ["MyLambda"]), - ], - dependencies: [ - .package(url: "https://github.com/swift-server/swift-aws-lambda-runtime.git", from: "0.1.0"), - ], - targets: [ - .executableTarget(name: "MyLambda", dependencies: [ - .product(name: "AWSLambdaRuntime", package: "swift-aws-lambda-runtime"), - ]), - ] - ) - ``` - -Next, create a `main.swift` and implement your Lambda. - - ### Using Closures - - The simplest way to use `AWSLambdaRuntime` is to pass in a closure, for example: - - ```swift - // Import the module - import AWSLambdaRuntime - - // in this example we are receiving and responding with strings - Lambda.run { (context, name: String, callback: @escaping (Result) -> Void) in - callback(.success("Hello, \(name)")) - } - ``` - - More commonly, the event would be a JSON, which is modeled using `Codable`, for example: - - ```swift - // Import the module - import AWSLambdaRuntime - - // Request, uses Codable for transparent JSON encoding - private struct Request: Codable { - let name: String - } - - // Response, uses Codable for transparent JSON encoding - private struct Response: Codable { - let message: String - } - - // In this example we are receiving and responding with `Codable`. - Lambda.run { (context, request: Request, callback: @escaping (Result) -> Void) in - callback(.success(Response(message: "Hello, \(request.name)"))) - } - ``` - - Since most Lambda functions are triggered by events originating in the AWS platform like `SNS`, `SQS` or `APIGateway`, the [Swift AWS Lambda Events](http://github.com/swift-server/swift-aws-lambda-events) package includes an `AWSLambdaEvents` module that provides implementations for most common AWS event types further simplifying writing Lambda functions. For example, handling an `SQS` message: - -First, add a dependency on the event packages: - - ```swift - // swift-tools-version:5.6 - - import PackageDescription - - let package = Package( - name: "my-lambda", - products: [ - .executable(name: "MyLambda", targets: ["MyLambda"]), - ], - dependencies: [ - .package(url: "https://github.com/swift-server/swift-aws-lambda-runtime.git", from: "0.1.0"), - ], - targets: [ - .executableTarget(name: "MyLambda", dependencies: [ - .product(name: "AWSLambdaRuntime", package: "swift-aws-lambda-runtime"), - .product(name: "AWSLambdaEvents", package: "swift-aws-lambda-runtime"), - ]), - ] - ) - ``` - - - ```swift - // Import the modules - import AWSLambdaRuntime - import AWSLambdaEvents - - // In this example we are receiving an SQS Event, with no response (Void). - Lambda.run { (context, message: SQS.Event, callback: @escaping (Result) -> Void) in - ... - callback(.success(Void())) - } - ``` - - 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. - - -## Deploying to AWS Lambda - -To deploy Lambda functions to AWS Lambda, you need to compile the code for Amazon Linux which is the OS used on AWS Lambda microVMs, package it as a Zip file, and upload to AWS. - -AWS offers several tools to interact and deploy Lambda functions to AWS Lambda including [SAM](https://aws.amazon.com/serverless/sam/) and the [AWS CLI](https://aws.amazon.com/cli/). - -To build the Lambda function for Amazon Linux, use the Docker image published by Swift.org on [Swift toolchains and Docker images for Amazon Linux 2](https://swift.org/download/). - -## Architecture - -The library defines three protocols for the implementation of a Lambda Handler. From low-level to more convenient: - -### ByteBufferLambdaHandler - -An `EventLoopFuture` based processing protocol for a Lambda that takes a `ByteBuffer` and returns a `ByteBuffer?` asynchronously. - -[`ByteBufferLambdaHandler`][bblh] is the lowest level protocol designed to power the higher level [`EventLoopLambdaHandler`][ellh] and [`LambdaHandler`][lh] based APIs. Users are not expected to use this protocol, though some performance sensitive applications that operate at the `ByteBuffer` level or have special serialization needs may choose to do so. - -```swift -public protocol ByteBufferLambdaHandler { - /// The Lambda handling method - /// Concrete Lambda handlers implement this method to provide the Lambda functionality. - /// - /// - parameters: - /// - context: Runtime `Context`. - /// - event: The event or request payload encoded as `ByteBuffer`. - /// - /// - Returns: An `EventLoopFuture` to report the result of the Lambda back to the runtime engine. - /// The `EventLoopFuture` should be completed with either a response encoded as `ByteBuffer` or an `Error` - func handle(context: Lambda.Context, event: ByteBuffer) -> EventLoopFuture -} -``` - -### EventLoopLambdaHandler - -[`EventLoopLambdaHandler`][ellh] is a strongly typed, `EventLoopFuture` based asynchronous processing protocol for a Lambda that takes a user defined `In` and returns a user defined `Out`. - -[`EventLoopLambdaHandler`][ellh] extends [`ByteBufferLambdaHandler`][bblh], providing `ByteBuffer` -> `In` decoding and `Out` -> `ByteBuffer?` encoding for `Codable` and `String`. - -[`EventLoopLambdaHandler`][ellh] executes the user provided Lambda on the same `EventLoop` as the core runtime engine, making the processing fast but requires more care from the implementation to never block the `EventLoop`. It is designed for performance sensitive applications that use `Codable` or `String` based Lambda functions. - -```swift -public protocol EventLoopLambdaHandler: ByteBufferLambdaHandler { - associatedtype In - associatedtype Out - - /// The Lambda handling method - /// Concrete Lambda handlers implement this method to provide the Lambda functionality. - /// - /// - parameters: - /// - context: Runtime `Context`. - /// - event: Event of type `In` representing the event or request. - /// - /// - Returns: An `EventLoopFuture` to report the result of the Lambda back to the runtime engine. - /// The `EventLoopFuture` should be completed with either a response of type `Out` or an `Error` - func handle(context: Lambda.Context, event: In) -> EventLoopFuture - - /// Encode a response of type `Out` to `ByteBuffer` - /// Concrete Lambda handlers implement this method to provide coding functionality. - /// - parameters: - /// - allocator: A `ByteBufferAllocator` to help allocate the `ByteBuffer`. - /// - value: Response of type `Out`. - /// - /// - Returns: A `ByteBuffer` with the encoded version of the `value`. - func encode(allocator: ByteBufferAllocator, value: Out) throws -> ByteBuffer? - - /// Decode a`ByteBuffer` to a request or event of type `In` - /// Concrete Lambda handlers implement this method to provide coding functionality. - /// - /// - parameters: - /// - buffer: The `ByteBuffer` to decode. - /// - /// - Returns: A request or event of type `In`. - func decode(buffer: ByteBuffer) throws -> In -} -``` - -### LambdaHandler - -[`LambdaHandler`][lh] is a strongly typed, completion handler based asynchronous processing protocol for a Lambda that takes a user defined `In` and returns a user defined `Out`. - -[`LambdaHandler`][lh] extends [`ByteBufferLambdaHandler`][bblh], performing `ByteBuffer` -> `In` decoding and `Out` -> `ByteBuffer` encoding for `Codable` and `String`. - -[`LambdaHandler`][lh] offloads the user provided Lambda execution to a `DispatchQueue` making processing safer but slower. - -```swift -public protocol LambdaHandler: EventLoopLambdaHandler { - /// Defines to which `DispatchQueue` the Lambda execution is offloaded to. - var offloadQueue: DispatchQueue { get } - - /// The Lambda handling method - /// Concrete Lambda handlers implement this method to provide the Lambda functionality. - /// - /// - parameters: - /// - context: Runtime `Context`. - /// - event: Event of type `In` representing the event or request. - /// - callback: Completion handler to report the result of the Lambda back to the runtime engine. - /// The completion handler expects a `Result` with either a response of type `Out` or an `Error` - func handle(context: Lambda.Context, event: In, callback: @escaping (Result) -> Void) -} -``` - -### Closures - -In addition to protocol-based Lambda, the library provides support for Closure-based ones, as demonstrated in the overview section above. Closure-based Lambdas are based on the [`LambdaHandler`][lh] protocol which mean they are safer. For most use cases, Closure-based Lambda is a great fit and users are encouraged to use them. - -The library includes implementations for `Codable` and `String` based Lambda. Since AWS Lambda is primarily JSON based, this covers the most common use cases. - -```swift -public typealias CodableClosure = (Lambda.Context, In, @escaping (Result) -> Void) -> Void -``` - -```swift -public typealias StringClosure = (Lambda.Context, String, @escaping (Result) -> Void) -> Void -``` - -This design allows for additional event types as well, and such Lambda implementation can extend one of the above protocols and provided their own `ByteBuffer` -> `In` decoding and `Out` -> `ByteBuffer` encoding. - -### Context - -When calling the user provided Lambda function, the library provides a `Context` class that provides metadata about the execution context, as well as utilities for logging and allocating buffers. - -```swift -public final class Context { - /// The request ID, which identifies the request that triggered the function invocation. - public let requestID: String - - /// The AWS X-Ray tracing header. - public let traceID: String - - /// The ARN of the Lambda function, version, or alias that's specified in the invocation. - public let invokedFunctionARN: String - - /// The timestamp that the function times out - public let deadline: DispatchWallTime - - /// For invocations from the AWS Mobile SDK, data about the Amazon Cognito identity provider. - public let cognitoIdentity: String? - - /// For invocations from the AWS Mobile SDK, data about the client application and device. - public let clientContext: String? - - /// `Logger` to log with - /// - /// - note: The `LogLevel` can be configured using the `LOG_LEVEL` environment variable. - public let logger: Logger - - /// The `EventLoop` the Lambda is executed on. Use this to schedule work with. - /// This is useful when implementing the `EventLoopLambdaHandler` protocol. - /// - /// - note: The `EventLoop` is shared with the Lambda runtime engine and should be handled with extra care. - /// Most importantly the `EventLoop` must never be blocked. - public let eventLoop: EventLoop - - /// `ByteBufferAllocator` to allocate `ByteBuffer` - /// This is useful when implementing `EventLoopLambdaHandler` - public let allocator: ByteBufferAllocator -} -``` - -### Configuration - -The library’s behavior can be fine tuned using environment variables based configuration. The library supported the following environment variables: - -* `LOG_LEVEL`: Define the logging level as defined by [SwiftLog](https://github.com/apple/swift-log). Set to INFO by default. -* `MAX_REQUESTS`: Max cycles the library should handle before exiting. Set to none by default. -* `STOP_SIGNAL`: Signal to capture for termination. Set to `TERM` by default. -* `REQUEST_TIMEOUT`: Max time to wait for responses to come back from the AWS Runtime engine. Set to none by default. - - -### AWS Lambda Runtime Engine Integration - -The library is designed to integrate with AWS Lambda Runtime Engine via the [AWS Lambda Runtime API](https://docs.aws.amazon.com/lambda/latest/dg/runtimes-custom.html) which was introduced as part of [AWS Lambda Custom Runtimes](https://aws.amazon.com/about-aws/whats-new/2018/11/aws-lambda-now-supports-custom-runtimes-and-layers/) in 2018. The latter is an HTTP server that exposes three main RESTful endpoint: - -* `/runtime/invocation/next` -* `/runtime/invocation/response` -* `/runtime/invocation/error` - -A single Lambda execution workflow is made of the following steps: - -1. The library calls AWS Lambda Runtime Engine `/next` endpoint to retrieve the next invocation request. -2. The library parses the response HTTP headers and populate the `Context` object. -3. The library reads the `/next` response body and attempt to decode it. Typically it decodes to user provided `In` type which extends `Decodable`, but users may choose to write Lambda functions that receive the input as `String` or `ByteBuffer` which require less, or no decoding. -4. The library hands off the `Context` and `In` event to the user provided handler. In the case of [`LambdaHandler`][lh] based handler this is done on a dedicated `DispatchQueue`, providing isolation between user's and the library's code. -5. User provided handler processes the request asynchronously, invoking a callback or returning a future upon completion, which returns a `Result` type with the `Out` or `Error` populated. -6. In case of error, the library posts to AWS Lambda Runtime Engine `/error` endpoint to provide the error details, which will show up on AWS Lambda logs. -7. In case of success, the library will attempt to encode the response. Typically it encodes from user provided `Out` type which extends `Encodable`, but users may choose to write Lambda functions that return a `String` or `ByteBuffer`, which require less, or no encoding. The library then posts the response to AWS Lambda Runtime Engine `/response` endpoint to provide the response to the callee. - -The library encapsulates the workflow via the internal `LambdaRuntimeClient` and `LambdaRunner` structs respectively. - -### Lifecycle Management - -AWS Lambda Runtime Engine controls the Application lifecycle and in the happy case never terminates the application, only suspends its execution when no work is available. - -As such, the library's main entry point is designed to run forever in a blocking fashion, performing the workflow described above in an endless loop. - -That loop is broken if/when an internal error occurs, such as a failure to communicate with AWS Lambda Runtime Engine API, or under other unexpected conditions. - -By default, the library also registers a Signal handler that traps `INT` and `TERM`, which are typical Signals used in modern deployment platforms to communicate shutdown request. - -### Integration with AWS Platform Events - -AWS Lambda functions can be invoked directly from the AWS Lambda console UI, AWS Lambda API, AWS SDKs, AWS CLI, and AWS toolkits. More commonly, they are invoked as a reaction to an event coming from the AWS platform. To make it easier to integrate with AWS platform events, [Swift AWS Lambda Runtime Events](http://github.com/swift-server/swift-aws-lambda-events) library is available, designed to work together with this runtime library. [Swift AWS Lambda Runtime Events](http://github.com/swift-server/swift-aws-lambda-events) includes an `AWSLambdaEvents` target which provides abstractions for many commonly used events. - -## Performance - -Lambda functions performance is usually measured across two axes: - -- **Cold start times**: The time it takes for a Lambda function to startup, ask for an invocation and process the first invocation. - -- **Warm invocation times**: The time it takes for a Lambda function to process an invocation after the Lambda has been invoked at least once. - -Larger packages size (Zip file uploaded to AWS Lambda) negatively impact the cold start time, since AWS needs to download and unpack the package before starting the process. - -Swift provides great Unicode support via [ICU](http://site.icu-project.org/home). Therefore, Swift-based Lambda functions include the ICU libraries which tend to be large. This impacts the download time mentioned above and an area for further optimization. Some of the alternatives worth exploring are using the system ICU that comes with Amazon Linux (albeit older than the one Swift ships with) or working to remove the ICU dependency altogether. We welcome ideas and contributions to this end. - - - -[lh]: ./AWSLambdaRuntimeCore/Protocols/LambdaHandler.html -[ellh]: ./AWSLambdaRuntimeCore/Protocols/EventLoopLambdaHandler.html -[bblh]: ./AWSLambdaRuntimeCore/Protocols/ByteBufferLambdaHandler.html diff --git a/Sources/AWSLambdaRuntimeCore/Documentation.docc/quick-setup.md b/Sources/AWSLambdaRuntime/Docs.docc/quick-setup.md similarity index 100% rename from Sources/AWSLambdaRuntimeCore/Documentation.docc/quick-setup.md rename to Sources/AWSLambdaRuntime/Docs.docc/quick-setup.md diff --git a/Sources/AWSLambdaRuntimeCore/Documentation.docc/tutorials/01-overview.tutorial b/Sources/AWSLambdaRuntime/Docs.docc/tutorials/01-overview.tutorial similarity index 100% rename from Sources/AWSLambdaRuntimeCore/Documentation.docc/tutorials/01-overview.tutorial rename to Sources/AWSLambdaRuntime/Docs.docc/tutorials/01-overview.tutorial diff --git a/Sources/AWSLambdaRuntimeCore/Documentation.docc/tutorials/02-what-is-lambda.tutorial b/Sources/AWSLambdaRuntime/Docs.docc/tutorials/02-what-is-lambda.tutorial similarity index 100% rename from Sources/AWSLambdaRuntimeCore/Documentation.docc/tutorials/02-what-is-lambda.tutorial rename to Sources/AWSLambdaRuntime/Docs.docc/tutorials/02-what-is-lambda.tutorial diff --git a/Sources/AWSLambdaRuntimeCore/Documentation.docc/tutorials/03-prerequisites.tutorial b/Sources/AWSLambdaRuntime/Docs.docc/tutorials/03-prerequisites.tutorial similarity index 100% rename from Sources/AWSLambdaRuntimeCore/Documentation.docc/tutorials/03-prerequisites.tutorial rename to Sources/AWSLambdaRuntime/Docs.docc/tutorials/03-prerequisites.tutorial diff --git a/Sources/AWSLambdaRuntimeCore/Documentation.docc/tutorials/03-write-function.tutorial b/Sources/AWSLambdaRuntime/Docs.docc/tutorials/03-write-function.tutorial similarity index 100% rename from Sources/AWSLambdaRuntimeCore/Documentation.docc/tutorials/03-write-function.tutorial rename to Sources/AWSLambdaRuntime/Docs.docc/tutorials/03-write-function.tutorial diff --git a/Sources/AWSLambdaRuntimeCore/Documentation.docc/tutorials/04-deploy-function.tutorial b/Sources/AWSLambdaRuntime/Docs.docc/tutorials/04-deploy-function.tutorial similarity index 100% rename from Sources/AWSLambdaRuntimeCore/Documentation.docc/tutorials/04-deploy-function.tutorial rename to Sources/AWSLambdaRuntime/Docs.docc/tutorials/04-deploy-function.tutorial diff --git a/Sources/AWSLambdaRuntimeCore/Documentation.docc/tutorials/table-of-content.tutorial b/Sources/AWSLambdaRuntime/Docs.docc/tutorials/table-of-content.tutorial similarity index 100% rename from Sources/AWSLambdaRuntimeCore/Documentation.docc/tutorials/table-of-content.tutorial rename to Sources/AWSLambdaRuntime/Docs.docc/tutorials/table-of-content.tutorial diff --git a/Sources/AWSLambdaRuntime/Context+Foundation.swift b/Sources/AWSLambdaRuntime/FoundationSupport/Context+Foundation.swift similarity index 92% rename from Sources/AWSLambdaRuntime/Context+Foundation.swift rename to Sources/AWSLambdaRuntime/FoundationSupport/Context+Foundation.swift index 105b7765..72b7a65d 100644 --- a/Sources/AWSLambdaRuntime/Context+Foundation.swift +++ b/Sources/AWSLambdaRuntime/FoundationSupport/Context+Foundation.swift @@ -12,8 +12,7 @@ // //===----------------------------------------------------------------------===// -import AWSLambdaRuntimeCore - +#if FoundationJSONSupport #if canImport(FoundationEssentials) import FoundationEssentials #else @@ -26,3 +25,4 @@ extension LambdaContext { return Date(timeIntervalSince1970: secondsSinceEpoch) } } +#endif // trait: FoundationJSONSupport diff --git a/Sources/AWSLambdaRuntime/FoundationSupport/Lambda+JSON.swift b/Sources/AWSLambdaRuntime/FoundationSupport/Lambda+JSON.swift new file mode 100644 index 00000000..9bd4d30f --- /dev/null +++ b/Sources/AWSLambdaRuntime/FoundationSupport/Lambda+JSON.swift @@ -0,0 +1,138 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the SwiftAWSLambdaRuntime open source project +// +// Copyright (c) 2017-2022 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 +// +//===----------------------------------------------------------------------===// + +#if FoundationJSONSupport +import NIOCore + +#if canImport(FoundationEssentials) +import FoundationEssentials +#else +import struct Foundation.Data +import class Foundation.JSONDecoder +import class Foundation.JSONEncoder +#endif + +public struct LambdaJSONEventDecoder: LambdaEventDecoder { + @usableFromInline let jsonDecoder: JSONDecoder + + @inlinable + public init(_ jsonDecoder: JSONDecoder) { + self.jsonDecoder = jsonDecoder + } + + @inlinable + public func decode(_ type: Event.Type, from buffer: NIOCore.ByteBuffer) throws -> Event + where Event: Decodable { + try buffer.getJSONDecodable( + Event.self, + decoder: self.jsonDecoder, + at: buffer.readerIndex, + length: buffer.readableBytes + )! // must work, enough readable bytes + } +} + +public struct LambdaJSONOutputEncoder: LambdaOutputEncoder { + @usableFromInline let jsonEncoder: JSONEncoder + + @inlinable + public init(_ jsonEncoder: JSONEncoder) { + self.jsonEncoder = jsonEncoder + } + + @inlinable + public func encode(_ value: Output, into buffer: inout ByteBuffer) throws { + try buffer.writeJSONEncodable(value, encoder: self.jsonEncoder) + } +} + +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`. 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 = JSONEncoder(), + decoder: JSONDecoder = JSONDecoder(), + handler: sending Handler + ) + where + Output: Encodable, + Output == Handler.Output, + Encoder == LambdaJSONOutputEncoder, + Decoder == LambdaJSONEventDecoder + { + self.init( + encoder: LambdaJSONOutputEncoder(encoder), + decoder: LambdaJSONEventDecoder(decoder), + handler: handler + ) + } +} + +extension LambdaRuntime { + /// Initialize an instance with a `LambdaHandler` defined in the form of a closure **with a non-`Void` return type**. + /// - Parameters: + /// - decoder: The decoder object that will be used to decode the incoming `ByteBuffer` event into the generic `Event` type. `JSONDecoder()` used as default. + /// - encoder: The encoder object that will be used to encode the generic `Output` into a `ByteBuffer`. `JSONEncoder()` used as default. + /// - body: The handler in the form of a closure. + public convenience init( + decoder: JSONDecoder = JSONDecoder(), + encoder: JSONEncoder = JSONEncoder(), + body: sending @escaping (Event, LambdaContext) async throws -> Output + ) + where + Handler == LambdaCodableAdapter< + LambdaHandlerAdapter>, + Event, + Output, + LambdaJSONEventDecoder, + LambdaJSONOutputEncoder + > + { + let handler = LambdaCodableAdapter( + encoder: encoder, + decoder: decoder, + handler: LambdaHandlerAdapter(handler: ClosureHandler(body: body)) + ) + + self.init(handler: handler) + } + + /// Initialize an instance with a `LambdaHandler` defined in the form of a closure **with a `Void` return type**. + /// - Parameter body: The handler in the form of a closure. + /// - Parameter decoder: The decoder object that will be used to decode the incoming `ByteBuffer` event into the generic `Event` type. `JSONDecoder()` used as default. + public convenience init( + decoder: JSONDecoder = JSONDecoder(), + body: sending @escaping (Event, LambdaContext) async throws -> Void + ) + where + Handler == LambdaCodableAdapter< + LambdaHandlerAdapter>, + Event, + Void, + LambdaJSONEventDecoder, + VoidEncoder + > + { + let handler = LambdaCodableAdapter( + decoder: LambdaJSONEventDecoder(decoder), + handler: LambdaHandlerAdapter(handler: ClosureHandler(body: body)) + ) + + self.init(handler: handler) + } +} +#endif // trait: FoundationJSONSupport diff --git a/Sources/AWSLambdaRuntime/Vendored/ByteBuffer-foundation.swift b/Sources/AWSLambdaRuntime/FoundationSupport/Vendored/ByteBuffer-foundation.swift similarity index 98% rename from Sources/AWSLambdaRuntime/Vendored/ByteBuffer-foundation.swift rename to Sources/AWSLambdaRuntime/FoundationSupport/Vendored/ByteBuffer-foundation.swift index 8dbd7326..482e020f 100644 --- a/Sources/AWSLambdaRuntime/Vendored/ByteBuffer-foundation.swift +++ b/Sources/AWSLambdaRuntime/FoundationSupport/Vendored/ByteBuffer-foundation.swift @@ -26,6 +26,7 @@ // //===----------------------------------------------------------------------===// +#if FoundationJSONSupport import NIOCore #if canImport(FoundationEssentials) @@ -104,3 +105,4 @@ extension ByteBuffer { } } } +#endif // trait: FoundationJSONSupport diff --git a/Sources/AWSLambdaRuntime/Vendored/JSON+ByteBuffer.swift b/Sources/AWSLambdaRuntime/FoundationSupport/Vendored/JSON+ByteBuffer.swift similarity index 98% rename from Sources/AWSLambdaRuntime/Vendored/JSON+ByteBuffer.swift rename to Sources/AWSLambdaRuntime/FoundationSupport/Vendored/JSON+ByteBuffer.swift index 092b2368..89ce9b87 100644 --- a/Sources/AWSLambdaRuntime/Vendored/JSON+ByteBuffer.swift +++ b/Sources/AWSLambdaRuntime/FoundationSupport/Vendored/JSON+ByteBuffer.swift @@ -26,6 +26,7 @@ // //===----------------------------------------------------------------------===// +#if FoundationJSONSupport import NIOCore #if canImport(FoundationEssentials) @@ -147,3 +148,4 @@ extension JSONEncoder { return buffer } } +#endif // trait: FoundationJSONSupport diff --git a/Sources/AWSLambdaRuntime/Lambda+Codable.swift b/Sources/AWSLambdaRuntime/Lambda+Codable.swift index ccb37dd8..a77c0542 100644 --- a/Sources/AWSLambdaRuntime/Lambda+Codable.swift +++ b/Sources/AWSLambdaRuntime/Lambda+Codable.swift @@ -2,7 +2,7 @@ // // This source file is part of the SwiftAWSLambdaRuntime open source project // -// Copyright (c) 2017-2022 Apple Inc. and the SwiftAWSLambdaRuntime project authors +// Copyright (c) 2024 Apple Inc. and the SwiftAWSLambdaRuntime project authors // Licensed under Apache License v2.0 // // See LICENSE.txt for license information @@ -12,126 +12,151 @@ // //===----------------------------------------------------------------------===// -@_exported import AWSLambdaRuntimeCore import NIOCore -#if canImport(FoundationEssentials) -import FoundationEssentials -#else -import struct Foundation.Data -import class Foundation.JSONDecoder -import class Foundation.JSONEncoder -#endif - -public struct LambdaJSONEventDecoder: LambdaEventDecoder { - @usableFromInline let jsonDecoder: JSONDecoder - - @inlinable - public init(_ jsonDecoder: JSONDecoder) { - self.jsonDecoder = jsonDecoder - } - - @inlinable - public func decode(_ type: Event.Type, from buffer: NIOCore.ByteBuffer) throws -> Event - where Event: Decodable { - try buffer.getJSONDecodable( - Event.self, - decoder: self.jsonDecoder, - at: buffer.readerIndex, - length: buffer.readableBytes - )! // must work, enough readable bytes - } -} - -public struct LambdaJSONOutputEncoder: LambdaOutputEncoder { - @usableFromInline let jsonEncoder: JSONEncoder - - @inlinable - public init(_ jsonEncoder: JSONEncoder) { - self.jsonEncoder = jsonEncoder - } - - @inlinable - public func encode(_ value: Output, into buffer: inout ByteBuffer) throws { - try buffer.writeJSONEncodable(value, encoder: self.jsonEncoder) - } -} - -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`. 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 = JSONEncoder(), - decoder: JSONDecoder = JSONDecoder(), - handler: sending Handler - ) - where - Output: Encodable, - Output == Handler.Output, - Encoder == LambdaJSONOutputEncoder, - Decoder == LambdaJSONEventDecoder - { - self.init( - encoder: LambdaJSONOutputEncoder(encoder), - decoder: LambdaJSONEventDecoder(decoder), - handler: handler - ) - } -} - -extension LambdaRuntime { - /// Initialize an instance with a `LambdaHandler` defined in the form of a closure **with a non-`Void` return type**. +/// The protocol a decoder must conform to so that it can be used with ``LambdaCodableAdapter`` to decode incoming +/// `ByteBuffer` events. +public protocol LambdaEventDecoder { + /// Decode the `ByteBuffer` representing the received event into the generic `Event` type + /// the handler will receive. /// - Parameters: - /// - decoder: The decoder object that will be used to decode the incoming `ByteBuffer` event into the generic `Event` type. `JSONDecoder()` used as default. - /// - encoder: The encoder object that will be used to encode the generic `Output` into a `ByteBuffer`. `JSONEncoder()` used as default. - /// - body: The handler in the form of a closure. - public convenience init( - decoder: JSONDecoder = JSONDecoder(), - encoder: JSONEncoder = JSONEncoder(), - body: sending @escaping (Event, LambdaContext) async throws -> Output - ) - where - Handler == LambdaCodableAdapter< - LambdaHandlerAdapter>, - Event, - Output, - LambdaJSONEventDecoder, - LambdaJSONOutputEncoder - > - { - let handler = LambdaCodableAdapter( - encoder: encoder, - decoder: decoder, - handler: LambdaHandlerAdapter(handler: ClosureHandler(body: body)) - ) + /// - type: The type of the object to decode the buffer into. + /// - buffer: The buffer to be decoded. + /// - Returns: An object containing the decoded data. + func decode(_ type: Event.Type, from buffer: ByteBuffer) throws -> Event +} - self.init(handler: handler) +/// The protocol an encoder must conform to so that it can be used with ``LambdaCodableAdapter`` to encode the generic +/// ``LambdaOutputEncoder/Output`` object into a `ByteBuffer`. +public protocol LambdaOutputEncoder { + associatedtype Output + + /// Encode the generic type `Output` the handler has returned into a `ByteBuffer`. + /// - Parameters: + /// - value: The object to encode into a `ByteBuffer`. + /// - buffer: The `ByteBuffer` where the encoded value will be written to. + func encode(_ value: Output, into buffer: inout ByteBuffer) throws +} + +public struct VoidEncoder: LambdaOutputEncoder { + public typealias Output = Void + + public init() {} + + @inlinable + public func encode(_ value: Void, into buffer: inout NIOCore.ByteBuffer) throws {} +} + +/// Adapts a ``LambdaHandler`` conforming handler to conform to ``LambdaWithBackgroundProcessingHandler``. +public struct LambdaHandlerAdapter< + Event: Decodable, + Output, + Handler: LambdaHandler +>: LambdaWithBackgroundProcessingHandler where Handler.Event == Event, Handler.Output == Output { + @usableFromInline let handler: Handler + + /// Initializes an instance given a concrete handler. + /// - Parameter handler: The ``LambdaHandler`` conforming handler that is to be adapted to ``LambdaWithBackgroundProcessingHandler``. + @inlinable + public init(handler: sending Handler) { + self.handler = handler } - /// Initialize an instance with a `LambdaHandler` defined in the form of a closure **with a `Void` return type**. - /// - Parameter body: The handler in the form of a closure. - /// - Parameter decoder: The decoder object that will be used to decode the incoming `ByteBuffer` event into the generic `Event` type. `JSONDecoder()` used as default. - public convenience init( - decoder: JSONDecoder = JSONDecoder(), - body: sending @escaping (Event, LambdaContext) async throws -> Void - ) - where - Handler == LambdaCodableAdapter< - LambdaHandlerAdapter>, - Event, - Void, - LambdaJSONEventDecoder, - VoidEncoder - > - { - let handler = LambdaCodableAdapter( - decoder: LambdaJSONEventDecoder(decoder), - handler: LambdaHandlerAdapter(handler: ClosureHandler(body: body)) + /// Passes the generic `Event` object to the ``LambdaHandler/handle(_:context:)`` function, and + /// the resulting output is then written to ``LambdaWithBackgroundProcessingHandler``'s `outputWriter`. + /// - Parameters: + /// - event: The received event. + /// - outputWriter: The writer to write the computed response to. + /// - context: The ``LambdaContext`` containing the invocation's metadata. + @inlinable + public func handle( + _ event: Event, + outputWriter: some LambdaResponseWriter, + context: LambdaContext + ) async throws { + let output = try await self.handler.handle(event, context: context) + try await outputWriter.write(output) + } +} + +/// Adapts a ``LambdaWithBackgroundProcessingHandler`` conforming handler to conform to ``StreamingLambdaHandler``. +public struct LambdaCodableAdapter< + Handler: LambdaWithBackgroundProcessingHandler, + Event: Decodable, + Output, + Decoder: LambdaEventDecoder, + Encoder: LambdaOutputEncoder +>: StreamingLambdaHandler where Handler.Event == Event, Handler.Output == Output, Encoder.Output == Output { + @usableFromInline let handler: Handler + @usableFromInline let encoder: Encoder + @usableFromInline let decoder: Decoder + @usableFromInline var byteBuffer: ByteBuffer = .init() + + /// 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`. + /// - handler: The handler object. + @inlinable + public init(encoder: sending Encoder, decoder: sending Decoder, handler: sending Handler) where Output: Encodable { + self.encoder = encoder + self.decoder = decoder + self.handler = handler + } + + /// Initializes an instance given a decoder, and a handler with a `Void` output. + /// - Parameters: + /// - decoder: The decoder object that will be used to decode the received `ByteBuffer` event into the generic `Event` type served to the `handler`. + /// - handler: The handler object. + @inlinable + public init(decoder: sending Decoder, handler: Handler) where Output == Void, Encoder == VoidEncoder { + self.encoder = VoidEncoder() + self.decoder = decoder + self.handler = handler + } + + /// A ``StreamingLambdaHandler/handle(_:responseWriter:context:)`` wrapper. + /// - Parameters: + /// - event: The received event. + /// - outputWriter: The writer to write the computed response to. + /// - context: The ``LambdaContext`` containing the invocation's metadata. + @inlinable + public mutating func handle( + _ request: ByteBuffer, + responseWriter: Writer, + context: LambdaContext + ) async throws { + let event = try self.decoder.decode(Event.self, from: request) + + let writer = LambdaCodableResponseWriter( + encoder: self.encoder, + streamWriter: responseWriter ) + try await self.handler.handle(event, outputWriter: writer, context: context) + } +} + +/// A ``LambdaResponseStreamWriter`` wrapper that conforms to ``LambdaResponseWriter``. +public struct LambdaCodableResponseWriter: + LambdaResponseWriter +where Output == Encoder.Output { + @usableFromInline let underlyingStreamWriter: Base + @usableFromInline let encoder: Encoder + + /// Initializes an instance given an encoder and an underlying ``LambdaResponseStreamWriter``. + /// - Parameters: + /// - encoder: The encoder object that will be used to encode the generic `Output` into a `ByteBuffer`, which will then be passed to `streamWriter`. + /// - streamWriter: The underlying ``LambdaResponseStreamWriter`` that will be wrapped. + @inlinable + public init(encoder: Encoder, streamWriter: Base) { + self.encoder = encoder + self.underlyingStreamWriter = streamWriter + } - self.init(handler: handler) + @inlinable + public func write(_ output: Output) async throws { + var outputBuffer = ByteBuffer() + try self.encoder.encode(output, into: &outputBuffer) + try await self.underlyingStreamWriter.writeAndFinish(outputBuffer) } } diff --git a/Sources/AWSLambdaRuntimeCore/Lambda+LocalServer.swift b/Sources/AWSLambdaRuntime/Lambda+LocalServer.swift similarity index 99% rename from Sources/AWSLambdaRuntimeCore/Lambda+LocalServer.swift rename to Sources/AWSLambdaRuntime/Lambda+LocalServer.swift index bee880cc..4d85f7b2 100644 --- a/Sources/AWSLambdaRuntimeCore/Lambda+LocalServer.swift +++ b/Sources/AWSLambdaRuntime/Lambda+LocalServer.swift @@ -12,7 +12,7 @@ // //===----------------------------------------------------------------------===// -#if DEBUG +#if LocalServerSupport import DequeModule import Dispatch import Logging @@ -524,7 +524,6 @@ private struct LambdaHTTPServer { } } } -#endif extension Result { var maybeError: Failure? { @@ -536,3 +535,4 @@ extension Result { } } } +#endif diff --git a/Sources/AWSLambdaRuntimeCore/Lambda.swift b/Sources/AWSLambdaRuntime/Lambda.swift similarity index 100% rename from Sources/AWSLambdaRuntimeCore/Lambda.swift rename to Sources/AWSLambdaRuntime/Lambda.swift diff --git a/Sources/AWSLambdaRuntimeCore/LambdaContext.swift b/Sources/AWSLambdaRuntime/LambdaContext.swift similarity index 100% rename from Sources/AWSLambdaRuntimeCore/LambdaContext.swift rename to Sources/AWSLambdaRuntime/LambdaContext.swift diff --git a/Sources/AWSLambdaRuntimeCore/LambdaHandlers.swift b/Sources/AWSLambdaRuntime/LambdaHandlers.swift similarity index 100% rename from Sources/AWSLambdaRuntimeCore/LambdaHandlers.swift rename to Sources/AWSLambdaRuntime/LambdaHandlers.swift diff --git a/Sources/AWSLambdaRuntimeCore/LambdaRequestID.swift b/Sources/AWSLambdaRuntime/LambdaRequestID.swift similarity index 100% rename from Sources/AWSLambdaRuntimeCore/LambdaRequestID.swift rename to Sources/AWSLambdaRuntime/LambdaRequestID.swift diff --git a/Sources/AWSLambdaRuntime/LambdaRuntime+ServiceLifecycle.swift b/Sources/AWSLambdaRuntime/LambdaRuntime+ServiceLifecycle.swift new file mode 100644 index 00000000..54ecb537 --- /dev/null +++ b/Sources/AWSLambdaRuntime/LambdaRuntime+ServiceLifecycle.swift @@ -0,0 +1,19 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the SwiftAWSLambdaRuntime open source project +// +// Copyright (c) 2025 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 +// +//===----------------------------------------------------------------------===// + +#if ServiceLifecycleSupport +import ServiceLifecycle + +extension LambdaRuntime: Service {} +#endif diff --git a/Sources/AWSLambdaRuntimeCore/LambdaRuntime.swift b/Sources/AWSLambdaRuntime/LambdaRuntime.swift similarity index 99% rename from Sources/AWSLambdaRuntimeCore/LambdaRuntime.swift rename to Sources/AWSLambdaRuntime/LambdaRuntime.swift index 317ee7ea..6bc2403c 100644 --- a/Sources/AWSLambdaRuntimeCore/LambdaRuntime.swift +++ b/Sources/AWSLambdaRuntime/LambdaRuntime.swift @@ -82,7 +82,7 @@ public final class LambdaRuntime: @unchecked Sendable where Handler: St } else { - #if DEBUG + #if LocalServerSupport // we're not running on Lambda and we're compiled in DEBUG mode, // let's start a local server for testing try await Lambda.withLocalServer(invocationEndpoint: Lambda.env("LOCAL_LAMBDA_SERVER_INVOCATION_ENDPOINT")) diff --git a/Sources/AWSLambdaRuntimeCore/LambdaRuntimeClient.swift b/Sources/AWSLambdaRuntime/LambdaRuntimeClient.swift similarity index 100% rename from Sources/AWSLambdaRuntimeCore/LambdaRuntimeClient.swift rename to Sources/AWSLambdaRuntime/LambdaRuntimeClient.swift diff --git a/Sources/AWSLambdaRuntimeCore/LambdaRuntimeClientProtocol.swift b/Sources/AWSLambdaRuntime/LambdaRuntimeClientProtocol.swift similarity index 100% rename from Sources/AWSLambdaRuntimeCore/LambdaRuntimeClientProtocol.swift rename to Sources/AWSLambdaRuntime/LambdaRuntimeClientProtocol.swift diff --git a/Sources/AWSLambdaRuntimeCore/LambdaRuntimeError.swift b/Sources/AWSLambdaRuntime/LambdaRuntimeError.swift similarity index 100% rename from Sources/AWSLambdaRuntimeCore/LambdaRuntimeError.swift rename to Sources/AWSLambdaRuntime/LambdaRuntimeError.swift diff --git a/Sources/AWSLambdaRuntimeCore/Utils.swift b/Sources/AWSLambdaRuntime/Utils.swift similarity index 100% rename from Sources/AWSLambdaRuntimeCore/Utils.swift rename to Sources/AWSLambdaRuntime/Utils.swift diff --git a/Sources/AWSLambdaRuntimeCore/Lambda+Codable.swift b/Sources/AWSLambdaRuntimeCore/Lambda+Codable.swift deleted file mode 100644 index a77c0542..00000000 --- a/Sources/AWSLambdaRuntimeCore/Lambda+Codable.swift +++ /dev/null @@ -1,162 +0,0 @@ -//===----------------------------------------------------------------------===// -// -// 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 NIOCore - -/// The protocol a decoder must conform to so that it can be used with ``LambdaCodableAdapter`` to decode incoming -/// `ByteBuffer` events. -public protocol LambdaEventDecoder { - /// Decode the `ByteBuffer` representing the received event into the generic `Event` type - /// the handler will receive. - /// - Parameters: - /// - type: The type of the object to decode the buffer into. - /// - buffer: The buffer to be decoded. - /// - Returns: An object containing the decoded data. - func decode(_ type: Event.Type, from buffer: ByteBuffer) throws -> Event -} - -/// The protocol an encoder must conform to so that it can be used with ``LambdaCodableAdapter`` to encode the generic -/// ``LambdaOutputEncoder/Output`` object into a `ByteBuffer`. -public protocol LambdaOutputEncoder { - associatedtype Output - - /// Encode the generic type `Output` the handler has returned into a `ByteBuffer`. - /// - Parameters: - /// - value: The object to encode into a `ByteBuffer`. - /// - buffer: The `ByteBuffer` where the encoded value will be written to. - func encode(_ value: Output, into buffer: inout ByteBuffer) throws -} - -public struct VoidEncoder: LambdaOutputEncoder { - public typealias Output = Void - - public init() {} - - @inlinable - public func encode(_ value: Void, into buffer: inout NIOCore.ByteBuffer) throws {} -} - -/// Adapts a ``LambdaHandler`` conforming handler to conform to ``LambdaWithBackgroundProcessingHandler``. -public struct LambdaHandlerAdapter< - Event: Decodable, - Output, - Handler: LambdaHandler ->: LambdaWithBackgroundProcessingHandler where Handler.Event == Event, Handler.Output == Output { - @usableFromInline let handler: Handler - - /// Initializes an instance given a concrete handler. - /// - Parameter handler: The ``LambdaHandler`` conforming handler that is to be adapted to ``LambdaWithBackgroundProcessingHandler``. - @inlinable - public init(handler: sending Handler) { - self.handler = handler - } - - /// Passes the generic `Event` object to the ``LambdaHandler/handle(_:context:)`` function, and - /// the resulting output is then written to ``LambdaWithBackgroundProcessingHandler``'s `outputWriter`. - /// - Parameters: - /// - event: The received event. - /// - outputWriter: The writer to write the computed response to. - /// - context: The ``LambdaContext`` containing the invocation's metadata. - @inlinable - public func handle( - _ event: Event, - outputWriter: some LambdaResponseWriter, - context: LambdaContext - ) async throws { - let output = try await self.handler.handle(event, context: context) - try await outputWriter.write(output) - } -} - -/// Adapts a ``LambdaWithBackgroundProcessingHandler`` conforming handler to conform to ``StreamingLambdaHandler``. -public struct LambdaCodableAdapter< - Handler: LambdaWithBackgroundProcessingHandler, - Event: Decodable, - Output, - Decoder: LambdaEventDecoder, - Encoder: LambdaOutputEncoder ->: StreamingLambdaHandler where Handler.Event == Event, Handler.Output == Output, Encoder.Output == Output { - @usableFromInline let handler: Handler - @usableFromInline let encoder: Encoder - @usableFromInline let decoder: Decoder - @usableFromInline var byteBuffer: ByteBuffer = .init() - - /// 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`. - /// - handler: The handler object. - @inlinable - public init(encoder: sending Encoder, decoder: sending Decoder, handler: sending Handler) where Output: Encodable { - self.encoder = encoder - self.decoder = decoder - self.handler = handler - } - - /// Initializes an instance given a decoder, and a handler with a `Void` output. - /// - Parameters: - /// - decoder: The decoder object that will be used to decode the received `ByteBuffer` event into the generic `Event` type served to the `handler`. - /// - handler: The handler object. - @inlinable - public init(decoder: sending Decoder, handler: Handler) where Output == Void, Encoder == VoidEncoder { - self.encoder = VoidEncoder() - self.decoder = decoder - self.handler = handler - } - - /// A ``StreamingLambdaHandler/handle(_:responseWriter:context:)`` wrapper. - /// - Parameters: - /// - event: The received event. - /// - outputWriter: The writer to write the computed response to. - /// - context: The ``LambdaContext`` containing the invocation's metadata. - @inlinable - public mutating func handle( - _ request: ByteBuffer, - responseWriter: Writer, - context: LambdaContext - ) async throws { - let event = try self.decoder.decode(Event.self, from: request) - - let writer = LambdaCodableResponseWriter( - encoder: self.encoder, - streamWriter: responseWriter - ) - try await self.handler.handle(event, outputWriter: writer, context: context) - } -} - -/// A ``LambdaResponseStreamWriter`` wrapper that conforms to ``LambdaResponseWriter``. -public struct LambdaCodableResponseWriter: - LambdaResponseWriter -where Output == Encoder.Output { - @usableFromInline let underlyingStreamWriter: Base - @usableFromInline let encoder: Encoder - - /// Initializes an instance given an encoder and an underlying ``LambdaResponseStreamWriter``. - /// - Parameters: - /// - encoder: The encoder object that will be used to encode the generic `Output` into a `ByteBuffer`, which will then be passed to `streamWriter`. - /// - streamWriter: The underlying ``LambdaResponseStreamWriter`` that will be wrapped. - @inlinable - public init(encoder: Encoder, streamWriter: Base) { - self.encoder = encoder - self.underlyingStreamWriter = streamWriter - } - - @inlinable - public func write(_ output: Output) async throws { - var outputBuffer = ByteBuffer() - try self.encoder.encode(output, into: &outputBuffer) - try await self.underlyingStreamWriter.writeAndFinish(outputBuffer) - } -} diff --git a/Tests/AWSLambdaRuntimeCoreTests/ControlPlaneRequestEncoderTests.swift b/Tests/AWSLambdaRuntimeCoreTests/ControlPlaneRequestEncoderTests.swift index df1a4044..5a51f8b4 100644 --- a/Tests/AWSLambdaRuntimeCoreTests/ControlPlaneRequestEncoderTests.swift +++ b/Tests/AWSLambdaRuntimeCoreTests/ControlPlaneRequestEncoderTests.swift @@ -17,7 +17,7 @@ import NIOEmbedded import NIOHTTP1 import XCTest -@testable import AWSLambdaRuntimeCore +@testable import AWSLambdaRuntime final class ControlPlaneRequestEncoderTests: XCTestCase { let host = "192.168.0.1" diff --git a/Tests/AWSLambdaRuntimeCoreTests/InvocationTests.swift b/Tests/AWSLambdaRuntimeCoreTests/InvocationTests.swift index fca58391..ea4eef1f 100644 --- a/Tests/AWSLambdaRuntimeCoreTests/InvocationTests.swift +++ b/Tests/AWSLambdaRuntimeCoreTests/InvocationTests.swift @@ -15,7 +15,7 @@ import NIOHTTP1 import Testing -@testable import AWSLambdaRuntimeCore +@testable import AWSLambdaRuntime #if canImport(FoundationEssentials) import FoundationEssentials diff --git a/Tests/AWSLambdaRuntimeCoreTests/LambdaMockClient.swift b/Tests/AWSLambdaRuntimeCoreTests/LambdaMockClient.swift index 613276ed..727658fd 100644 --- a/Tests/AWSLambdaRuntimeCoreTests/LambdaMockClient.swift +++ b/Tests/AWSLambdaRuntimeCoreTests/LambdaMockClient.swift @@ -12,7 +12,7 @@ // //===----------------------------------------------------------------------===// -import AWSLambdaRuntimeCore +import AWSLambdaRuntime import Logging import NIOCore diff --git a/Tests/AWSLambdaRuntimeCoreTests/LambdaRequestIDTests.swift b/Tests/AWSLambdaRuntimeCoreTests/LambdaRequestIDTests.swift index 45886c64..a144e20c 100644 --- a/Tests/AWSLambdaRuntimeCoreTests/LambdaRequestIDTests.swift +++ b/Tests/AWSLambdaRuntimeCoreTests/LambdaRequestIDTests.swift @@ -15,7 +15,7 @@ import NIOCore import Testing -@testable import AWSLambdaRuntimeCore +@testable import AWSLambdaRuntime #if canImport(FoundationEssentials) import FoundationEssentials diff --git a/Tests/AWSLambdaRuntimeCoreTests/LambdaRunLoopTests.swift b/Tests/AWSLambdaRuntimeCoreTests/LambdaRunLoopTests.swift index f57f051b..493985d8 100644 --- a/Tests/AWSLambdaRuntimeCoreTests/LambdaRunLoopTests.swift +++ b/Tests/AWSLambdaRuntimeCoreTests/LambdaRunLoopTests.swift @@ -16,7 +16,7 @@ import Logging import NIOCore import Testing -@testable import AWSLambdaRuntimeCore +@testable import AWSLambdaRuntime #if canImport(FoundationEssentials) import FoundationEssentials diff --git a/Tests/AWSLambdaRuntimeCoreTests/LambdaRuntimeClientTests.swift b/Tests/AWSLambdaRuntimeCoreTests/LambdaRuntimeClientTests.swift index 5d430a0f..53afbaf6 100644 --- a/Tests/AWSLambdaRuntimeCoreTests/LambdaRuntimeClientTests.swift +++ b/Tests/AWSLambdaRuntimeCoreTests/LambdaRuntimeClientTests.swift @@ -19,7 +19,7 @@ import Testing import struct Foundation.UUID -@testable import AWSLambdaRuntimeCore +@testable import AWSLambdaRuntime @Suite struct LambdaRuntimeClientTests { diff --git a/Tests/AWSLambdaRuntimeCoreTests/MockLambdaServer.swift b/Tests/AWSLambdaRuntimeCoreTests/MockLambdaServer.swift index dc325742..11f43ba4 100644 --- a/Tests/AWSLambdaRuntimeCoreTests/MockLambdaServer.swift +++ b/Tests/AWSLambdaRuntimeCoreTests/MockLambdaServer.swift @@ -17,7 +17,7 @@ import NIOCore import NIOHTTP1 import NIOPosix -@testable import AWSLambdaRuntimeCore +@testable import AWSLambdaRuntime #if canImport(FoundationEssentials) import FoundationEssentials diff --git a/Tests/AWSLambdaRuntimeCoreTests/UtilsTest.swift b/Tests/AWSLambdaRuntimeCoreTests/UtilsTest.swift index bdc0b0e5..0b3b5917 100644 --- a/Tests/AWSLambdaRuntimeCoreTests/UtilsTest.swift +++ b/Tests/AWSLambdaRuntimeCoreTests/UtilsTest.swift @@ -14,7 +14,7 @@ import XCTest -@testable import AWSLambdaRuntimeCore +@testable import AWSLambdaRuntime class UtilsTest: XCTestCase { func testGenerateXRayTraceID() { diff --git a/readme.md b/readme.md index f945b1e1..37596ed2 100644 --- a/readme.md +++ b/readme.md @@ -39,7 +39,7 @@ Swift AWS Lambda Runtime was designed to make building Lambda functions in Swift To get started, read [the Swift AWS Lambda runtime tutorial](https://swiftpackageindex.com/swift-server/swift-aws-lambda-runtime/main/tutorials/table-of-content). It provides developers with detailed step-by-step instructions to develop, build, and deploy a Lambda function. -We also wrote a comprehensive [deployment guide](https://swiftpackageindex.com/swift-server/swift-aws-lambda-runtime/main/documentation/awslambdaruntimecore/deployment). +We also wrote a comprehensive [deployment guide](https://swiftpackageindex.com/swift-server/swift-aws-lambda-runtime/main/documentation/awslambdaruntime/deployment). Or, if you're impatient to start with runtime v2, try these six steps: @@ -379,7 +379,7 @@ LOCAL_LAMBDA_SERVER_INVOCATION_ENDPOINT=/2015-03-31/functions/function/invocatio ## Deploying your Swift Lambda functions -There is a full deployment guide available in [the documentation](https://swiftpackageindex.com/swift-server/swift-aws-lambda-runtime/main/documentation/awslambdaruntimecore/deployment). +There is a full deployment guide available in [the documentation](https://swiftpackageindex.com/swift-server/swift-aws-lambda-runtime/main/documentation/awslambdaruntime/deployment). There are multiple ways to deploy your Swift code to AWS Lambda. The very first time, you'll probably use the AWS Console to create a new Lambda function and upload your code as a zip file. However, as you iterate on your code, you'll want to automate the deployment process. @@ -421,9 +421,9 @@ Please refer to the full deployment guide available in [the documentation](https ## Swift AWS Lambda Runtime - Design Principles -The [design document](Sources/AWSLambdaRuntimeCore/Documentation.docc/Proposals/0001-v2-api.md) details the v2 API proposal for the swift-aws-lambda-runtime library, which aims to enhance the developer experience for building serverless functions in Swift. +The [design document](Sources/AWSLambdaRuntime/Documentation.docc/Proposals/0001-v2-api.md) details the v2 API proposal for the swift-aws-lambda-runtime library, which aims to enhance the developer experience for building serverless functions in Swift. -The proposal has been reviewed and [incorporated feedback from the community](https://forums.swift.org/t/aws-lambda-v2-api-proposal/73819). The full v2 API design document is available [in this repository](Sources/AWSLambdaRuntimeCore/Documentation.docc/Proposals/0001-v2-api.md). +The proposal has been reviewed and [incorporated feedback from the community](https://forums.swift.org/t/aws-lambda-v2-api-proposal/73819). The full v2 API design document is available [in this repository](Sources/AWSLambdaRuntime/Documentation.docc/Proposals/0001-v2-api.md). ### Key Design Principles From bf02bcec9a22c060234ee9e417d36519eda6a572 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A9bastien=20Stormacq?= Date: Thu, 6 Mar 2025 16:52:42 +0100 Subject: [PATCH 20/24] declare user agent string only once (#493) The user-agent string was repeated at 5+ different places in the source code. I defined it as a constant in the `ControlPlaneRequest` struct where other similar constant have been defined and make sure the rest of the code only uses that constant. This should address https://github.com/swift-server/swift-aws-lambda-runtime/issues/492 --- .../AWSLambdaRuntime/ControlPlaneRequestEncoder.swift | 3 ++- Sources/AWSLambdaRuntime/LambdaRuntimeClient.swift | 8 ++++---- .../ControlPlaneRequestEncoderTests.swift | 10 +++++----- 3 files changed, 11 insertions(+), 10 deletions(-) diff --git a/Sources/AWSLambdaRuntime/ControlPlaneRequestEncoder.swift b/Sources/AWSLambdaRuntime/ControlPlaneRequestEncoder.swift index 31e64d27..5848ec76 100644 --- a/Sources/AWSLambdaRuntime/ControlPlaneRequestEncoder.swift +++ b/Sources/AWSLambdaRuntime/ControlPlaneRequestEncoder.swift @@ -93,7 +93,8 @@ struct ControlPlaneRequestEncoder: _EmittingChannelHandler { extension String { static let CRLF: String = "\r\n" - static let userAgentHeader: String = "user-agent: Swift-Lambda/Unknown\r\n" + static let userAgent = "Swift-Lambda/Unknown" + static let userAgentHeader: String = "user-agent: \(userAgent)\r\n" static let unhandledErrorHeader: String = "lambda-runtime-function-error-type: Unhandled\r\n" static let nextInvocationRequestLine: String = diff --git a/Sources/AWSLambdaRuntime/LambdaRuntimeClient.swift b/Sources/AWSLambdaRuntime/LambdaRuntimeClient.swift index 809a6a0e..196cbeb1 100644 --- a/Sources/AWSLambdaRuntime/LambdaRuntimeClient.swift +++ b/Sources/AWSLambdaRuntime/LambdaRuntimeClient.swift @@ -458,16 +458,16 @@ private final class LambdaChannelHandler self.configuration = configuration self.defaultHeaders = [ "host": "\(self.configuration.ip):\(self.configuration.port)", - "user-agent": "Swift-Lambda/Unknown", + "user-agent": .userAgent, ] self.errorHeaders = [ "host": "\(self.configuration.ip):\(self.configuration.port)", - "user-agent": "Swift-Lambda/Unknown", + "user-agent": .userAgent, "lambda-runtime-function-error-type": "Unhandled", ] self.streamingHeaders = [ "host": "\(self.configuration.ip):\(self.configuration.port)", - "user-agent": "Swift-Lambda/Unknown", + "user-agent": .userAgent, "transfer-encoding": "chunked", ] } @@ -638,7 +638,7 @@ private final class LambdaChannelHandler if byteBuffer?.readableBytes ?? 0 < 6_000_000 { [ "host": "\(self.configuration.ip):\(self.configuration.port)", - "user-agent": "Swift-Lambda/Unknown", + "user-agent": .userAgent, "content-length": "\(byteBuffer?.readableBytes ?? 0)", ] } else { diff --git a/Tests/AWSLambdaRuntimeCoreTests/ControlPlaneRequestEncoderTests.swift b/Tests/AWSLambdaRuntimeCoreTests/ControlPlaneRequestEncoderTests.swift index 5a51f8b4..840a17c6 100644 --- a/Tests/AWSLambdaRuntimeCoreTests/ControlPlaneRequestEncoderTests.swift +++ b/Tests/AWSLambdaRuntimeCoreTests/ControlPlaneRequestEncoderTests.swift @@ -49,7 +49,7 @@ final class ControlPlaneRequestEncoderTests: XCTestCase { XCTAssertEqual(request?.head.uri, "/2018-06-01/runtime/invocation/next") XCTAssertEqual(request?.head.version, .http1_1) XCTAssertEqual(request?.head.headers["host"], [self.host]) - XCTAssertEqual(request?.head.headers["user-agent"], ["Swift-Lambda/Unknown"]) + XCTAssertEqual(request?.head.headers["user-agent"], [.userAgent]) XCTAssertNil(try self.server.readInbound(as: NIOHTTPServerRequestFull.self)) } @@ -64,7 +64,7 @@ final class ControlPlaneRequestEncoderTests: XCTestCase { XCTAssertEqual(request?.head.uri, "/2018-06-01/runtime/invocation/\(requestID)/response") XCTAssertEqual(request?.head.version, .http1_1) XCTAssertEqual(request?.head.headers["host"], [self.host]) - XCTAssertEqual(request?.head.headers["user-agent"], ["Swift-Lambda/Unknown"]) + XCTAssertEqual(request?.head.headers["user-agent"], [.userAgent]) XCTAssertEqual(request?.head.headers["content-length"], ["0"]) XCTAssertNil(try self.server.readInbound(as: NIOHTTPServerRequestFull.self)) @@ -82,7 +82,7 @@ final class ControlPlaneRequestEncoderTests: XCTestCase { XCTAssertEqual(request?.head.uri, "/2018-06-01/runtime/invocation/\(requestID)/response") XCTAssertEqual(request?.head.version, .http1_1) XCTAssertEqual(request?.head.headers["host"], [self.host]) - XCTAssertEqual(request?.head.headers["user-agent"], ["Swift-Lambda/Unknown"]) + XCTAssertEqual(request?.head.headers["user-agent"], [.userAgent]) XCTAssertEqual(request?.head.headers["content-length"], ["\(payload.readableBytes)"]) XCTAssertEqual(request?.body, payload) @@ -100,7 +100,7 @@ final class ControlPlaneRequestEncoderTests: XCTestCase { XCTAssertEqual(request?.head.uri, "/2018-06-01/runtime/invocation/\(requestID)/error") XCTAssertEqual(request?.head.version, .http1_1) XCTAssertEqual(request?.head.headers["host"], [self.host]) - XCTAssertEqual(request?.head.headers["user-agent"], ["Swift-Lambda/Unknown"]) + XCTAssertEqual(request?.head.headers["user-agent"], [.userAgent]) XCTAssertEqual(request?.head.headers["lambda-runtime-function-error-type"], ["Unhandled"]) let expectedBody = #"{"errorType":"SomeError","errorMessage":"An error happened"}"# @@ -123,7 +123,7 @@ final class ControlPlaneRequestEncoderTests: XCTestCase { XCTAssertEqual(request?.head.uri, "/2018-06-01/runtime/init/error") XCTAssertEqual(request?.head.version, .http1_1) XCTAssertEqual(request?.head.headers["host"], [self.host]) - XCTAssertEqual(request?.head.headers["user-agent"], ["Swift-Lambda/Unknown"]) + XCTAssertEqual(request?.head.headers["user-agent"], [.userAgent]) XCTAssertEqual(request?.head.headers["lambda-runtime-function-error-type"], ["Unhandled"]) let expectedBody = #"{"errorType":"StartupError","errorMessage":"Urgh! Startup failed. 😨"}"# XCTAssertEqual(request?.head.headers["content-length"], ["\(expectedBody.utf8.count)"]) From bba711bd1899e7f5f7250012b955dd340e6ed442 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A9bastien=20Stormacq?= Date: Thu, 6 Mar 2025 18:50:32 +0100 Subject: [PATCH 21/24] remove mentions of LambdaRuntImeCore (#495) Cleanup code from mentions of `LambdaRuntimeCore` --- Examples/HelloWorld/README.md | 2 +- Examples/Testing/README.md | 4 ++-- Examples/Testing/Tests/HandlerTests.swift | 2 +- Package.swift | 8 +------- Package@swift-6.0.swift | 8 +------- .../AWSLambdaRuntime/Docs.docc/Proposals/0001-v2-api.md | 2 +- .../ControlPlaneRequestEncoderTests.swift | 0 .../InvocationTests.swift | 0 .../LambdaMockClient.swift | 0 .../LambdaRequestIDTests.swift | 0 .../LambdaRunLoopTests.swift | 0 .../LambdaRuntimeClientTests.swift | 0 .../MockLambdaServer.swift | 0 .../Utils.swift | 0 .../UtilsTest.swift | 0 15 files changed, 7 insertions(+), 19 deletions(-) rename Tests/{AWSLambdaRuntimeCoreTests => AWSLambdaRuntimeTests}/ControlPlaneRequestEncoderTests.swift (100%) rename Tests/{AWSLambdaRuntimeCoreTests => AWSLambdaRuntimeTests}/InvocationTests.swift (100%) rename Tests/{AWSLambdaRuntimeCoreTests => AWSLambdaRuntimeTests}/LambdaMockClient.swift (100%) rename Tests/{AWSLambdaRuntimeCoreTests => AWSLambdaRuntimeTests}/LambdaRequestIDTests.swift (100%) rename Tests/{AWSLambdaRuntimeCoreTests => AWSLambdaRuntimeTests}/LambdaRunLoopTests.swift (100%) rename Tests/{AWSLambdaRuntimeCoreTests => AWSLambdaRuntimeTests}/LambdaRuntimeClientTests.swift (100%) rename Tests/{AWSLambdaRuntimeCoreTests => AWSLambdaRuntimeTests}/MockLambdaServer.swift (100%) rename Tests/{AWSLambdaRuntimeCoreTests => AWSLambdaRuntimeTests}/Utils.swift (100%) rename Tests/{AWSLambdaRuntimeCoreTests => AWSLambdaRuntimeTests}/UtilsTest.swift (100%) diff --git a/Examples/HelloWorld/README.md b/Examples/HelloWorld/README.md index 245233c3..c4eacfc2 100644 --- a/Examples/HelloWorld/README.md +++ b/Examples/HelloWorld/README.md @@ -28,7 +28,7 @@ It will compile your code and start the local server. You know the local server Building for debugging... [1/1] Write swift-version--644A47CB88185983.txt Build of product 'MyLambda' complete! (0.31s) -2025-01-29T12:44:48+0100 info LocalServer : host="127.0.0.1" port=7000 [AWSLambdaRuntimeCore] Server started and listening +2025-01-29T12:44:48+0100 info LocalServer : host="127.0.0.1" port=7000 [AWSLambdaRuntime] Server started and listening ``` Then, from another Terminal, send your payload with `curl`. Note that the payload must be a valid JSON string. In the case of this function that accepts a simple String, it means the String must be wrapped in between double quotes. diff --git a/Examples/Testing/README.md b/Examples/Testing/README.md index 3f911b21..9bfd0e28 100644 --- a/Examples/Testing/README.md +++ b/Examples/Testing/README.md @@ -160,8 +160,8 @@ You must pass an event to the Lambda function. You can use the `event.json` file sam local invoke -e Tests/event.json START RequestId: 3270171f-46d3-45f9-9bb6-3c2e5e9dc625 Version: $LATEST -2024-12-21T16:49:31+0000 debug LambdaRuntime : [AWSLambdaRuntimeCore] LambdaRuntime initialized -2024-12-21T16:49:31+0000 trace LambdaRuntime : lambda_ip=127.0.0.1 lambda_port=9001 [AWSLambdaRuntimeCore] Connection to control plane created +2024-12-21T16:49:31+0000 debug LambdaRuntime : [AWSLambdaRuntime] LambdaRuntime initialized +2024-12-21T16:49:31+0000 trace LambdaRuntime : lambda_ip=127.0.0.1 lambda_port=9001 [AWSLambdaRuntime] Connection to control plane created 2024-12-21T16:49:31+0000 debug LambdaRuntime : [APIGatewayLambda] HTTP API Message received 2024-12-21T16:49:31+0000 trace LambdaRuntime : [APIGatewayLambda] Event: APIGatewayV2Request(version: "2.0", routeKey: "$default", rawPath: "/", rawQueryString: "", cookies: [], headers: ["x-forwarded-proto": "https", "host": "a5q74es3k2.execute-api.us-east-1.amazonaws.com", "content-length": "0", "x-forwarded-for": "81.0.0.43", "accept": "*/*", "x-amzn-trace-id": "Root=1-66fb03de-07533930192eaf5f540db0cb", "x-forwarded-port": "443", "user-agent": "curl/8.7.1"], queryStringParameters: [:], pathParameters: [:], context: AWSLambdaEvents.APIGatewayV2Request.Context(accountId: "012345678901", apiId: "a5q74es3k2", domainName: "a5q74es3k2.execute-api.us-east-1.amazonaws.com", domainPrefix: "a5q74es3k2", stage: "$default", requestId: "e72KxgsRoAMEMSA=", http: AWSLambdaEvents.APIGatewayV2Request.Context.HTTP(method: GET, path: "/", protocol: "HTTP/1.1", sourceIp: "81.0.0.43", userAgent: "curl/8.7.1"), authorizer: nil, authentication: nil, time: "30/Sep/2024:20:02:38 +0000", timeEpoch: 1727726558220), stageVariables: [:], body: Optional("aGVsbG8gd29ybGQgb2YgU1dJRlQgTEFNQkRBIQ=="), isBase64Encoded: false) END RequestId: 5b71587a-39da-445e-855d-27a700e57efd diff --git a/Examples/Testing/Tests/HandlerTests.swift b/Examples/Testing/Tests/HandlerTests.swift index 7fa245f9..85cc4e4e 100644 --- a/Examples/Testing/Tests/HandlerTests.swift +++ b/Examples/Testing/Tests/HandlerTests.swift @@ -18,7 +18,7 @@ import Logging import Testing @testable import APIGatewayLambda // to access the business code -@testable import AWSLambdaRuntimeCore // to access the LambdaContext +@testable import AWSLambdaRuntime // to access the LambdaContext #if canImport(FoundationEssentials) import FoundationEssentials diff --git a/Package.swift b/Package.swift index 3bd026f9..1aef921d 100644 --- a/Package.swift +++ b/Package.swift @@ -64,19 +64,13 @@ let package = Package( ) ), .testTarget( - name: "AWSLambdaRuntimeCoreTests", + name: "AWSLambdaRuntimeTests", dependencies: [ .byName(name: "AWSLambdaRuntime"), .product(name: "NIOTestUtils", package: "swift-nio"), .product(name: "NIOFoundationCompat", package: "swift-nio"), ] ), - .testTarget( - name: "AWSLambdaRuntimeTests", - dependencies: [ - .byName(name: "AWSLambdaRuntime") - ] - ), // testing helper .target( name: "AWSLambdaTesting", diff --git a/Package@swift-6.0.swift b/Package@swift-6.0.swift index ff45ec88..29929f40 100644 --- a/Package@swift-6.0.swift +++ b/Package@swift-6.0.swift @@ -53,19 +53,13 @@ let package = Package( ) ), .testTarget( - name: "AWSLambdaRuntimeCoreTests", + name: "AWSLambdaRuntimeTests", dependencies: [ .byName(name: "AWSLambdaRuntime"), .product(name: "NIOTestUtils", package: "swift-nio"), .product(name: "NIOFoundationCompat", package: "swift-nio"), ] ), - .testTarget( - name: "AWSLambdaRuntimeTests", - dependencies: [ - .byName(name: "AWSLambdaRuntime") - ] - ), // testing helper .target( name: "AWSLambdaTesting", diff --git a/Sources/AWSLambdaRuntime/Docs.docc/Proposals/0001-v2-api.md b/Sources/AWSLambdaRuntime/Docs.docc/Proposals/0001-v2-api.md index 624182d0..0396d48d 100644 --- a/Sources/AWSLambdaRuntime/Docs.docc/Proposals/0001-v2-api.md +++ b/Sources/AWSLambdaRuntime/Docs.docc/Proposals/0001-v2-api.md @@ -80,7 +80,7 @@ initialized services: struct MyLambda: LambdaHandler { let pgClient: PostgresClient - init(context: AWSLambdaRuntimeCore.LambdaInitializationContext) async throws { + init(context: AWSLambdaRuntime.LambdaInitializationContext) async throws { /// Instantiate service let client = PostgresClient(configuration: ...) diff --git a/Tests/AWSLambdaRuntimeCoreTests/ControlPlaneRequestEncoderTests.swift b/Tests/AWSLambdaRuntimeTests/ControlPlaneRequestEncoderTests.swift similarity index 100% rename from Tests/AWSLambdaRuntimeCoreTests/ControlPlaneRequestEncoderTests.swift rename to Tests/AWSLambdaRuntimeTests/ControlPlaneRequestEncoderTests.swift diff --git a/Tests/AWSLambdaRuntimeCoreTests/InvocationTests.swift b/Tests/AWSLambdaRuntimeTests/InvocationTests.swift similarity index 100% rename from Tests/AWSLambdaRuntimeCoreTests/InvocationTests.swift rename to Tests/AWSLambdaRuntimeTests/InvocationTests.swift diff --git a/Tests/AWSLambdaRuntimeCoreTests/LambdaMockClient.swift b/Tests/AWSLambdaRuntimeTests/LambdaMockClient.swift similarity index 100% rename from Tests/AWSLambdaRuntimeCoreTests/LambdaMockClient.swift rename to Tests/AWSLambdaRuntimeTests/LambdaMockClient.swift diff --git a/Tests/AWSLambdaRuntimeCoreTests/LambdaRequestIDTests.swift b/Tests/AWSLambdaRuntimeTests/LambdaRequestIDTests.swift similarity index 100% rename from Tests/AWSLambdaRuntimeCoreTests/LambdaRequestIDTests.swift rename to Tests/AWSLambdaRuntimeTests/LambdaRequestIDTests.swift diff --git a/Tests/AWSLambdaRuntimeCoreTests/LambdaRunLoopTests.swift b/Tests/AWSLambdaRuntimeTests/LambdaRunLoopTests.swift similarity index 100% rename from Tests/AWSLambdaRuntimeCoreTests/LambdaRunLoopTests.swift rename to Tests/AWSLambdaRuntimeTests/LambdaRunLoopTests.swift diff --git a/Tests/AWSLambdaRuntimeCoreTests/LambdaRuntimeClientTests.swift b/Tests/AWSLambdaRuntimeTests/LambdaRuntimeClientTests.swift similarity index 100% rename from Tests/AWSLambdaRuntimeCoreTests/LambdaRuntimeClientTests.swift rename to Tests/AWSLambdaRuntimeTests/LambdaRuntimeClientTests.swift diff --git a/Tests/AWSLambdaRuntimeCoreTests/MockLambdaServer.swift b/Tests/AWSLambdaRuntimeTests/MockLambdaServer.swift similarity index 100% rename from Tests/AWSLambdaRuntimeCoreTests/MockLambdaServer.swift rename to Tests/AWSLambdaRuntimeTests/MockLambdaServer.swift diff --git a/Tests/AWSLambdaRuntimeCoreTests/Utils.swift b/Tests/AWSLambdaRuntimeTests/Utils.swift similarity index 100% rename from Tests/AWSLambdaRuntimeCoreTests/Utils.swift rename to Tests/AWSLambdaRuntimeTests/Utils.swift diff --git a/Tests/AWSLambdaRuntimeCoreTests/UtilsTest.swift b/Tests/AWSLambdaRuntimeTests/UtilsTest.swift similarity index 100% rename from Tests/AWSLambdaRuntimeCoreTests/UtilsTest.swift rename to Tests/AWSLambdaRuntimeTests/UtilsTest.swift From 8f10a6bbdd682e3020d9279f597f4cd3b05c18cd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A9bastien=20Stormacq?= Date: Thu, 6 Mar 2025 18:58:50 +0100 Subject: [PATCH 22/24] Fix CI error on Amazon S3 examples (#496) When we include Traits, we changed the CI container from Swift 6.0 Amazon Linux to Swift 6.1 Nightly on Ubuntu. This broke the CI because AWS SDK used in the `Examples/S3_AWSSDK` example depends on `libssl-devel` which is not installed by default on Jammy images. This PR reverts back the container used to test all examples to an Amazon Linux 2, but this time the 6.1-nightly version to allow to compile the package traits. This should address https://github.com/swift-server/swift-aws-lambda-runtime/issues/494 --- .github/workflows/integration_tests.yml | 6 ++---- .github/workflows/scripts/integration_tests.sh | 1 + 2 files changed, 3 insertions(+), 4 deletions(-) diff --git a/.github/workflows/integration_tests.yml b/.github/workflows/integration_tests.yml index af02f0d4..422ef82e 100644 --- a/.github/workflows/integration_tests.yml +++ b/.github/workflows/integration_tests.yml @@ -38,8 +38,8 @@ on: required: true matrix_linux_swift_container_image: type: string - description: "Container image for the matrix job. Defaults to matching latest Swift Ubuntu image." - default: "swiftlang/swift:nightly-6.1-jammy" + description: "Container image for the matrix job. Defaults to matching latest Swift 6.1 Amazon Linux 2 image." + default: "swiftlang/swift:nightly-6.1-amazonlinux2" ## We are cancelling previously triggered workflow runs concurrency: @@ -59,7 +59,6 @@ jobs: # We are using only one Swift version swift: - image: ${{ inputs.matrix_linux_swift_container_image }} - swift_version: "6.0.3-amazonlinux2" container: image: ${{ matrix.swift.image }} steps: @@ -93,7 +92,6 @@ jobs: - name: Run matrix job working-directory: ${{ github.event.repository.name }} # until we can use action/checkout@v4 env: - SWIFT_VERSION: ${{ matrix.swift.swift_version }} COMMAND: ${{ inputs.matrix_linux_command }} EXAMPLE: ${{ matrix.examples }} run: | diff --git a/.github/workflows/scripts/integration_tests.sh b/.github/workflows/scripts/integration_tests.sh index cb0e031b..8d11b313 100755 --- a/.github/workflows/scripts/integration_tests.sh +++ b/.github/workflows/scripts/integration_tests.sh @@ -19,6 +19,7 @@ log() { printf -- "** %s\n" "$*" >&2; } error() { printf -- "** ERROR: %s\n" "$*" >&2; } fatal() { error "$@"; exit 1; } +SWIFT_VERSION=$(swift --version) test -n "${SWIFT_VERSION:-}" || fatal "SWIFT_VERSION unset" test -n "${COMMAND:-}" || fatal "COMMAND unset" test -n "${EXAMPLE:-}" || fatal "EXAMPLE unset" From ae06d5953999c85477ee7c9fe5562cf982baeb3f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A9bastien=20Stormacq?= Date: Fri, 7 Mar 2025 06:48:00 +0100 Subject: [PATCH 23/24] cleanup unused code (#498) --- Package.swift | 17 ----------------- Package@swift-6.0.swift | 17 ----------------- .../AWSLambdaTesting/LambdaTestRuntime.swift | 17 ----------------- .../LambdaTestRuntimeTests.swift | 18 ------------------ 4 files changed, 69 deletions(-) delete mode 100644 Sources/AWSLambdaTesting/LambdaTestRuntime.swift delete mode 100644 Tests/AWSLambdaTestingTests/LambdaTestRuntimeTests.swift diff --git a/Package.swift b/Package.swift index 1aef921d..a94ba22b 100644 --- a/Package.swift +++ b/Package.swift @@ -10,8 +10,6 @@ let package = Package( // plugin to package the lambda, creating an archive that can be uploaded to AWS // requires Linux or at least macOS v15 .plugin(name: "AWSLambdaPackager", targets: ["AWSLambdaPackager"]), - // for testing only - .library(name: "AWSLambdaTesting", targets: ["AWSLambdaTesting"]), ], traits: [ "FoundationJSONSupport", @@ -71,21 +69,6 @@ let package = Package( .product(name: "NIOFoundationCompat", package: "swift-nio"), ] ), - // testing helper - .target( - name: "AWSLambdaTesting", - dependencies: [ - .byName(name: "AWSLambdaRuntime"), - .product(name: "NIOCore", package: "swift-nio"), - .product(name: "NIOPosix", package: "swift-nio"), - ] - ), - .testTarget( - name: "AWSLambdaTestingTests", - dependencies: [ - .byName(name: "AWSLambdaTesting") - ] - ), // for perf testing .executableTarget( name: "MockServer", diff --git a/Package@swift-6.0.swift b/Package@swift-6.0.swift index 29929f40..fb4e82d0 100644 --- a/Package@swift-6.0.swift +++ b/Package@swift-6.0.swift @@ -10,8 +10,6 @@ let package = Package( // plugin to package the lambda, creating an archive that can be uploaded to AWS // requires Linux or at least macOS v15 .plugin(name: "AWSLambdaPackager", targets: ["AWSLambdaPackager"]), - // for testing only - .library(name: "AWSLambdaTesting", targets: ["AWSLambdaTesting"]), ], dependencies: [ .package(url: "https://github.com/apple/swift-nio.git", from: "2.81.0"), @@ -60,21 +58,6 @@ let package = Package( .product(name: "NIOFoundationCompat", package: "swift-nio"), ] ), - // testing helper - .target( - name: "AWSLambdaTesting", - dependencies: [ - .byName(name: "AWSLambdaRuntime"), - .product(name: "NIOCore", package: "swift-nio"), - .product(name: "NIOPosix", package: "swift-nio"), - ] - ), - .testTarget( - name: "AWSLambdaTestingTests", - dependencies: [ - .byName(name: "AWSLambdaTesting") - ] - ), // for perf testing .executableTarget( name: "MockServer", diff --git a/Sources/AWSLambdaTesting/LambdaTestRuntime.swift b/Sources/AWSLambdaTesting/LambdaTestRuntime.swift deleted file mode 100644 index 6fba9656..00000000 --- a/Sources/AWSLambdaTesting/LambdaTestRuntime.swift +++ /dev/null @@ -1,17 +0,0 @@ -//===----------------------------------------------------------------------===// -// -// This source file is part of the SwiftAWSLambdaRuntime open source project -// -// Copyright (c) 2020 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 Logging -import NIOCore -import NIOPosix diff --git a/Tests/AWSLambdaTestingTests/LambdaTestRuntimeTests.swift b/Tests/AWSLambdaTestingTests/LambdaTestRuntimeTests.swift deleted file mode 100644 index 47d29dfe..00000000 --- a/Tests/AWSLambdaTestingTests/LambdaTestRuntimeTests.swift +++ /dev/null @@ -1,18 +0,0 @@ -//===----------------------------------------------------------------------===// -// -// This source file is part of the SwiftAWSLambdaRuntime open source project -// -// Copyright (c) 2020 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 AWSLambdaTesting -import NIOCore -import Testing From 3ce0a873e3889ad4cca160ff13831805b1bc8580 Mon Sep 17 00:00:00 2001 From: Fabian Fett Date: Fri, 7 Mar 2025 07:34:05 +0100 Subject: [PATCH 24/24] Attach requestID to logger (#497) --- Sources/AWSLambdaRuntime/Lambda.swift | 2 + .../CollectEverythingLogHandler.swift | 138 ++++++++++++++++++ .../LambdaMockClient.swift | 4 +- .../LambdaRunLoopTests.swift | 22 ++- 4 files changed, 160 insertions(+), 6 deletions(-) create mode 100644 Tests/AWSLambdaRuntimeTests/CollectEverythingLogHandler.swift diff --git a/Sources/AWSLambdaRuntime/Lambda.swift b/Sources/AWSLambdaRuntime/Lambda.swift index 4634fca0..24f10343 100644 --- a/Sources/AWSLambdaRuntime/Lambda.swift +++ b/Sources/AWSLambdaRuntime/Lambda.swift @@ -37,9 +37,11 @@ public enum Lambda { ) async throws where Handler: StreamingLambdaHandler { var handler = handler + var logger = logger do { while !Task.isCancelled { let (invocation, writer) = try await runtimeClient.nextInvocation() + logger[metadataKey: "aws-request-id"] = "\(invocation.metadata.requestID)" do { try await handler.handle( diff --git a/Tests/AWSLambdaRuntimeTests/CollectEverythingLogHandler.swift b/Tests/AWSLambdaRuntimeTests/CollectEverythingLogHandler.swift new file mode 100644 index 00000000..537847d8 --- /dev/null +++ b/Tests/AWSLambdaRuntimeTests/CollectEverythingLogHandler.swift @@ -0,0 +1,138 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the SwiftAWSLambdaRuntime open source project +// +// Copyright (c) 2025 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 Logging +import Synchronization +import Testing + +struct CollectEverythingLogHandler: LogHandler { + var metadata: Logger.Metadata = [:] + var logLevel: Logger.Level = .info + let logStore: LogStore + + final class LogStore: Sendable { + struct Entry: Sendable { + var level: Logger.Level + var message: String + var metadata: [String: String] + } + + let logs: Mutex<[Entry]> = .init([]) + + func append(level: Logger.Level, message: Logger.Message, metadata: Logger.Metadata?) { + self.logs.withLock { entries in + entries.append( + Entry( + level: level, + message: message.description, + metadata: metadata?.mapValues { $0.description } ?? [:] + ) + ) + } + } + + func clear() { + self.logs.withLock { + $0.removeAll() + } + } + + enum LogFieldExpectedValue: ExpressibleByStringLiteral, ExpressibleByStringInterpolation { + case exactMatch(String) + case beginsWith(String) + case wildcard + case predicate((String) -> Bool) + + init(stringLiteral value: String) { + self = .exactMatch(value) + } + } + + @discardableResult + func assertContainsLog( + _ message: String, + _ metadata: (String, LogFieldExpectedValue)..., + sourceLocation: SourceLocation = #_sourceLocation + ) -> [Entry] { + var candidates = self.getAllLogsWithMessage(message) + if candidates.isEmpty { + Issue.record("Logs do not contain entry with message: \(message)", sourceLocation: sourceLocation) + return [] + } + for (key, value) in metadata { + var errorMsg: String + switch value { + case .wildcard: + candidates = candidates.filter { $0.metadata.contains { $0.key == key } } + errorMsg = "Logs do not contain entry with message: \(message) and metadata: \(key) *" + case .predicate(let predicate): + candidates = candidates.filter { $0.metadata[key].map(predicate) ?? false } + errorMsg = + "Logs do not contain entry with message: \(message) and metadata: \(key) matching predicate" + case .beginsWith(let prefix): + candidates = candidates.filter { $0.metadata[key]?.hasPrefix(prefix) ?? false } + errorMsg = "Logs do not contain entry with message: \(message) and metadata: \(key), \(value)" + case .exactMatch(let value): + candidates = candidates.filter { $0.metadata[key] == value } + errorMsg = "Logs do not contain entry with message: \(message) and metadata: \(key), \(value)" + } + if candidates.isEmpty { + Issue.record("Error: \(errorMsg)", sourceLocation: sourceLocation) + return [] + } + } + return candidates + } + + func assertDoesNotContainMessage(_ message: String, sourceLocation: SourceLocation = #_sourceLocation) { + let candidates = self.getAllLogsWithMessage(message) + if candidates.count > 0 { + Issue.record("Logs contain entry with message: \(message)", sourceLocation: sourceLocation) + } + } + + func getAllLogs() -> [Entry] { + self.logs.withLock { $0 } + } + + func getAllLogsWithMessage(_ message: String) -> [Entry] { + self.getAllLogs().filter { $0.message == message } + } + } + + init(logStore: LogStore) { + self.logStore = logStore + } + + func log( + level: Logger.Level, + message: Logger.Message, + metadata: Logger.Metadata?, + source: String, + file: String, + function: String, + line: UInt + ) { + self.logStore.append(level: level, message: message, metadata: self.metadata.merging(metadata ?? [:]) { $1 }) + } + + subscript(metadataKey key: String) -> Logger.Metadata.Value? { + get { + self.metadata[key] + } + set { + self.metadata[key] = newValue + } + } +} diff --git a/Tests/AWSLambdaRuntimeTests/LambdaMockClient.swift b/Tests/AWSLambdaRuntimeTests/LambdaMockClient.swift index 727658fd..7714c84a 100644 --- a/Tests/AWSLambdaRuntimeTests/LambdaMockClient.swift +++ b/Tests/AWSLambdaRuntimeTests/LambdaMockClient.swift @@ -214,12 +214,12 @@ final actor LambdaMockClient: LambdaRuntimeClientProtocol { let eventProcessedHandler: CheckedContinuation } - func invoke(event: ByteBuffer) async throws -> ByteBuffer { + func invoke(event: ByteBuffer, requestID: String = UUID().uuidString) async throws -> ByteBuffer { try await withCheckedThrowingContinuation { eventProcessedHandler in do { let metadata = try InvocationMetadata( headers: .init([ - ("Lambda-Runtime-Aws-Request-Id", "100"), // arbitrary values + ("Lambda-Runtime-Aws-Request-Id", "\(requestID)"), // arbitrary values ("Lambda-Runtime-Deadline-Ms", "100"), ("Lambda-Runtime-Invoked-Function-Arn", "100"), ]) diff --git a/Tests/AWSLambdaRuntimeTests/LambdaRunLoopTests.swift b/Tests/AWSLambdaRuntimeTests/LambdaRunLoopTests.swift index 493985d8..3253238e 100644 --- a/Tests/AWSLambdaRuntimeTests/LambdaRunLoopTests.swift +++ b/Tests/AWSLambdaRuntimeTests/LambdaRunLoopTests.swift @@ -32,6 +32,7 @@ struct LambdaRunLoopTests { responseWriter: some LambdaResponseStreamWriter, context: LambdaContext ) async throws { + context.logger.info("Test") try await responseWriter.writeAndFinish(event) } } @@ -42,6 +43,7 @@ struct LambdaRunLoopTests { responseWriter: some LambdaResponseStreamWriter, context: LambdaContext ) async throws { + context.logger.info("Test") throw LambdaError.handlerError } } @@ -54,16 +56,22 @@ struct LambdaRunLoopTests { let inputEvent = ByteBuffer(string: "Test Invocation Event") try await withThrowingTaskGroup(of: Void.self) { group in + let logStore = CollectEverythingLogHandler.LogStore() group.addTask { try await Lambda.runLoop( runtimeClient: self.mockClient, handler: self.mockEchoHandler, - logger: Logger(label: "RunLoopTest") + logger: Logger( + label: "RunLoopTest", + factory: { _ in CollectEverythingLogHandler(logStore: logStore) } + ) ) } - let response = try await self.mockClient.invoke(event: inputEvent) + let requestID = UUID().uuidString + let response = try await self.mockClient.invoke(event: inputEvent, requestID: requestID) #expect(response == inputEvent) + logStore.assertContainsLog("Test", ("aws-request-id", .exactMatch(requestID))) group.cancelAll() } @@ -73,20 +81,26 @@ struct LambdaRunLoopTests { let inputEvent = ByteBuffer(string: "Test Invocation Event") await withThrowingTaskGroup(of: Void.self) { group in + let logStore = CollectEverythingLogHandler.LogStore() group.addTask { try await Lambda.runLoop( runtimeClient: self.mockClient, handler: self.failingHandler, - logger: Logger(label: "RunLoopTest") + logger: Logger( + label: "RunLoopTest", + factory: { _ in CollectEverythingLogHandler(logStore: logStore) } + ) ) } + let requestID = UUID().uuidString await #expect( throws: LambdaError.handlerError, performing: { - try await self.mockClient.invoke(event: inputEvent) + try await self.mockClient.invoke(event: inputEvent, requestID: requestID) } ) + logStore.assertContainsLog("Test", ("aws-request-id", .exactMatch(requestID))) group.cancelAll() }