diff --git a/.github/dependabot.yml b/.github/dependabot.yml index 3722537ae..88f18ea29 100644 --- a/.github/dependabot.yml +++ b/.github/dependabot.yml @@ -1,6 +1,9 @@ version: 2 updates: - + - package-ecosystem: "maven" + directory: "/aws-lambda-java-runtime-interface" + schedule: + interval: "weekly" - package-ecosystem: "github-actions" directory: "/" schedule: diff --git a/.github/workflows/aws-lambda-java-core.yml b/.github/workflows/aws-lambda-java-core.yml index 267d901c9..3e4364672 100644 --- a/.github/workflows/aws-lambda-java-core.yml +++ b/.github/workflows/aws-lambda-java-core.yml @@ -1,9 +1,10 @@ # This workflow will be triggered if there will be changes to aws-lambda-java-core -# package and it builds the package and the packages that depend on it. +# package and it builds the package. name: Java CI aws-lambda-java-core on: + workflow_dispatch: push: branches: [ main ] paths: @@ -14,30 +15,22 @@ on: - 'aws-lambda-java-core/**' - '.github/workflows/aws-lambda-java-core.yml' +permissions: + contents: read + jobs: build: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v6 - name: Set up JDK 1.8 - uses: actions/setup-java@v4 + uses: actions/setup-java@v5 with: java-version: 8 distribution: corretto - - # Install base module + cache: maven + - name: Install core with Maven run: mvn -B install --file aws-lambda-java-core/pom.xml - - # Package modules that depend on base module - - name: Package log4j2 with Maven - run: mvn -B package --file aws-lambda-java-log4j2/pom.xml - - # Test Runtime Interface Client - - name: Run 'pr' target - working-directory: ./aws-lambda-java-runtime-interface-client - run: make pr - env: - IS_JAVA_8: true diff --git a/.github/workflows/aws-lambda-java-events-sdk-transformer.yml b/.github/workflows/aws-lambda-java-events-sdk-transformer.yml index 66f6b2bfe..144d52f86 100644 --- a/.github/workflows/aws-lambda-java-events-sdk-transformer.yml +++ b/.github/workflows/aws-lambda-java-events-sdk-transformer.yml @@ -1,36 +1,43 @@ -# This workflow will be triggered if there will be changes to -# aws-lambda-java-events-sdk-transformer package and it builds the package. +# This workflow will be triggered if there will be changes to +# aws-lambda-java-events-sdk-transformer package or its dependency (events), +# and it builds the package. name: Java CI aws-lambda-java-events-sdk-transformer on: + workflow_dispatch: push: branches: [ main ] paths: - 'aws-lambda-java-events-sdk-transformer/**' + - 'aws-lambda-java-events/**' pull_request: branches: [ '*' ] paths: - 'aws-lambda-java-events-sdk-transformer/**' + - 'aws-lambda-java-events/**' - '.github/workflows/aws-lambda-java-events-sdk-transformer.yml' +permissions: + contents: read + jobs: build: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v6 - name: Set up JDK 1.8 - uses: actions/setup-java@v4 + uses: actions/setup-java@v5 with: java-version: 8 distribution: corretto + cache: maven - # Install base module + # Install dependency - name: Install events with Maven run: mvn -B install --file aws-lambda-java-events/pom.xml # Package target module - name: Package events-sdk-transformer with Maven run: mvn -B package --file aws-lambda-java-events-sdk-transformer/pom.xml - diff --git a/.github/workflows/aws-lambda-java-events.yml b/.github/workflows/aws-lambda-java-events.yml index 04ab53a50..18be63cf9 100644 --- a/.github/workflows/aws-lambda-java-events.yml +++ b/.github/workflows/aws-lambda-java-events.yml @@ -1,9 +1,10 @@ # This workflow will be triggered if there will be changes to aws-lambda-java-events -# package and it builds the package and the packages that depend on it. +# package and it builds the package. name: Java CI aws-lambda-java-events on: + workflow_dispatch: push: branches: [ main ] paths: @@ -14,26 +15,22 @@ on: - 'aws-lambda-java-events/**' - '.github/workflows/aws-lambda-java-events.yml' +permissions: + contents: read + jobs: build: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v6 - name: Set up JDK 1.8 - uses: actions/setup-java@v4 + uses: actions/setup-java@v5 with: java-version: 8 distribution: corretto - - # Install base module + cache: maven + - name: Install events with Maven run: mvn -B install --file aws-lambda-java-events/pom.xml - - # Package modules that depend on base module - - name: Package serialization with Maven - run: mvn -B package --file aws-lambda-java-serialization/pom.xml - - name: Package events-sdk-transformer with Maven - run: mvn -B package --file aws-lambda-java-events-sdk-transformer/pom.xml - diff --git a/.github/workflows/aws-lambda-java-log4j2.yml b/.github/workflows/aws-lambda-java-log4j2.yml index 7ae54cbe1..945a1cb30 100644 --- a/.github/workflows/aws-lambda-java-log4j2.yml +++ b/.github/workflows/aws-lambda-java-log4j2.yml @@ -1,36 +1,42 @@ -# This workflow will be triggered if there will be changes to -# aws-lambda-java-log4j2 package and it builds the package. +# This workflow will be triggered if there will be changes to +# aws-lambda-java-log4j2 package or its dependency (core), and it builds the package. name: Java CI aws-lambda-java-log4j2 on: push: + workflow_dispatch: branches: [ main ] paths: - 'aws-lambda-java-log4j2/**' + - 'aws-lambda-java-core/**' pull_request: branches: [ '*' ] paths: - 'aws-lambda-java-log4j2/**' + - 'aws-lambda-java-core/**' - '.github/workflows/aws-lambda-java-log4j2.yml' +permissions: + contents: read + jobs: build: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v6 - name: Set up JDK 1.8 - uses: actions/setup-java@v4 + uses: actions/setup-java@v5 with: java-version: 8 distribution: corretto - - # Install base module + cache: maven + + # Install dependency - name: Install core with Maven run: mvn -B install --file aws-lambda-java-core/pom.xml # Package target module - name: Package log4j2 with Maven run: mvn -B package --file aws-lambda-java-log4j2/pom.xml - diff --git a/.github/workflows/aws-lambda-java-profiler.yml b/.github/workflows/aws-lambda-java-profiler.yml index db9fc225e..a098bfd14 100644 --- a/.github/workflows/aws-lambda-java-profiler.yml +++ b/.github/workflows/aws-lambda-java-profiler.yml @@ -22,16 +22,17 @@ jobs: contents: read steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6 - name: Set up JDK - uses: actions/setup-java@v4 + uses: actions/setup-java@be666c2fcd27ec809703dec50e508c2fdc7f6654 # v5 with: java-version: 21 distribution: corretto + cache: maven - name: Issue AWS credentials - uses: aws-actions/configure-aws-credentials@v4 + uses: aws-actions/configure-aws-credentials@7474bc4690e29a8392af63c5b98e7449536d5c3a # v4 with: aws-region: ${{ secrets.AWS_REGION_PROFILER_EXTENSION_INTEGRATION_TEST }} role-to-assume: ${{ secrets.AWS_ROLE_PROFILER_EXTENSION_INTEGRATION_TEST }} @@ -58,12 +59,16 @@ jobs: working-directory: ./experimental/aws-lambda-java-profiler run: ./integration_tests/invoke_function.sh + - name: Invoke Java Custom Options function + working-directory: ./experimental/aws-lambda-java-profiler + run: ./integration_tests/invoke_function_custom_options.sh + - name: Download from s3 working-directory: ./experimental/aws-lambda-java-profiler run: ./integration_tests/download_from_s3.sh - name: Upload profiles - uses: actions/upload-artifact@v4 + uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4 with: name: profiles path: /tmp/s3-artifacts diff --git a/.github/workflows/aws-lambda-java-serialization.yml b/.github/workflows/aws-lambda-java-serialization.yml index c24c48d72..f52c96fed 100644 --- a/.github/workflows/aws-lambda-java-serialization.yml +++ b/.github/workflows/aws-lambda-java-serialization.yml @@ -1,39 +1,46 @@ -# This workflow will be triggered if there will be changes to aws-lambda-java-serialization -# package and it builds the package and the packages that depend on it. +# This workflow will be triggered if there will be changes to aws-lambda-java-serialization +# package or its dependency (events), and it builds the package. name: Java CI aws-lambda-java-serialization on: + workflow_dispatch: push: branches: [ main ] paths: - 'aws-lambda-java-serialization/**' + - 'aws-lambda-java-events/**' pull_request: branches: [ '*' ] paths: - 'aws-lambda-java-serialization/**' + - 'aws-lambda-java-events/**' - '.github/workflows/aws-lambda-java-serialization.yml' +permissions: + contents: read + jobs: build: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v6 - name: Set up JDK 1.8 - uses: actions/setup-java@v4 + uses: actions/setup-java@v5 with: java-version: 8 distribution: corretto - - # Install base module + cache: maven + + # Install dependency - name: Install events with Maven run: mvn -B install --file aws-lambda-java-events/pom.xml # Package and install target module - name: Package serialization with Maven - run: mvn -B package install --file aws-lambda-java-serialization/pom.xml + run: mvn -B install --file aws-lambda-java-serialization/pom.xml # Run tests - name: Run tests from aws-lambda-java-tests diff --git a/.github/workflows/aws-lambda-java-tests.yml b/.github/workflows/aws-lambda-java-tests.yml index a28bca886..324c44514 100644 --- a/.github/workflows/aws-lambda-java-tests.yml +++ b/.github/workflows/aws-lambda-java-tests.yml @@ -1,33 +1,42 @@ # This workflow will be triggered if there will be changes to aws-lambda-java-tests -# package and it builds the package and the packages that depend on it. +# package or its dependencies (events, serialization), and it builds the package. name: Java CI aws-lambda-java-tests on: + workflow_dispatch: push: branches: [ main ] paths: - 'aws-lambda-java-tests/**' + - 'aws-lambda-java-events/**' + - 'aws-lambda-java-serialization/**' pull_request: branches: [ '*' ] paths: - 'aws-lambda-java-tests/**' + - 'aws-lambda-java-events/**' + - 'aws-lambda-java-serialization/**' - '.github/workflows/aws-lambda-java-tests.yml' +permissions: + contents: read + jobs: build: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v6 - name: Set up JDK 1.8 - uses: actions/setup-java@v4 + uses: actions/setup-java@v5 with: java-version: 8 distribution: corretto - - # Install base module + cache: maven + + # Install dependencies - name: Install events with Maven run: mvn -B install --file aws-lambda-java-events/pom.xml - name: Install serialization with Maven @@ -36,4 +45,3 @@ jobs: # Package target module - name: Package tests with Maven run: mvn -B package --file aws-lambda-java-tests/pom.xml - diff --git a/.github/workflows/repo-sync.yml b/.github/workflows/repo-sync.yml index 25f05029a..4934754d8 100644 --- a/.github/workflows/repo-sync.yml +++ b/.github/workflows/repo-sync.yml @@ -9,6 +9,10 @@ on: - '.github/workflows/repo-sync.yml' workflow_dispatch: +permissions: + contents: write + pull-requests: write + jobs: repo-sync: name: Repo Sync @@ -16,9 +20,9 @@ jobs: env: IS_CONFIGURED: ${{ secrets.SOURCE_REPO != '' }} steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v6 if: ${{ env.IS_CONFIGURED == 'true' }} - - uses: repo-sync/github-sync@v2 + - uses: repo-sync/github-sync@3832fe8e2be32372e1b3970bbae8e7079edeec88 # v2.3.0 name: Sync repo to branch if: ${{ env.IS_CONFIGURED == 'true' }} with: @@ -26,7 +30,7 @@ jobs: source_branch: main destination_branch: ${{ secrets.INTERMEDIATE_BRANCH }} github_token: ${{ secrets.GITHUB_TOKEN }} - - uses: repo-sync/pull-request@v2 + - uses: repo-sync/pull-request@7e79a9f5dc3ad0ce53138f01df2fad14a04831c5 # v2.12.1 name: Create pull request if: ${{ env.IS_CONFIGURED == 'true' }} with: diff --git a/.github/workflows/runtime-interface-client_merge_to_main.yml b/.github/workflows/runtime-interface-client_merge_to_main.yml index e07b191e1..d0d479111 100644 --- a/.github/workflows/runtime-interface-client_merge_to_main.yml +++ b/.github/workflows/runtime-interface-client_merge_to_main.yml @@ -11,12 +11,12 @@ name: Publish artifact for aws-lambda-java-runtime-interface-client on: + workflow_dispatch: push: branches: [ main ] paths: - 'aws-lambda-java-runtime-interface-client/**' - '.github/workflows/runtime-interface-client_*.yml' - workflow_dispatch: jobs: @@ -28,25 +28,30 @@ jobs: contents: read steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6 - name: Set up JDK 1.8 - uses: actions/setup-java@v4 + uses: actions/setup-java@be666c2fcd27ec809703dec50e508c2fdc7f6654 # v5 with: java-version: 8 distribution: corretto + cache: maven - name: Set up QEMU - uses: docker/setup-qemu-action@v3 + uses: docker/setup-qemu-action@c7c53464625b32c7a7e944ae62b3e17d2b600130 # v3 - name: Set up Docker Buildx - uses: docker/setup-buildx-action@v3 + uses: docker/setup-buildx-action@8d2750c68a42422c14e847fe6c8ac0403b4cbd6f # v3 with: install: true - name: Available buildx platforms run: echo ${{ steps.buildx.outputs.platforms }} + - name: Build and install serialization dependency locally + working-directory: ./aws-lambda-java-serialization + run: mvn clean install + - name: Test Runtime Interface Client xplatform build - Run 'build' target working-directory: ./aws-lambda-java-runtime-interface-client run: make build @@ -57,7 +62,7 @@ jobs: if: env.ENABLE_SNAPSHOT != null env: ENABLE_SNAPSHOT: ${{ secrets.ENABLE_SNAPSHOT }} - uses: aws-actions/configure-aws-credentials@v4 + uses: aws-actions/configure-aws-credentials@7474bc4690e29a8392af63c5b98e7449536d5c3a # v4 with: aws-region: ${{ secrets.AWS_REGION }} role-to-assume: ${{ secrets.AWS_ROLE }} @@ -86,6 +91,6 @@ jobs: - name: Upload coverage to Codecov if: env.CODECOV_TOKEN != null - uses: codecov/codecov-action@v5 + uses: codecov/codecov-action@75cd11691c0faa626561e295848008c8a7dddffe # v5 env: CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }} diff --git a/.github/workflows/runtime-interface-client_pr.yml b/.github/workflows/runtime-interface-client_pr.yml index 33c6df50b..a0d8c6cc8 100644 --- a/.github/workflows/runtime-interface-client_pr.yml +++ b/.github/workflows/runtime-interface-client_pr.yml @@ -1,56 +1,81 @@ -# This workflow will be triggered if there will be changes to -# aws-lambda-java-runtime-interface-client package and it builds the package. +# This workflow will be triggered if there will be changes to +# aws-lambda-java-runtime-interface-client package or its dependencies (core, serialization), +# and it builds the package. name: PR to runtime-interface-client on: + workflow_dispatch: pull_request: branches: [ '*' ] paths: - 'aws-lambda-java-runtime-interface-client/**' + - 'aws-lambda-java-core/**' + - 'aws-lambda-java-serialization/**' - '.github/workflows/runtime-interface-client_*.yml' +permissions: + contents: read + jobs: smoke-test: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6 - name: Set up JDK 1.8 - uses: actions/setup-java@v4 + uses: actions/setup-java@be666c2fcd27ec809703dec50e508c2fdc7f6654 # v5 with: java-version: 8 distribution: corretto + cache: maven + + - name: Build and install core dependency locally + working-directory: ./aws-lambda-java-core + run: mvn clean install + + - name: Build and install serialization dependency locally + working-directory: ./aws-lambda-java-serialization + run: mvn clean install - name: Runtime Interface Client smoke tests - Run 'pr' target working-directory: ./aws-lambda-java-runtime-interface-client run: make pr env: IS_JAVA_8: true - + build: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6 - name: Set up JDK 1.8 - uses: actions/setup-java@v4 + uses: actions/setup-java@be666c2fcd27ec809703dec50e508c2fdc7f6654 # v5 with: java-version: 8 distribution: corretto + cache: maven - name: Set up QEMU - uses: docker/setup-qemu-action@v3 + uses: docker/setup-qemu-action@c7c53464625b32c7a7e944ae62b3e17d2b600130 # v3 - name: Set up Docker Buildx - uses: docker/setup-buildx-action@v3 + uses: docker/setup-buildx-action@8d2750c68a42422c14e847fe6c8ac0403b4cbd6f # v3 with: install: true - name: Available buildx platforms run: echo ${{ steps.buildx.outputs.platforms }} + - name: Build and install core dependency locally + working-directory: ./aws-lambda-java-core + run: mvn clean install + + - name: Build and install serialization dependency locally + working-directory: ./aws-lambda-java-serialization + run: mvn clean install + - name: Test Runtime Interface Client xplatform build - Run 'build' target working-directory: ./aws-lambda-java-runtime-interface-client run: make build @@ -58,13 +83,13 @@ jobs: IS_JAVA_8: true - name: Save the built jar - uses: actions/upload-artifact@v4 + uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4 with: name: aws-lambda-java-runtime-interface-client path: ./aws-lambda-java-runtime-interface-client/target/aws-lambda-java-runtime-interface-client-*.jar - name: Upload coverage to Codecov if: env.CODECOV_TOKEN != null - uses: codecov/codecov-action@v5 + uses: codecov/codecov-action@75cd11691c0faa626561e295848008c8a7dddffe # v5 env: CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }} diff --git a/.github/workflows/samples.yml b/.github/workflows/samples.yml index 8346b7c2f..68e25827d 100644 --- a/.github/workflows/samples.yml +++ b/.github/workflows/samples.yml @@ -1,37 +1,46 @@ -# This workflow will be triggered if there will be changes to aws-lambda-java-core -# package and it builds the package and the packages that depend on it. +# This workflow will be triggered if there will be changes to samples +# or their dependencies (events, serialization, tests). name: Java CI samples on: + workflow_dispatch: push: branches: [ main ] paths: - 'samples/**' + - 'aws-lambda-java-events/**' + - 'aws-lambda-java-serialization/**' + - 'aws-lambda-java-tests/**' pull_request: branches: [ '*' ] paths: - 'samples/**' + - 'aws-lambda-java-events/**' + - 'aws-lambda-java-serialization/**' + - 'aws-lambda-java-tests/**' - '.github/workflows/samples.yml' +permissions: + contents: read + jobs: build: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v6 - name: Set up JDK 1.8 - uses: actions/setup-java@v4 + uses: actions/setup-java@v5 with: java-version: 8 distribution: corretto + cache: maven - # Install events module + # Install dependencies - name: Install events with Maven run: mvn -B install --file aws-lambda-java-events/pom.xml - # Install serialization module - name: Install serialization with Maven run: mvn -B install --file aws-lambda-java-serialization/pom.xml - # Install tests module - name: Install tests with Maven run: mvn -B install --file aws-lambda-java-tests/pom.xml @@ -42,15 +51,16 @@ jobs: custom-serialization: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v6 # Set up both Java 8 and 21 - name: Set up Java 8 and 21 - uses: actions/setup-java@v4 + uses: actions/setup-java@v5 with: java-version: | 8 21 distribution: corretto + cache: maven # Install events module using Java 8 - name: Install events with Maven @@ -63,7 +73,7 @@ jobs: # Build custom-serialization samples - name: install sam - uses: aws-actions/setup-sam@v2 + uses: aws-actions/setup-sam@d78e1a4a9656d3b223e59b80676a797f20093133 # v2 - name: test fastJson run: cd samples/custom-serialization/fastJson && sam build && sam local invoke -e events/event.json | grep 200 - name: test gson diff --git a/.gitignore b/.gitignore index 9f99cc415..5a277e5d6 100644 --- a/.gitignore +++ b/.gitignore @@ -35,3 +35,7 @@ experimental/aws-lambda-java-profiler/extension/build/ experimental/aws-lambda-java-profiler/integration_tests/helloworld/bin !experimental/aws-lambda-java-profiler/extension/gradle/wrapper/*.jar /scratch/ +.vscode +.kiro +build +mise.toml diff --git a/README.md b/README.md index b5153a87f..580e14e41 100644 --- a/README.md +++ b/README.md @@ -163,7 +163,7 @@ The purpose of this package is to allow developers to deploy their applications com.amazonaws aws-lambda-java-runtime-interface-client - 2.7.0 + 2.10.1 ``` diff --git a/aws-lambda-java-core/RELEASE.CHANGELOG.md b/aws-lambda-java-core/RELEASE.CHANGELOG.md index 527e7dd46..aebc8ecd9 100644 --- a/aws-lambda-java-core/RELEASE.CHANGELOG.md +++ b/aws-lambda-java-core/RELEASE.CHANGELOG.md @@ -1,3 +1,7 @@ +### September 3, 2025 +`1.4.0` +- Getter support for x-ray trace ID through the Context object + ### May 26, 2025 `1.3.0` - Adding support for multi tenancy ([#545](https://github.com/aws/aws-lambda-java-libs/pull/545)) diff --git a/aws-lambda-java-core/pom.xml b/aws-lambda-java-core/pom.xml index 2b3abc0ba..cca9d0cdf 100644 --- a/aws-lambda-java-core/pom.xml +++ b/aws-lambda-java-core/pom.xml @@ -5,7 +5,7 @@ com.amazonaws aws-lambda-java-core - 1.3.0 + 1.4.0 jar AWS Lambda Java Core Library @@ -36,13 +36,6 @@ 1.8 - - - sonatype-nexus-staging - https://oss.sonatype.org/service/local/staging/deploy/maven2/ - - - dev @@ -115,14 +108,12 @@ - org.sonatype.plugins - nexus-staging-maven-plugin - 1.6.3 + org.sonatype.central + central-publishing-maven-plugin + 0.8.0 true - sonatype-nexus-staging - https://aws.oss.sonatype.org/ - false + central diff --git a/aws-lambda-java-core/src/main/java/com/amazonaws/services/lambda/runtime/Context.java b/aws-lambda-java-core/src/main/java/com/amazonaws/services/lambda/runtime/Context.java index 152765df1..ed9311a11 100644 --- a/aws-lambda-java-core/src/main/java/com/amazonaws/services/lambda/runtime/Context.java +++ b/aws-lambda-java-core/src/main/java/com/amazonaws/services/lambda/runtime/Context.java @@ -109,4 +109,14 @@ public interface Context { default String getTenantId() { return null; } + + /** + * + * Returns the X-Ray trace ID associated with the request. + * + * @return null by default + */ + default String getXrayTraceId() { + return null; + } } diff --git a/aws-lambda-java-events-sdk-transformer/pom.xml b/aws-lambda-java-events-sdk-transformer/pom.xml index 6a2b1735c..6de599ef7 100644 --- a/aws-lambda-java-events-sdk-transformer/pom.xml +++ b/aws-lambda-java-events-sdk-transformer/pom.xml @@ -5,7 +5,7 @@ com.amazonaws aws-lambda-java-events-sdk-transformer - 3.1.0 + 3.1.1 jar AWS Lambda Java Events SDK Transformer Library @@ -38,6 +38,8 @@ 1.8 1.11.914 2.15.40 + 5.12.2 + 3.5.4 @@ -63,14 +65,14 @@ com.amazonaws aws-lambda-java-events - 3.11.2 + 3.16.1 provided org.junit.jupiter junit-jupiter-engine - 5.7.0 + ${junit-jupiter.version} test @@ -79,11 +81,14 @@ maven-surefire-plugin - 2.22.2 + ${maven-surefire-plugin.version} + + true + maven-failsafe-plugin - 2.22.2 + ${maven-surefire-plugin.version} @@ -160,18 +165,16 @@ - org.sonatype.plugins - nexus-staging-maven-plugin - 1.6.3 + org.sonatype.central + central-publishing-maven-plugin + 0.8.0 true - sonatype-nexus-staging - https://aws.oss.sonatype.org/ - false + central - + \ No newline at end of file diff --git a/aws-lambda-java-events/pom.xml b/aws-lambda-java-events/pom.xml index 8799966be..c8c40e0c7 100644 --- a/aws-lambda-java-events/pom.xml +++ b/aws-lambda-java-events/pom.xml @@ -1,11 +1,11 @@ + xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/maven-v4_0_0.xsd"> 4.0.0 com.amazonaws aws-lambda-java-events - 3.16.0 + 3.16.1 jar AWS Lambda Java Events Library @@ -37,6 +37,9 @@ 1.18.22 UTF-8 UTF-8 + 2.20.1 + 2.40.1 + 5.12.2 @@ -56,19 +59,19 @@ org.junit.jupiter junit-jupiter-engine - 5.9.2 + ${junit-jupiter.version} test com.fasterxml.jackson.core jackson-databind - 2.14.2 + ${jackson.version} test net.javacrumbs.json-unit json-unit-assertj - 2.36.1 + ${json.unit} test @@ -152,20 +155,18 @@ - org.sonatype.plugins - nexus-staging-maven-plugin - 1.6.3 + org.sonatype.central + central-publishing-maven-plugin + 0.8.0 true - sonatype-nexus-staging - https://aws.oss.sonatype.org/ - false + central org.apache.maven.plugins maven-resources-plugin - 3.2.0 + 3.3.1 UTF-8 @@ -173,7 +174,7 @@ org.apache.maven.plugins maven-compiler-plugin - 3.8.1 + 3.11.0 @@ -189,4 +190,4 @@ - + \ No newline at end of file diff --git a/aws-lambda-java-events/src/main/java/com/amazonaws/services/lambda/runtime/events/ConnectEvent.java b/aws-lambda-java-events/src/main/java/com/amazonaws/services/lambda/runtime/events/ConnectEvent.java index 38547ac2a..e94875614 100644 --- a/aws-lambda-java-events/src/main/java/com/amazonaws/services/lambda/runtime/events/ConnectEvent.java +++ b/aws-lambda-java-events/src/main/java/com/amazonaws/services/lambda/runtime/events/ConnectEvent.java @@ -59,7 +59,7 @@ public static class ContactData implements Serializable, Cloneable { private String initiationMethod; private String instanceArn; private String previousContactId; - private String queue; + private Queue queue; private SystemEndpoint systemEndpoint; } @@ -80,4 +80,13 @@ public static class SystemEndpoint implements Serializable, Cloneable { private String address; private String type; } + @Data + @Builder(setterPrefix = "with") + @NoArgsConstructor + @AllArgsConstructor + public static class Queue implements Serializable, Cloneable { + private String name; + private String ARN; + } + } diff --git a/aws-lambda-java-log4j2/README.md b/aws-lambda-java-log4j2/README.md index b1b739b69..f13121750 100644 --- a/aws-lambda-java-log4j2/README.md +++ b/aws-lambda-java-log4j2/README.md @@ -39,7 +39,7 @@ If using maven shade plugin, set the plugin configuration as follows org.apache.maven.plugins maven-shade-plugin - 2.4.3 + 3.6.1 package diff --git a/aws-lambda-java-log4j2/pom.xml b/aws-lambda-java-log4j2/pom.xml index b33300ef2..0124598a0 100644 --- a/aws-lambda-java-log4j2/pom.xml +++ b/aws-lambda-java-log4j2/pom.xml @@ -5,7 +5,7 @@ com.amazonaws aws-lambda-java-log4j2 - 1.6.0 + 1.6.1 jar AWS Lambda Java Log4j 2.x Libraries @@ -34,7 +34,7 @@ 1.8 1.8 - 2.17.1 + 2.25.3 @@ -134,18 +134,16 @@ - org.sonatype.plugins - nexus-staging-maven-plugin - 1.6.3 + org.sonatype.central + central-publishing-maven-plugin + 0.8.0 true - sonatype-nexus-staging - https://aws.oss.sonatype.org/ - false + central - + \ No newline at end of file diff --git a/aws-lambda-java-runtime-interface-client/Dockerfile.rie b/aws-lambda-java-runtime-interface-client/Dockerfile.rie new file mode 100644 index 000000000..66a01c834 --- /dev/null +++ b/aws-lambda-java-runtime-interface-client/Dockerfile.rie @@ -0,0 +1,8 @@ +FROM public.ecr.aws/lambda/java:21 + +COPY target/aws-lambda-java-runtime-interface-client-*.jar ${LAMBDA_TASK_ROOT}/ +COPY target/aws-lambda-java-core-*.jar ${LAMBDA_TASK_ROOT}/ +COPY target/aws-lambda-java-serialization-*.jar ${LAMBDA_TASK_ROOT}/ +COPY test-handlers/EchoHandler.class ${LAMBDA_TASK_ROOT}/ + +CMD ["EchoHandler::handleRequest"] \ No newline at end of file diff --git a/aws-lambda-java-runtime-interface-client/Makefile b/aws-lambda-java-runtime-interface-client/Makefile index b3a204213..6c3a268fb 100644 --- a/aws-lambda-java-runtime-interface-client/Makefile +++ b/aws-lambda-java-runtime-interface-client/Makefile @@ -65,6 +65,10 @@ publish: test-publish: ./ric-dev-environment/test-platform-specific-jar-snapshot.sh +.PHONY: test-rie +test-rie: + ./scripts/test-rie.sh "EchoHandler::handleRequest" + define HELP_MESSAGE Usage: $ make [TARGETS] @@ -74,5 +78,5 @@ TARGETS dev Run all development tests after a change. pr Perform all checks before submitting a Pull Request. test Run the Unit tests. - + test-rie Build and test RIC locally with Lambda Runtime Interface Emulator. (Requires building the project first) endef diff --git a/aws-lambda-java-runtime-interface-client/README.md b/aws-lambda-java-runtime-interface-client/README.md index d49201bd5..c448e7a89 100644 --- a/aws-lambda-java-runtime-interface-client/README.md +++ b/aws-lambda-java-runtime-interface-client/README.md @@ -70,7 +70,7 @@ pom.xml com.amazonaws aws-lambda-java-runtime-interface-client - 2.7.0 + 2.10.1 @@ -138,6 +138,49 @@ This command invokes the function running in the container image and returns a r *Alternately, you can also include RIE as a part of your base image. See the AWS documentation on how to [Build RIE into your base image](https://docs.aws.amazon.com/lambda/latest/dg/images-test.html#images-test-alternative).* +### Automated Local Testing + +For developers working on this runtime interface client, we provide an automated testing script that handles RIE setup, dependency management, and Docker orchestration. + +*Prerequisites:* +- Build the project first: `mvn clean install` +- Docker must be installed and running + +*To run automated tests:* + +```shell script +make test-rie +``` + +This single command will: +- Automatically download required dependencies (aws-lambda-java-core, aws-lambda-java-serialization) +- Build a Docker image with RIE pre-installed +- Compile and run a test Lambda function (EchoHandler) +- Execute the function and validate the response +- Clean up containers automatically + +The test uses a simple EchoHandler that returns the input event, making it easy to verify the runtime interface client is working correctly. + +## Test Coverage + +This project uses JaCoCo for code coverage analysis. To exclude classes from JaCoCo coverage, add them to the `jacoco-maven-plugin` configuration: + +```xml + + org.jacoco + jacoco-maven-plugin + + + **/*Exception.class + **/dto/*.class + **/YourClassName.class + + + +``` + +This project excludes by default: exceptions, interfaces, DTOs, constants, and runtime-only classes. + ### Troubleshooting While running integration tests, you might encounter the Docker Hub rate limit error with the following body: @@ -160,7 +203,7 @@ platform-specific JAR by setting the ``. com.amazonaws aws-lambda-java-runtime-interface-client - 2.7.0 + 2.10.1 linux-x86_64 ``` diff --git a/aws-lambda-java-runtime-interface-client/RELEASE.CHANGELOG.md b/aws-lambda-java-runtime-interface-client/RELEASE.CHANGELOG.md index ac073ae85..94a8a8057 100644 --- a/aws-lambda-java-runtime-interface-client/RELEASE.CHANGELOG.md +++ b/aws-lambda-java-runtime-interface-client/RELEASE.CHANGELOG.md @@ -1,3 +1,48 @@ +### March 19, 2026 +`2.10.1` +- Revert aws-lambda-java-serialization dependency to 1.2.0 + +### March 12, 2026 +`2.10.0` +- Update aws-lambda-java-serialization dependency to 1.3.0 + +### March 12, 2026 +`2.9.0` +- Update aws-lambda-java-serialization dependency to 1.2.0 + +### September 22, 2025 +`2.8.7` +- Remove Minimum and Maximum Limits of AWS_LAMBDA_MAX_CONCURRENCY. + +### September 22, 2025 +`2.8.6` +- Set Multiconcurrent Trace ID using utils-lite. + +### September 17, 2025 +`2.8.5` +- Log errorType and errorMessage from RAPID in C++ Client. +- Performance Upgrade for Multiconcurrency Mode. + +### September 9, 2025 +`2.8.4` +- Make Trace ID Accessible through Context Object. + +### July 19, 2025 +`2.8.3` +- Ensure EventHandlerLoader Thread Safety. + +### June 26, 2025 +`2.8.2` +- Allow AWS_LAMBDA_MAX_CONCURRENCY to be One. Crash the RIC if it is set to an un-parsable string to an integer or an out of bounds value. + +### June 26, 2025 +`2.8.1` +- Refactoring + +### June 26, 2025 +`2.8.0` +- Refactoring + ### May 21, 2025 `2.7.0` - Adding support for multi tenancy ([#540](https://github.com/aws/aws-lambda-java-libs/pull/540)) diff --git a/aws-lambda-java-runtime-interface-client/pom.xml b/aws-lambda-java-runtime-interface-client/pom.xml index d4f7fd5e3..a09fd3df7 100644 --- a/aws-lambda-java-runtime-interface-client/pom.xml +++ b/aws-lambda-java-runtime-interface-client/pom.xml @@ -1,10 +1,10 @@ + xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd"> 4.0.0 com.amazonaws aws-lambda-java-runtime-interface-client - 2.7.0 + 2.10.1 jar AWS Lambda Java Runtime Interface Client @@ -37,8 +37,9 @@ 0.8.12 2.4 3.1.1 - 5.9.2 + 5.12.2 3.4.0 + 3.5.4 true - - - + + + + **/*Exception.class + + **/Resource.class + + **/dto/*.class + + **/ReservedRuntimeEnvironmentVariables.class + **/RapidErrorType.class + + **/FrameType.class + **/StructuredLogMessage.class + + **/AWSLambda.class + + default-prepare-agent @@ -251,7 +279,7 @@ - + org.apache.maven.plugins maven-checkstyle-plugin ${maven-checkstyle-plugin.version} @@ -347,16 +375,52 @@ - org.sonatype.plugins - nexus-staging-maven-plugin - 1.6.3 + org.sonatype.central + central-publishing-maven-plugin + 0.8.0 true - sonatype-nexus-staging - https://aws.oss.sonatype.org/ - false + central + + org.codehaus.mojo + build-helper-maven-plugin + 3.4.0 + + + attach-platform-artifacts + package + + attach-artifact + + + + + ${project.build.directory}/${project.build.finalName}-linux-x86_64.jar + jar + linux-x86_64 + + + ${project.build.directory}/${project.build.finalName}-linux-aarch_64.jar + jar + linux-aarch_64 + + + ${project.build.directory}/${project.build.finalName}-linux_musl-x86_64.jar + jar + linux_musl-x86_64 + + + ${project.build.directory}/${project.build.finalName}-linux_musl-aarch_64.jar + jar + linux_musl-aarch_64 + + + + + + diff --git a/aws-lambda-java-runtime-interface-client/scripts/test-rie.sh b/aws-lambda-java-runtime-interface-client/scripts/test-rie.sh new file mode 100755 index 000000000..b69c967a1 --- /dev/null +++ b/aws-lambda-java-runtime-interface-client/scripts/test-rie.sh @@ -0,0 +1,46 @@ +#!/bin/bash +set -euo pipefail + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +PROJECT_ROOT="$(dirname "$SCRIPT_DIR")" +SERIALIZATION_ROOT="$(dirname "$PROJECT_ROOT")/aws-lambda-java-serialization" + +if ! ls "$PROJECT_ROOT"/target/aws-lambda-java-runtime-interface-client-*.jar >/dev/null 2>&1; then + echo "RIC jar not found. Please build the project first with 'mvn package'." + exit 1 +fi + +IMAGE_TAG="java-ric-rie-test" + +HANDLER="${1:-EchoHandler::handleRequest}" + +echo "Starting RIE test setup for Java..." + +# Build local dependencies if not present +CORE_ROOT="$(dirname "$PROJECT_ROOT")/aws-lambda-java-core" +if ! ls "$PROJECT_ROOT"/target/aws-lambda-java-core-*.jar >/dev/null 2>&1; then + echo "Building local aws-lambda-java-core..." + (cd "$CORE_ROOT" && mvn package -DskipTests) + cp "$CORE_ROOT"/target/aws-lambda-java-core-*.jar "$PROJECT_ROOT/target/" +fi + +if ! ls "$PROJECT_ROOT"/target/aws-lambda-java-serialization-*.jar >/dev/null 2>&1; then + echo "Building local aws-lambda-java-serialization..." + (cd "$SERIALIZATION_ROOT" && mvn package -DskipTests) + cp "$SERIALIZATION_ROOT"/target/aws-lambda-java-serialization-*.jar "$PROJECT_ROOT/target/" +fi + +echo "Compiling EchoHandler..." +javac -source 21 -target 21 -cp "$(ls "$PROJECT_ROOT"/target/aws-lambda-java-runtime-interface-client-*.jar):$(ls "$PROJECT_ROOT"/target/aws-lambda-java-core-*.jar):$(ls "$PROJECT_ROOT"/target/aws-lambda-java-serialization-*.jar)" \ + -d "$PROJECT_ROOT/test-handlers/" "$PROJECT_ROOT/test-handlers/EchoHandler.java" + +echo "Building test Docker image..." +docker build -t "$IMAGE_TAG" -f "$PROJECT_ROOT/Dockerfile.rie" "$PROJECT_ROOT" + +echo "Starting test container on port 9000..." +echo "" +echo "In another terminal, invoke with:" +echo "curl -s -X POST -H 'Content-Type: application/json' \"http://localhost:9000/2015-03-31/functions/function/invocations\" -d '{\"message\":\"test\"}'" +echo "" + +exec docker run -it -p 9000:8080 -e _HANDLER="$HANDLER" "$IMAGE_TAG" \ No newline at end of file diff --git a/aws-lambda-java-runtime-interface-client/src/main/java/com/amazonaws/services/lambda/runtime/api/client/AWSLambda.java b/aws-lambda-java-runtime-interface-client/src/main/java/com/amazonaws/services/lambda/runtime/api/client/AWSLambda.java index 2eeb14e3d..e5b221a80 100644 --- a/aws-lambda-java-runtime-interface-client/src/main/java/com/amazonaws/services/lambda/runtime/api/client/AWSLambda.java +++ b/aws-lambda-java-runtime-interface-client/src/main/java/com/amazonaws/services/lambda/runtime/api/client/AWSLambda.java @@ -15,10 +15,12 @@ import com.amazonaws.services.lambda.runtime.api.client.runtimeapi.LambdaError; import com.amazonaws.services.lambda.runtime.api.client.runtimeapi.LambdaRuntimeApiClient; import com.amazonaws.services.lambda.runtime.api.client.runtimeapi.LambdaRuntimeApiClientImpl; +import com.amazonaws.services.lambda.runtime.api.client.runtimeapi.LambdaRuntimeClientMaxRetriesExceededException; import com.amazonaws.services.lambda.runtime.api.client.runtimeapi.RapidErrorType; import com.amazonaws.services.lambda.runtime.api.client.runtimeapi.converters.LambdaErrorConverter; import com.amazonaws.services.lambda.runtime.api.client.runtimeapi.converters.XRayErrorCauseConverter; import com.amazonaws.services.lambda.runtime.api.client.runtimeapi.dto.InvocationRequest; +import com.amazonaws.services.lambda.runtime.api.client.util.ConcurrencyConfig; import com.amazonaws.services.lambda.runtime.api.client.util.LambdaOutputStream; import com.amazonaws.services.lambda.runtime.api.client.util.UnsafeUtil; import com.amazonaws.services.lambda.runtime.logging.LogFormat; @@ -35,6 +37,10 @@ import java.net.URLClassLoader; import java.security.Security; import java.util.Properties; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; +import java.util.concurrent.TimeUnit; +import software.amazon.awssdk.utilslite.SdkInternalThreadLocal; /** * The entrypoint of this class is {@link AWSLambda#startRuntime}. It performs two main tasks: @@ -49,8 +55,8 @@ */ public class AWSLambda { - protected static URLClassLoader customerClassLoader; - + private static URLClassLoader customerClassLoader; + private static final String TRUST_STORE_PROPERTY = "javax.net.ssl.trustStore"; private static final String JAVA_SECURITY_PROPERTIES = "java.security.properties"; @@ -69,8 +75,8 @@ public class AWSLambda { private static final String AWS_LAMBDA_INITIALIZATION_TYPE = System.getenv(ReservedRuntimeEnvironmentVariables.AWS_LAMBDA_INITIALIZATION_TYPE); - private static LambdaRuntimeApiClient runtimeClient; - + private static final String CONCURRENT_TRACE_ID_KEY = "AWS_LAMBDA_X_TRACE_ID"; + static { // Override the disabledAlgorithms setting to match configuration for openjdk8-u181. // This is to keep DES ciphers around while we deploying security updates. @@ -137,15 +143,13 @@ private static LambdaRequestHandler findRequestHandler(final String handlerStrin return requestHandler; } - private static LambdaRequestHandler getLambdaRequestHandlerObject(String handler, LambdaContextLogger lambdaLogger) throws ClassNotFoundException, IOException { + private static LambdaRequestHandler getLambdaRequestHandlerObject(String handler, LambdaContextLogger lambdaLogger, LambdaRuntimeApiClient runtimeClient) throws ClassNotFoundException, IOException { UnsafeUtil.disableIllegalAccessWarning(); System.setOut(new PrintStream(new LambdaOutputStream(System.out), false, "UTF-8")); System.setErr(new PrintStream(new LambdaOutputStream(System.err), false, "UTF-8")); setupRuntimeLogger(lambdaLogger); - runtimeClient = new LambdaRuntimeApiClientImpl(LambdaEnvironment.RUNTIME_API); - String taskRoot = System.getProperty("user.dir"); String libRoot = "/opt/java"; // Make system classloader the customer classloader's parent to ensure any aws-lambda-java-core classes @@ -167,13 +171,13 @@ private static LambdaRequestHandler getLambdaRequestHandlerObject(String handler } if (INIT_TYPE_SNAP_START.equals(AWS_LAMBDA_INITIALIZATION_TYPE)) { - onInitComplete(lambdaLogger); + onInitComplete(lambdaLogger, runtimeClient); } return requestHandler; } - public static void setupRuntimeLogger(LambdaLogger lambdaLogger) + private static void setupRuntimeLogger(LambdaLogger lambdaLogger) throws ClassNotFoundException { ReflectUtil.setStaticField( Class.forName("com.amazonaws.services.lambda.runtime.LambdaRuntime"), @@ -213,10 +217,11 @@ private static LogSink createLogSink() { } public static void main(String[] args) throws Throwable { - try (LambdaContextLogger logger = initLogger()) { - LambdaRequestHandler lambdaRequestHandler = getLambdaRequestHandlerObject(args[0], logger); - startRuntimeLoop(lambdaRequestHandler, logger); - + try (LambdaContextLogger lambdaLogger = initLogger()) { + LambdaRuntimeApiClient runtimeClient = new LambdaRuntimeApiClientImpl(LambdaEnvironment.RUNTIME_API); + LambdaRequestHandler lambdaRequestHandler = getLambdaRequestHandlerObject(args[0], lambdaLogger, runtimeClient); + ConcurrencyConfig concurrencyConfig = new ConcurrencyConfig(lambdaLogger); + startRuntimeLoops(lambdaRequestHandler, lambdaLogger, concurrencyConfig, runtimeClient); } catch (IOException | ClassNotFoundException t) { throw new Error(t); } @@ -232,50 +237,111 @@ private static LambdaContextLogger initLogger() { return logger; } - private static void startRuntimeLoop(LambdaRequestHandler requestHandler, LambdaContextLogger lambdaLogger) throws Throwable { - boolean shouldExit = false; - while (!shouldExit) { - UserFault userFault = null; - InvocationRequest request = runtimeClient.nextInvocation(); - if (request.getXrayTraceId() != null) { - System.setProperty(LAMBDA_TRACE_HEADER_PROP, request.getXrayTraceId()); - } else { - System.clearProperty(LAMBDA_TRACE_HEADER_PROP); + private static void startRuntimeLoopWithExecutor(LambdaRequestHandler lambdaRequestHandler, LambdaContextLogger lambdaLogger, ExecutorService executorService, LambdaRuntimeApiClient runtimeClient) { + executorService.submit(() -> { + try { + startRuntimeLoop(lambdaRequestHandler, lambdaLogger, runtimeClient, false); + } catch (Exception e) { + lambdaLogger.log(String.format("Runtime Loop on Thread ID: %s Failed.\n%s", Thread.currentThread().getName(), UserFault.trace(e)), lambdaLogger.getLogFormat() == LogFormat.JSON ? LogLevel.ERROR : LogLevel.UNDEFINED); } + }); + } - ByteArrayOutputStream payload; + protected static void startRuntimeLoops(LambdaRequestHandler lambdaRequestHandler, LambdaContextLogger lambdaLogger, ConcurrencyConfig concurrencyConfig, LambdaRuntimeApiClient runtimeClient) throws Exception { + if (concurrencyConfig.isMultiConcurrent()) { + lambdaLogger.log(concurrencyConfig.getConcurrencyConfigMessage(), lambdaLogger.getLogFormat() == LogFormat.JSON ? LogLevel.INFO : LogLevel.UNDEFINED); + ExecutorService platformThreadExecutor = Executors.newFixedThreadPool(concurrencyConfig.getNumberOfPlatformThreads()); try { - payload = requestHandler.call(request); - runtimeClient.reportInvocationSuccess(request.getId(), payload.toByteArray()); - // clear interrupted flag in case if it was set by user's code - Thread.interrupted(); - } catch (UserFault f) { - shouldExit = f.fatal; - userFault = f; - UserFault.filterStackTrace(f); - LambdaError error = new LambdaError( - LambdaErrorConverter.fromUserFault(f), - RapidErrorType.BadFunctionCode); - runtimeClient.reportInvocationError(request.getId(), error); - } catch (Throwable t) { - shouldExit = t instanceof VirtualMachineError || t instanceof IOError; - UserFault.filterStackTrace(t); - userFault = UserFault.makeUserFault(t); - - LambdaError error = new LambdaError( - LambdaErrorConverter.fromThrowable(t), - XRayErrorCauseConverter.fromThrowable(t), - RapidErrorType.UserException); - runtimeClient.reportInvocationError(request.getId(), error); + for (int i = 0; i < concurrencyConfig.getNumberOfPlatformThreads(); i++) { + startRuntimeLoopWithExecutor(lambdaRequestHandler, lambdaLogger, platformThreadExecutor, runtimeClient); + } } finally { - if (userFault != null) { - lambdaLogger.log(userFault.reportableError(), lambdaLogger.getLogFormat() == LogFormat.JSON ? LogLevel.ERROR : LogLevel.UNDEFINED); + platformThreadExecutor.shutdown(); + try { + platformThreadExecutor.awaitTermination(Long.MAX_VALUE, TimeUnit.DAYS); + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); } } + } else { + startRuntimeLoop(lambdaRequestHandler, lambdaLogger, runtimeClient, true); + } + } + + private static LambdaError createLambdaErrorFromThrowableOrUserFault(Throwable t) { + if (t instanceof UserFault) { + return new LambdaError( + LambdaErrorConverter.fromUserFault((UserFault) t), + RapidErrorType.BadFunctionCode); + } else { + return new LambdaError( + LambdaErrorConverter.fromThrowable(t), + XRayErrorCauseConverter.fromThrowable(t), + RapidErrorType.UserException); } } - static void onInitComplete(final LambdaContextLogger lambdaLogger) throws IOException { + private static void setEnvVarForXrayTraceId(InvocationRequest request) { + if (request.getXrayTraceId() != null) { + System.setProperty(LAMBDA_TRACE_HEADER_PROP, request.getXrayTraceId()); + } else { + System.clearProperty(LAMBDA_TRACE_HEADER_PROP); + } + } + + private static void reportNonLoopTerminatingException(LambdaContextLogger lambdaLogger, Throwable t) { + lambdaLogger.log( + String.format( + "Runtime Loop on Thread ID: %s Faced and Exception. This exception will not stop the runtime loop.\nException:\n%s", + Thread.currentThread().getName(), UserFault.trace(t)), + lambdaLogger.getLogFormat() == LogFormat.JSON ? LogLevel.ERROR : LogLevel.UNDEFINED); + } + + /* + * In multiconcurrent mode (exitLoopOnErrors = false), The Runtime Loop will not exit unless LambdaRuntimeClientMaxRetriesExceededException is thrown when calling nextInvocationWithExponentialBackoff. + * In normal/sequential mode (exitLoopOnErrors = true), The Runtime Loop will exit if nextInvocation call fails, when UserFault is fatal, or an Error of type VirtualMachineError or IOError is thrown. + */ + private static void startRuntimeLoop(LambdaRequestHandler lambdaRequestHandler, LambdaContextLogger lambdaLogger, LambdaRuntimeApiClient runtimeClient, boolean exitLoopOnErrors) throws Exception { + boolean shouldExit = false; + while (!shouldExit) { + try { + UserFault userFault = null; + InvocationRequest request = exitLoopOnErrors ? runtimeClient.nextInvocation() : runtimeClient.nextInvocationWithExponentialBackoff(lambdaLogger); + if (exitLoopOnErrors) { + setEnvVarForXrayTraceId(request); + } else { + SdkInternalThreadLocal.put(CONCURRENT_TRACE_ID_KEY, request.getXrayTraceId()); + } + + try { + ByteArrayOutputStream payload = lambdaRequestHandler.call(request); + runtimeClient.reportInvocationSuccess(request.getId(), payload.toByteArray()); + // clear interrupted flag in case if it was set by user's code + Thread.interrupted(); + } catch (Throwable t) { + UserFault.filterStackTrace(t); + userFault = UserFault.makeUserFault(t); + shouldExit = exitLoopOnErrors && (t instanceof VirtualMachineError || t instanceof IOError || userFault.fatal); + LambdaError error = createLambdaErrorFromThrowableOrUserFault(t); + runtimeClient.reportInvocationError(request.getId(), error); + } finally { + if (userFault != null) { + lambdaLogger.log(userFault.reportableError(), lambdaLogger.getLogFormat() == LogFormat.JSON ? LogLevel.ERROR : LogLevel.UNDEFINED); + } + + SdkInternalThreadLocal.remove(CONCURRENT_TRACE_ID_KEY); + } + } catch (Throwable t) { + if (exitLoopOnErrors || t instanceof LambdaRuntimeClientMaxRetriesExceededException) { + throw t; + } + + reportNonLoopTerminatingException(lambdaLogger, t); + } + } + } + + private static void onInitComplete(final LambdaContextLogger lambdaLogger, LambdaRuntimeApiClient runtimeClient) throws IOException { try { Core.getGlobalContext().beforeCheckpoint(null); runtimeClient.restoreNext(); @@ -303,4 +369,8 @@ private static void logExceptionCloudWatch(LambdaContextLogger lambdaLogger, Exc UserFault userFault = UserFault.makeUserFault(exc, true); lambdaLogger.log(userFault.reportableError(), lambdaLogger.getLogFormat() == LogFormat.JSON ? LogLevel.ERROR : LogLevel.UNDEFINED); } + + protected static URLClassLoader getCustomerClassLoader() { + return customerClassLoader; + } } diff --git a/aws-lambda-java-runtime-interface-client/src/main/java/com/amazonaws/services/lambda/runtime/api/client/EventHandlerLoader.java b/aws-lambda-java-runtime-interface-client/src/main/java/com/amazonaws/services/lambda/runtime/api/client/EventHandlerLoader.java index db6ceceb2..f679c217c 100644 --- a/aws-lambda-java-runtime-interface-client/src/main/java/com/amazonaws/services/lambda/runtime/api/client/EventHandlerLoader.java +++ b/aws-lambda-java-runtime-interface-client/src/main/java/com/amazonaws/services/lambda/runtime/api/client/EventHandlerLoader.java @@ -57,10 +57,10 @@ private enum Platform { UNKNOWN } - private static volatile PojoSerializer contextSerializer; - private static volatile PojoSerializer cognitoSerializer; + private static volatile ThreadLocal> contextSerializer = new ThreadLocal<>(); + private static volatile ThreadLocal> cognitoSerializer = new ThreadLocal<>(); - private static final EnumMap>> typeCache = new EnumMap<>(Platform.class); + private static final ThreadLocal>>> typeCache = ThreadLocal.withInitial(() -> new EnumMap<>(Platform.class)); private static final Comparator methodPriority = new Comparator() { public int compare(Method lhs, Method rhs) { @@ -116,7 +116,7 @@ private static PojoSerializer getSerializer(Platform platform, Type type if (type instanceof Class) { Class clazz = ((Class) type); if (LambdaEventSerializers.isLambdaSupportedEvent(clazz.getName())) { - return LambdaEventSerializers.serializerFor(clazz, AWSLambda.customerClassLoader); + return LambdaEventSerializers.serializerFor(clazz, AWSLambda.getCustomerClassLoader()); } } // else platform dependent (Android uses GSON but all other platforms use Jackson) @@ -127,10 +127,11 @@ private static PojoSerializer getSerializer(Platform platform, Type type } private static PojoSerializer getSerializerCached(Platform platform, Type type) { - Map> cache = typeCache.get(platform); + EnumMap>> threadTypeCache = typeCache.get(); + Map> cache = threadTypeCache.get(platform); if (cache == null) { cache = new HashMap<>(); - typeCache.put(platform, cache); + threadTypeCache.put(platform, cache); } PojoSerializer serializer = cache.get(type); @@ -143,17 +144,17 @@ private static PojoSerializer getSerializerCached(Platform platform, Typ } private static PojoSerializer getContextSerializer() { - if (contextSerializer == null) { - contextSerializer = GsonFactory.getInstance().getSerializer(LambdaClientContext.class); + if (contextSerializer.get() == null) { + contextSerializer.set(GsonFactory.getInstance().getSerializer(LambdaClientContext.class)); } - return contextSerializer; + return contextSerializer.get(); } private static PojoSerializer getCognitoSerializer() { - if (cognitoSerializer == null) { - cognitoSerializer = GsonFactory.getInstance().getSerializer(LambdaCognitoIdentity.class); + if (cognitoSerializer.get() == null) { + cognitoSerializer.set(GsonFactory.getInstance().getSerializer(LambdaCognitoIdentity.class)); } - return cognitoSerializer; + return cognitoSerializer.get(); } @@ -527,15 +528,14 @@ private static LambdaRequestHandler wrapPojoHandler(RequestHandler instance, Typ private static LambdaRequestHandler wrapRequestStreamHandler(final RequestStreamHandler handler) { return new LambdaRequestHandler() { - private final ByteArrayOutputStream output = new ByteArrayOutputStream(1024); - private Functions.V2 log4jContextPutMethod = null; + private final ThreadLocal outputBuffers = ThreadLocal.withInitial(() -> new ByteArrayOutputStream(1024)); + private ThreadLocal> log4jContextPutMethod = new ThreadLocal<>(); - private void safeAddRequestIdToLog4j(String log4jContextClassName, - InvocationRequest request, Class contextMapValueClass) { + private void safeAddRequestIdToLog4j(String log4jContextClassName, InvocationRequest request, Class contextMapValueClass) { try { - Class log4jContextClass = ReflectUtil.loadClass(AWSLambda.customerClassLoader, log4jContextClassName); - log4jContextPutMethod = ReflectUtil.loadStaticV2(log4jContextClass, "put", false, String.class, contextMapValueClass); - log4jContextPutMethod.call("AWSRequestId", request.getId()); + Class log4jContextClass = ReflectUtil.loadClass(AWSLambda.getCustomerClassLoader(), log4jContextClassName); + log4jContextPutMethod.set(ReflectUtil.loadStaticV2(log4jContextClass, "put", false, String.class, contextMapValueClass)); + log4jContextPutMethod.get().call("AWSRequestId", request.getId()); } catch (Exception e) { // nothing to do here } @@ -558,6 +558,7 @@ private void safeAddContextToLambdaLogger(LambdaContext context) { } public ByteArrayOutputStream call(InvocationRequest request) throws Error, Exception { + ByteArrayOutputStream output = outputBuffers.get(); output.reset(); LambdaCognitoIdentity cognitoIdentity = null; @@ -582,6 +583,7 @@ public ByteArrayOutputStream call(InvocationRequest request) throws Error, Excep LambdaEnvironment.FUNCTION_VERSION, request.getInvokedFunctionArn(), request.getTenantId(), + request.getXrayTraceId(), clientContext ); @@ -591,7 +593,7 @@ public ByteArrayOutputStream call(InvocationRequest request) throws Error, Excep safeAddRequestIdToLog4j("org.apache.log4j.MDC", request, Object.class); safeAddRequestIdToLog4j("org.apache.logging.log4j.ThreadContext", request, String.class); // if put method not assigned in either call to safeAddRequestIdtoLog4j then log4jContextPutMethod = null - if (log4jContextPutMethod == null) { + if (log4jContextPutMethod.get() == null) { System.err.println("Customer using log4j appender but unable to load either " + "org.apache.log4j.MDC or org.apache.logging.log4j.ThreadContext. " + "Customer cannot see RequestId in log4j log lines."); diff --git a/aws-lambda-java-runtime-interface-client/src/main/java/com/amazonaws/services/lambda/runtime/api/client/PojoSerializerLoader.java b/aws-lambda-java-runtime-interface-client/src/main/java/com/amazonaws/services/lambda/runtime/api/client/PojoSerializerLoader.java index daea5911f..da37f7ca7 100644 --- a/aws-lambda-java-runtime-interface-client/src/main/java/com/amazonaws/services/lambda/runtime/api/client/PojoSerializerLoader.java +++ b/aws-lambda-java-runtime-interface-client/src/main/java/com/amazonaws/services/lambda/runtime/api/client/PojoSerializerLoader.java @@ -28,7 +28,7 @@ private static CustomPojoSerializer loadSerializer() return customPojoSerializer; } - ServiceLoader loader = ServiceLoader.load(CustomPojoSerializer.class, AWSLambda.customerClassLoader); + ServiceLoader loader = ServiceLoader.load(CustomPojoSerializer.class, AWSLambda.getCustomerClassLoader()); Iterator serializers = loader.iterator(); if (!serializers.hasNext()) { diff --git a/aws-lambda-java-runtime-interface-client/src/main/java/com/amazonaws/services/lambda/runtime/api/client/ReservedRuntimeEnvironmentVariables.java b/aws-lambda-java-runtime-interface-client/src/main/java/com/amazonaws/services/lambda/runtime/api/client/ReservedRuntimeEnvironmentVariables.java index 7500a4943..9fdec6b9f 100644 --- a/aws-lambda-java-runtime-interface-client/src/main/java/com/amazonaws/services/lambda/runtime/api/client/ReservedRuntimeEnvironmentVariables.java +++ b/aws-lambda-java-runtime-interface-client/src/main/java/com/amazonaws/services/lambda/runtime/api/client/ReservedRuntimeEnvironmentVariables.java @@ -106,4 +106,10 @@ public interface ReservedRuntimeEnvironmentVariables { * The environment's time zone (UTC). The execution environment uses NTP to synchronize the system clock. */ String TZ = "TZ"; + + /* + * If set to a string parsable as an integer > 0, It enables multiconcurrency mode. + * Otherwise, if it is set to an invalid value, it will crash the whole RIC process. + */ + String AWS_LAMBDA_MAX_CONCURRENCY = "AWS_LAMBDA_MAX_CONCURRENCY"; } diff --git a/aws-lambda-java-runtime-interface-client/src/main/java/com/amazonaws/services/lambda/runtime/api/client/UserFault.java b/aws-lambda-java-runtime-interface-client/src/main/java/com/amazonaws/services/lambda/runtime/api/client/UserFault.java index c7c5c9ddf..7d8a50347 100644 --- a/aws-lambda-java-runtime-interface-client/src/main/java/com/amazonaws/services/lambda/runtime/api/client/UserFault.java +++ b/aws-lambda-java-runtime-interface-client/src/main/java/com/amazonaws/services/lambda/runtime/api/client/UserFault.java @@ -38,7 +38,7 @@ public UserFault(String msg, String exception, String trace, Boolean fatal) { * No more user code should run after a fault. */ public static UserFault makeUserFault(Throwable t) { - return makeUserFault(t, false); + return t instanceof UserFault ? (UserFault) t : makeUserFault(t, false); } public static UserFault makeUserFault(Throwable t, boolean fatal) { diff --git a/aws-lambda-java-runtime-interface-client/src/main/java/com/amazonaws/services/lambda/runtime/api/client/api/LambdaContext.java b/aws-lambda-java-runtime-interface-client/src/main/java/com/amazonaws/services/lambda/runtime/api/client/api/LambdaContext.java index bd1463db6..20b77262d 100644 --- a/aws-lambda-java-runtime-interface-client/src/main/java/com/amazonaws/services/lambda/runtime/api/client/api/LambdaContext.java +++ b/aws-lambda-java-runtime-interface-client/src/main/java/com/amazonaws/services/lambda/runtime/api/client/api/LambdaContext.java @@ -23,6 +23,7 @@ public class LambdaContext implements Context { private final CognitoIdentity cognitoIdentity; private final ClientContext clientContext; private final String tenantId; + private final String xrayTraceId; private final LambdaLogger logger; public LambdaContext( @@ -36,6 +37,7 @@ public LambdaContext( String functionVersion, String invokedFunctionArn, String tenantId, + String xrayTraceId, ClientContext clientContext ) { this.memoryLimit = memoryLimit; @@ -49,6 +51,7 @@ public LambdaContext( this.functionVersion = functionVersion; this.invokedFunctionArn = invokedFunctionArn; this.tenantId = tenantId; + this.xrayTraceId = xrayTraceId; this.logger = com.amazonaws.services.lambda.runtime.LambdaRuntime.getLogger(); } @@ -98,6 +101,10 @@ public String getTenantId() { return tenantId; } + public String getXrayTraceId() { + return xrayTraceId; + } + public LambdaLogger getLogger() { return logger; } diff --git a/aws-lambda-java-runtime-interface-client/src/main/java/com/amazonaws/services/lambda/runtime/api/client/logging/JsonLogFormatter.java b/aws-lambda-java-runtime-interface-client/src/main/java/com/amazonaws/services/lambda/runtime/api/client/logging/JsonLogFormatter.java index f463e7ee5..f1051a216 100644 --- a/aws-lambda-java-runtime-interface-client/src/main/java/com/amazonaws/services/lambda/runtime/api/client/logging/JsonLogFormatter.java +++ b/aws-lambda-java-runtime-interface-client/src/main/java/com/amazonaws/services/lambda/runtime/api/client/logging/JsonLogFormatter.java @@ -22,7 +22,7 @@ public class JsonLogFormatter implements LogFormatter { withZone(ZoneId.of("UTC")); private final PojoSerializer serializer = GsonFactory.getInstance().getSerializer(StructuredLogMessage.class); - private LambdaContext lambdaContext; + private ThreadLocal lambdaContext = new ThreadLocal<>(); @Override public String format(String message, LogLevel logLevel) { @@ -39,10 +39,12 @@ private StructuredLogMessage createLogMessage(String message, LogLevel logLevel) msg.message = message; msg.level = logLevel; - if (lambdaContext != null) { - msg.AWSRequestId = lambdaContext.getAwsRequestId(); - msg.tenantId = lambdaContext.getTenantId(); + LambdaContext lambdaContextForCurrentThread = lambdaContext.get(); + if (lambdaContextForCurrentThread != null) { + msg.AWSRequestId = lambdaContextForCurrentThread.getAwsRequestId(); + msg.tenantId = lambdaContextForCurrentThread.getTenantId(); } + return msg; } @@ -53,6 +55,10 @@ private StructuredLogMessage createLogMessage(String message, LogLevel logLevel) */ @Override public void setLambdaContext(LambdaContext context) { - this.lambdaContext = context; + if (context == null) { + lambdaContext.remove(); + } else { + lambdaContext.set(context); + } } } diff --git a/aws-lambda-java-runtime-interface-client/src/main/java/com/amazonaws/services/lambda/runtime/api/client/logging/StdOutLogSink.java b/aws-lambda-java-runtime-interface-client/src/main/java/com/amazonaws/services/lambda/runtime/api/client/logging/StdOutLogSink.java index 873e6fde5..90e7d39c2 100644 --- a/aws-lambda-java-runtime-interface-client/src/main/java/com/amazonaws/services/lambda/runtime/api/client/logging/StdOutLogSink.java +++ b/aws-lambda-java-runtime-interface-client/src/main/java/com/amazonaws/services/lambda/runtime/api/client/logging/StdOutLogSink.java @@ -15,7 +15,7 @@ public void log(byte[] message) { log(LogLevel.UNDEFINED, LogFormat.TEXT, message); } - public void log(LogLevel logLevel, LogFormat logFormat, byte[] message) { + public synchronized void log(LogLevel logLevel, LogFormat logFormat, byte[] message) { try { System.out.write(message); } catch (IOException e) { diff --git a/aws-lambda-java-runtime-interface-client/src/main/java/com/amazonaws/services/lambda/runtime/api/client/runtimeapi/LambdaRuntimeApiClient.java b/aws-lambda-java-runtime-interface-client/src/main/java/com/amazonaws/services/lambda/runtime/api/client/runtimeapi/LambdaRuntimeApiClient.java index e2ae0969a..a62aeb9b8 100644 --- a/aws-lambda-java-runtime-interface-client/src/main/java/com/amazonaws/services/lambda/runtime/api/client/runtimeapi/LambdaRuntimeApiClient.java +++ b/aws-lambda-java-runtime-interface-client/src/main/java/com/amazonaws/services/lambda/runtime/api/client/runtimeapi/LambdaRuntimeApiClient.java @@ -4,6 +4,7 @@ */ package com.amazonaws.services.lambda.runtime.api.client.runtimeapi; +import com.amazonaws.services.lambda.runtime.api.client.logging.LambdaContextLogger; import com.amazonaws.services.lambda.runtime.api.client.runtimeapi.dto.InvocationRequest; import java.io.IOException; @@ -24,6 +25,11 @@ public interface LambdaRuntimeApiClient { */ InvocationRequest nextInvocation() throws IOException; + /** + * Get next invocation with exponential backoff + */ + InvocationRequest nextInvocationWithExponentialBackoff(LambdaContextLogger lambdaLogger) throws Exception; + /** * Report invocation success * @param requestId request id diff --git a/aws-lambda-java-runtime-interface-client/src/main/java/com/amazonaws/services/lambda/runtime/api/client/runtimeapi/LambdaRuntimeApiClientImpl.java b/aws-lambda-java-runtime-interface-client/src/main/java/com/amazonaws/services/lambda/runtime/api/client/runtimeapi/LambdaRuntimeApiClientImpl.java index 65024b98e..caca69aa7 100644 --- a/aws-lambda-java-runtime-interface-client/src/main/java/com/amazonaws/services/lambda/runtime/api/client/runtimeapi/LambdaRuntimeApiClientImpl.java +++ b/aws-lambda-java-runtime-interface-client/src/main/java/com/amazonaws/services/lambda/runtime/api/client/runtimeapi/LambdaRuntimeApiClientImpl.java @@ -4,7 +4,11 @@ */ package com.amazonaws.services.lambda.runtime.api.client.runtimeapi; +import com.amazonaws.services.lambda.runtime.api.client.UserFault; +import com.amazonaws.services.lambda.runtime.api.client.logging.LambdaContextLogger; import com.amazonaws.services.lambda.runtime.api.client.runtimeapi.dto.InvocationRequest; +import com.amazonaws.services.lambda.runtime.logging.LogFormat; +import com.amazonaws.services.lambda.runtime.logging.LogLevel; import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; @@ -14,6 +18,8 @@ import java.util.HashMap; import java.util.Map; import java.util.Objects; +import java.util.function.Function; +import java.util.function.Supplier; import static java.net.HttpURLConnection.HTTP_ACCEPTED; import static java.net.HttpURLConnection.HTTP_OK; import static java.nio.charset.StandardCharsets.UTF_8; @@ -30,6 +36,11 @@ public class LambdaRuntimeApiClientImpl implements LambdaRuntimeApiClient { private static final String ERROR_TYPE_HEADER = "Lambda-Runtime-Function-Error-Type"; // 1MiB private static final int XRAY_ERROR_CAUSE_MAX_HEADER_SIZE = 1024 * 1024; + + // ~32 Seconds Max Backoff. + private static final long MAX_BACKOFF_PERIOD_MS = 1024 * 32; + private static final long INITIAL_BACKOFF_PERIOD_MS = 100; + private static final int MAX_NUMBER_OF_RETRIALS = 5; private final String baseUrl; private final String invocationEndpoint; @@ -52,6 +63,65 @@ public InvocationRequest nextInvocation() { return NativeClient.next(); } + /* + * Retry immediately then retry with exponential backoff. + */ + public static T getSupplierResultWithExponentialBackoff(LambdaContextLogger lambdaLogger, long initialDelayMS, long maxBackoffPeriodMS, int maxNumOfAttempts, Supplier supplier, Function exceptionMessageComposer, Exception maxRetriesException) throws Exception { + long delayMS = initialDelayMS; + for (int attempts = 0; attempts < maxNumOfAttempts; attempts++) { + boolean isFirstAttempt = attempts == 0; + boolean isLastAttempt = (attempts + 1) == maxNumOfAttempts; + + // Try and log whichever exceptions happened + try { + return supplier.get(); + } catch (Exception e) { + String logMessage = exceptionMessageComposer.apply(e); + if (!isLastAttempt) { + logMessage += String.format("\nRetrying%s", isFirstAttempt ? "." : String.format(" in %d ms.", delayMS)); + } + + lambdaLogger.log(logMessage, lambdaLogger.getLogFormat() == LogFormat.JSON ? LogLevel.ERROR : LogLevel.UNDEFINED); + } + + // throw if ran out of attempts. + if (isLastAttempt) { + throw maxRetriesException; + } + + // update the delay duration. + if (!isFirstAttempt) { + try { + Thread.sleep(delayMS); + delayMS = Math.min(delayMS * 2, maxBackoffPeriodMS); + } catch (InterruptedException e) { + Thread.interrupted(); + } + } + } + + // Should Not be reached. + throw new IllegalStateException(); + } + + @Override + public InvocationRequest nextInvocationWithExponentialBackoff(LambdaContextLogger lambdaLogger) throws Exception { + Supplier nextInvocationSupplier = () -> nextInvocation(); + Function exceptionMessageComposer = (e) -> { + return String.format("Runtime Loop on Thread ID: %s Failed to fetch next invocation.\n%s", Thread.currentThread().getName(), UserFault.trace(e)); + }; + + return getSupplierResultWithExponentialBackoff( + lambdaLogger, + INITIAL_BACKOFF_PERIOD_MS, + MAX_BACKOFF_PERIOD_MS, + MAX_NUMBER_OF_RETRIALS, + nextInvocationSupplier, + exceptionMessageComposer, + new LambdaRuntimeClientMaxRetriesExceededException("Get Next Invocation") + ); + } + @Override public void reportInvocationSuccess(String requestId, byte[] response) { NativeClient.postInvocationResponse(requestId.getBytes(UTF_8), response); diff --git a/aws-lambda-java-runtime-interface-client/src/main/java/com/amazonaws/services/lambda/runtime/api/client/runtimeapi/LambdaRuntimeClientMaxRetriesExceededException.java b/aws-lambda-java-runtime-interface-client/src/main/java/com/amazonaws/services/lambda/runtime/api/client/runtimeapi/LambdaRuntimeClientMaxRetriesExceededException.java new file mode 100644 index 000000000..467afa25c --- /dev/null +++ b/aws-lambda-java-runtime-interface-client/src/main/java/com/amazonaws/services/lambda/runtime/api/client/runtimeapi/LambdaRuntimeClientMaxRetriesExceededException.java @@ -0,0 +1,15 @@ +/* +Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +SPDX-License-Identifier: Apache-2.0 +*/ +package com.amazonaws.services.lambda.runtime.api.client.runtimeapi; + +public class LambdaRuntimeClientMaxRetriesExceededException extends LambdaRuntimeClientException { + // 429 is possible; however, that is more appropriate when a server is responding to a spamming client that it wants to rate limit. + // In Our case, however, the RIC is a client that is not able to get a response from an upstream server, so 500 is more appropriate. + public LambdaRuntimeClientMaxRetriesExceededException(String operationName) { + super("Maximum Number of retries have been exceed" + (operationName.equals(null) + ? String.format(" for the %s operation.", operationName) + : "."), 500); + } +} diff --git a/aws-lambda-java-runtime-interface-client/src/main/java/com/amazonaws/services/lambda/runtime/api/client/util/ConcurrencyConfig.java b/aws-lambda-java-runtime-interface-client/src/main/java/com/amazonaws/services/lambda/runtime/api/client/util/ConcurrencyConfig.java new file mode 100644 index 000000000..a768e240e --- /dev/null +++ b/aws-lambda-java-runtime-interface-client/src/main/java/com/amazonaws/services/lambda/runtime/api/client/util/ConcurrencyConfig.java @@ -0,0 +1,50 @@ +/* +Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +SPDX-License-Identifier: Apache-2.0 +*/ + +package com.amazonaws.services.lambda.runtime.api.client.util; + +import com.amazonaws.services.lambda.runtime.api.client.ReservedRuntimeEnvironmentVariables; +import com.amazonaws.services.lambda.runtime.api.client.UserFault; +import com.amazonaws.services.lambda.runtime.api.client.logging.LambdaContextLogger; +import com.amazonaws.services.lambda.runtime.logging.LogFormat; +import com.amazonaws.services.lambda.runtime.logging.LogLevel; + +public class ConcurrencyConfig { + private final int numberOfPlatformThreads; + private final String INVALID_CONFIG_MESSAGE_PREFIX = String.format("User configured %s is invalid.", ReservedRuntimeEnvironmentVariables.AWS_LAMBDA_MAX_CONCURRENCY); + + public ConcurrencyConfig(LambdaContextLogger logger) { + this(logger, new EnvReader()); + } + + public ConcurrencyConfig(LambdaContextLogger logger, EnvReader envReader) { + int readNumOfPlatformThreads = 0; + try { + String readLambdaMaxConcurrencyEnvVar = envReader.getEnv(ReservedRuntimeEnvironmentVariables.AWS_LAMBDA_MAX_CONCURRENCY); + + if (readLambdaMaxConcurrencyEnvVar != null) { + readNumOfPlatformThreads = Integer.parseInt(readLambdaMaxConcurrencyEnvVar); + } + } catch (Exception e) { + String message = String.format("%s\n%s", INVALID_CONFIG_MESSAGE_PREFIX, UserFault.trace(e)); + logger.log(message, logger.getLogFormat() == LogFormat.JSON ? LogLevel.ERROR : LogLevel.UNDEFINED); + throw e; + } + + this.numberOfPlatformThreads = readNumOfPlatformThreads; + } + + public String getConcurrencyConfigMessage() { + return String.format("Starting %d concurrent function handler threads.", this.numberOfPlatformThreads); + } + + public boolean isMultiConcurrent() { + return this.numberOfPlatformThreads >= 1; + } + + public int getNumberOfPlatformThreads() { + return numberOfPlatformThreads; + } +} diff --git a/aws-lambda-java-runtime-interface-client/src/main/jni/deps/aws-lambda-cpp-0.2.7/include/aws/lambda-runtime/runtime.h b/aws-lambda-java-runtime-interface-client/src/main/jni/deps/aws-lambda-cpp-0.2.7/include/aws/lambda-runtime/runtime.h index d7db5f183..c4868c1ba 100644 --- a/aws-lambda-java-runtime-interface-client/src/main/jni/deps/aws-lambda-cpp-0.2.7/include/aws/lambda-runtime/runtime.h +++ b/aws-lambda-java-runtime-interface-client/src/main/jni/deps/aws-lambda-cpp-0.2.7/include/aws/lambda-runtime/runtime.h @@ -172,7 +172,6 @@ class runtime { private: std::string const m_user_agent_header; std::array const m_endpoints; - CURL* const m_curl_handle; }; inline std::chrono::milliseconds invocation_request::get_time_remaining() const diff --git a/aws-lambda-java-runtime-interface-client/src/main/jni/deps/aws-lambda-cpp-0.2.7/src/runtime.cpp b/aws-lambda-java-runtime-interface-client/src/main/jni/deps/aws-lambda-cpp-0.2.7/src/runtime.cpp index eeaf0e7b9..84a84b439 100644 --- a/aws-lambda-java-runtime-interface-client/src/main/jni/deps/aws-lambda-cpp-0.2.7/src/runtime.cpp +++ b/aws-lambda-java-runtime-interface-client/src/main/jni/deps/aws-lambda-cpp-0.2.7/src/runtime.cpp @@ -41,6 +41,7 @@ static constexpr auto COGNITO_IDENTITY_HEADER = "lambda-runtime-cognito-identity static constexpr auto DEADLINE_MS_HEADER = "lambda-runtime-deadline-ms"; static constexpr auto FUNCTION_ARN_HEADER = "lambda-runtime-invoked-function-arn"; static constexpr auto TENANT_ID_HEADER = "lambda-runtime-aws-tenant-id"; +thread_local static CURL* m_curl_handle = curl_easy_init(); enum Endpoints { INIT, @@ -163,63 +164,62 @@ runtime::runtime(std::string const& endpoint) : runtime(endpoint, "AWS_Lambda_Cp runtime::runtime(std::string const& endpoint, std::string const& user_agent) : m_user_agent_header("User-Agent: " + user_agent), m_endpoints{{endpoint + "/2018-06-01/runtime/init/error", endpoint + "/2018-06-01/runtime/invocation/next", - endpoint + "/2018-06-01/runtime/invocation/"}}, - m_curl_handle(curl_easy_init()) + endpoint + "/2018-06-01/runtime/invocation/"}} { - if (!m_curl_handle) { + if (!lambda_runtime::m_curl_handle) { logging::log_error(LOG_TAG, "Failed to acquire curl easy handle for next."); } } runtime::~runtime() { - curl_easy_cleanup(m_curl_handle); + curl_easy_cleanup(lambda_runtime::m_curl_handle); } void runtime::set_curl_next_options() { // lambda freezes the container when no further tasks are available. The freezing period could be longer than the // request timeout, which causes the following get_next request to fail with a timeout error. - curl_easy_reset(m_curl_handle); - curl_easy_setopt(m_curl_handle, CURLOPT_TIMEOUT, 0L); - curl_easy_setopt(m_curl_handle, CURLOPT_CONNECTTIMEOUT, 1L); - curl_easy_setopt(m_curl_handle, CURLOPT_NOSIGNAL, 1L); - curl_easy_setopt(m_curl_handle, CURLOPT_TCP_NODELAY, 1L); - curl_easy_setopt(m_curl_handle, CURLOPT_HTTP_VERSION, CURL_HTTP_VERSION_1_1); + curl_easy_reset(lambda_runtime::m_curl_handle); + curl_easy_setopt(lambda_runtime::m_curl_handle, CURLOPT_TIMEOUT, 0L); + curl_easy_setopt(lambda_runtime::m_curl_handle, CURLOPT_CONNECTTIMEOUT, 1L); + curl_easy_setopt(lambda_runtime::m_curl_handle, CURLOPT_NOSIGNAL, 1L); + curl_easy_setopt(lambda_runtime::m_curl_handle, CURLOPT_TCP_NODELAY, 1L); + curl_easy_setopt(lambda_runtime::m_curl_handle, CURLOPT_HTTP_VERSION, CURL_HTTP_VERSION_1_1); - curl_easy_setopt(m_curl_handle, CURLOPT_HTTPGET, 1L); - curl_easy_setopt(m_curl_handle, CURLOPT_URL, m_endpoints[Endpoints::NEXT].c_str()); + curl_easy_setopt(lambda_runtime::m_curl_handle, CURLOPT_HTTPGET, 1L); + curl_easy_setopt(lambda_runtime::m_curl_handle, CURLOPT_URL, m_endpoints[Endpoints::NEXT].c_str()); - curl_easy_setopt(m_curl_handle, CURLOPT_WRITEFUNCTION, write_data); - curl_easy_setopt(m_curl_handle, CURLOPT_HEADERFUNCTION, write_header); + curl_easy_setopt(lambda_runtime::m_curl_handle, CURLOPT_WRITEFUNCTION, write_data); + curl_easy_setopt(lambda_runtime::m_curl_handle, CURLOPT_HEADERFUNCTION, write_header); - curl_easy_setopt(m_curl_handle, CURLOPT_PROXY, ""); + curl_easy_setopt(lambda_runtime::m_curl_handle, CURLOPT_PROXY, ""); #ifndef NDEBUG - curl_easy_setopt(m_curl_handle, CURLOPT_VERBOSE, 1); - curl_easy_setopt(m_curl_handle, CURLOPT_DEBUGFUNCTION, rt_curl_debug_callback); + curl_easy_setopt(lambda_runtime::m_curl_handle, CURLOPT_VERBOSE, 1); + curl_easy_setopt(lambda_runtime::m_curl_handle, CURLOPT_DEBUGFUNCTION, rt_curl_debug_callback); #endif } void runtime::set_curl_post_result_options() { - curl_easy_reset(m_curl_handle); - curl_easy_setopt(m_curl_handle, CURLOPT_TIMEOUT, 0L); - curl_easy_setopt(m_curl_handle, CURLOPT_CONNECTTIMEOUT, 1L); - curl_easy_setopt(m_curl_handle, CURLOPT_NOSIGNAL, 1L); - curl_easy_setopt(m_curl_handle, CURLOPT_TCP_NODELAY, 1L); - curl_easy_setopt(m_curl_handle, CURLOPT_HTTP_VERSION, CURL_HTTP_VERSION_1_1); + curl_easy_reset(lambda_runtime::m_curl_handle); + curl_easy_setopt(lambda_runtime::m_curl_handle, CURLOPT_TIMEOUT, 0L); + curl_easy_setopt(lambda_runtime::m_curl_handle, CURLOPT_CONNECTTIMEOUT, 1L); + curl_easy_setopt(lambda_runtime::m_curl_handle, CURLOPT_NOSIGNAL, 1L); + curl_easy_setopt(lambda_runtime::m_curl_handle, CURLOPT_TCP_NODELAY, 1L); + curl_easy_setopt(lambda_runtime::m_curl_handle, CURLOPT_HTTP_VERSION, CURL_HTTP_VERSION_1_1); - curl_easy_setopt(m_curl_handle, CURLOPT_POST, 1L); - curl_easy_setopt(m_curl_handle, CURLOPT_READFUNCTION, read_data); - curl_easy_setopt(m_curl_handle, CURLOPT_WRITEFUNCTION, write_data); - curl_easy_setopt(m_curl_handle, CURLOPT_HEADERFUNCTION, write_header); + curl_easy_setopt(lambda_runtime::m_curl_handle, CURLOPT_POST, 1L); + curl_easy_setopt(lambda_runtime::m_curl_handle, CURLOPT_READFUNCTION, read_data); + curl_easy_setopt(lambda_runtime::m_curl_handle, CURLOPT_WRITEFUNCTION, write_data); + curl_easy_setopt(lambda_runtime::m_curl_handle, CURLOPT_HEADERFUNCTION, write_header); - curl_easy_setopt(m_curl_handle, CURLOPT_PROXY, ""); + curl_easy_setopt(lambda_runtime::m_curl_handle, CURLOPT_PROXY, ""); #ifndef NDEBUG - curl_easy_setopt(m_curl_handle, CURLOPT_VERBOSE, 1); - curl_easy_setopt(m_curl_handle, CURLOPT_DEBUGFUNCTION, rt_curl_debug_callback); + curl_easy_setopt(lambda_runtime::m_curl_handle, CURLOPT_VERBOSE, 1); + curl_easy_setopt(lambda_runtime::m_curl_handle, CURLOPT_DEBUGFUNCTION, rt_curl_debug_callback); #endif } @@ -227,15 +227,15 @@ runtime::next_outcome runtime::get_next() { http::response resp; set_curl_next_options(); - curl_easy_setopt(m_curl_handle, CURLOPT_WRITEDATA, &resp); - curl_easy_setopt(m_curl_handle, CURLOPT_HEADERDATA, &resp); + curl_easy_setopt(lambda_runtime::m_curl_handle, CURLOPT_WRITEDATA, &resp); + curl_easy_setopt(lambda_runtime::m_curl_handle, CURLOPT_HEADERDATA, &resp); curl_slist* headers = nullptr; headers = curl_slist_append(headers, m_user_agent_header.c_str()); - curl_easy_setopt(m_curl_handle, CURLOPT_HTTPHEADER, headers); + curl_easy_setopt(lambda_runtime::m_curl_handle, CURLOPT_HTTPHEADER, headers); logging::log_debug(LOG_TAG, "Making request to %s", m_endpoints[Endpoints::NEXT].c_str()); - CURLcode curl_code = curl_easy_perform(m_curl_handle); + CURLcode curl_code = curl_easy_perform(lambda_runtime::m_curl_handle); logging::log_debug(LOG_TAG, "Completed request to %s", m_endpoints[Endpoints::NEXT].c_str()); curl_slist_free_all(headers); @@ -247,13 +247,13 @@ runtime::next_outcome runtime::get_next() { long resp_code; - curl_easy_getinfo(m_curl_handle, CURLINFO_RESPONSE_CODE, &resp_code); + curl_easy_getinfo(lambda_runtime::m_curl_handle, CURLINFO_RESPONSE_CODE, &resp_code); resp.set_response_code(static_cast(resp_code)); } { char* content_type = nullptr; - curl_easy_getinfo(m_curl_handle, CURLINFO_CONTENT_TYPE, &content_type); + curl_easy_getinfo(lambda_runtime::m_curl_handle, CURLINFO_CONTENT_TYPE, &content_type); resp.set_content_type(content_type); } @@ -327,7 +327,7 @@ runtime::post_outcome runtime::do_post( invocation_response const& handler_response) { set_curl_post_result_options(); - curl_easy_setopt(m_curl_handle, CURLOPT_URL, url.c_str()); + curl_easy_setopt(lambda_runtime::m_curl_handle, CURLOPT_URL, url.c_str()); logging::log_info(LOG_TAG, "Making request to %s", url.c_str()); curl_slist* headers = nullptr; @@ -348,11 +348,11 @@ runtime::post_outcome runtime::do_post( std::pair ctx{payload, 0}; aws::http::response resp; - curl_easy_setopt(m_curl_handle, CURLOPT_WRITEDATA, &resp); - curl_easy_setopt(m_curl_handle, CURLOPT_HEADERDATA, &resp); - curl_easy_setopt(m_curl_handle, CURLOPT_READDATA, &ctx); - curl_easy_setopt(m_curl_handle, CURLOPT_HTTPHEADER, headers); - CURLcode curl_code = curl_easy_perform(m_curl_handle); + curl_easy_setopt(lambda_runtime::m_curl_handle, CURLOPT_WRITEDATA, &resp); + curl_easy_setopt(lambda_runtime::m_curl_handle, CURLOPT_HEADERDATA, &resp); + curl_easy_setopt(lambda_runtime::m_curl_handle, CURLOPT_READDATA, &ctx); + curl_easy_setopt(lambda_runtime::m_curl_handle, CURLOPT_HTTPHEADER, headers); + CURLcode curl_code = curl_easy_perform(lambda_runtime::m_curl_handle); curl_slist_free_all(headers); if (curl_code != CURLE_OK) { @@ -366,11 +366,11 @@ runtime::post_outcome runtime::do_post( } long http_response_code; - curl_easy_getinfo(m_curl_handle, CURLINFO_RESPONSE_CODE, &http_response_code); + curl_easy_getinfo(lambda_runtime::m_curl_handle, CURLINFO_RESPONSE_CODE, &http_response_code); if (!is_success(aws::http::response_code(http_response_code))) { logging::log_error( - LOG_TAG, "Failed to post handler success response. Http response code: %ld.", http_response_code); + LOG_TAG, "Failed to post handler success response. Http response code: %ld. %s", http_response_code, resp.get_body().c_str()); return aws::http::response_code(http_response_code); } diff --git a/aws-lambda-java-runtime-interface-client/src/test/java/com/amazonaws/services/lambda/runtime/api/client/AWSLambdaTest.java b/aws-lambda-java-runtime-interface-client/src/test/java/com/amazonaws/services/lambda/runtime/api/client/AWSLambdaTest.java new file mode 100644 index 000000000..49b59c2cd --- /dev/null +++ b/aws-lambda-java-runtime-interface-client/src/test/java/com/amazonaws/services/lambda/runtime/api/client/AWSLambdaTest.java @@ -0,0 +1,578 @@ +/* +Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +SPDX-License-Identifier: Apache-2.0 +*/ + +package com.amazonaws.services.lambda.runtime.api.client; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertTrue; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyString; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.*; + +import java.io.ByteArrayOutputStream; +import java.io.IOError; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.HashSet; +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicInteger; +import java.util.concurrent.atomic.AtomicReference; + +import com.amazonaws.services.lambda.runtime.api.client.runtimeapi.LambdaRuntimeApiClientImpl; +import com.amazonaws.services.lambda.runtime.api.client.runtimeapi.LambdaRuntimeClientMaxRetriesExceededException; +import com.amazonaws.services.lambda.runtime.api.client.runtimeapi.dto.InvocationRequest; +import com.amazonaws.services.lambda.runtime.api.client.util.ConcurrencyConfig; +import com.amazonaws.services.lambda.runtime.api.client.util.EnvReader; +import com.amazonaws.services.lambda.runtime.Context; +import com.amazonaws.services.lambda.runtime.RequestHandler; +import com.amazonaws.services.lambda.runtime.api.client.logging.LambdaContextLogger; +import com.amazonaws.services.lambda.runtime.logging.LogFormat; +import com.amazonaws.services.lambda.runtime.logging.LogLevel; +import software.amazon.awssdk.utilslite.SdkInternalThreadLocal; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.Timeout; +import org.mockito.Mock; +import org.mockito.MockitoAnnotations; + +class AWSLambdaTest { + + private static final String CONCURRENT_TRACE_ID_KEY = "AWS_LAMBDA_X_TRACE_ID"; + + private static class SampleHandler implements RequestHandler, String> { + public static final String ADD_ENTRY_TO_MAP_ID_OP_MODE = "ADD_ENTRY_TO_MAP_ID"; + public static final String FAIL_IMMEDIATELY_OP_MODE = "FAIL_IMMEDIATELY"; + + public static final int nOfIterations = 10; + public static final int perIterationDelayMS = 10; + public static Map hashMap = new ConcurrentHashMap(); + public static AtomicInteger globalCounter = new AtomicInteger(); + + public static void resetStaticFields() { + hashMap.clear(); + globalCounter = new AtomicInteger(); + } + + private static void addEntryToMapImplementation(String name) { + int i = 0; + while (i++ < nOfIterations) { + hashMap.put(name, hashMap.getOrDefault(name, 0) + 1); + globalCounter.incrementAndGet(); + try { + Thread.sleep(perIterationDelayMS); + } catch (InterruptedException e) { + } + } + } + + @Override + public String handleRequest(Map event, Context context) { + // Thread.currentThread().getId() instead of Thread.currentThread().getName() when upgrading JAVA + String name = "Thread " + Thread.currentThread().getName(); + String opMode = event.get("id"); + + switch (opMode) { + case ADD_ENTRY_TO_MAP_ID_OP_MODE: + addEntryToMapImplementation(name); + break; + case FAIL_IMMEDIATELY_OP_MODE: + String[] sArr = {}; + return sArr[1]; + default: + break; + } + + return name; + } + } + + // Handler for testing SdkInternalThreadLocal trace ID functionality in concurrent scenarios + private static class SdkInternalThreadLocalTraceIdHandler implements RequestHandler, String> { + public static final String CAPTURE_TRACE_ID_OP_MODE = "CAPTURE_TRACE_ID"; + public static final int nOfIterations = 5; + public static final int perIterationDelayMS = 20; + public static CountDownLatch cdl = new CountDownLatch(1); + public static CountDownLatch readyLatch = null; + + public static Map capturedTraceIds = new ConcurrentHashMap<>(); + + public static void resetStaticFields() { + capturedTraceIds.clear(); + cdl = new CountDownLatch(1); + readyLatch = null; + } + + @Override + public String handleRequest(Map event, Context context) { + readyLatch.countDown(); + try { + cdl.await(); + } catch (InterruptedException e) { + e.printStackTrace(); + } + + String threadName = Thread.currentThread().getName(); + String opMode = event.get("id"); + + if (CAPTURE_TRACE_ID_OP_MODE.equals(opMode)) { + // Capture the SdkInternalThreadLocal trace ID for this thread + String traceId = SdkInternalThreadLocal.get(CONCURRENT_TRACE_ID_KEY); + if (traceId != null) { + capturedTraceIds.put(threadName, traceId); + } + + // Simulate some work with delays to ensure concurrent execution + for (int i = 0; i < nOfIterations; i++) { + try { + Thread.sleep(perIterationDelayMS); + // Re-check SdkInternalThreadLocal during processing to ensure it's consistent + String currentTraceId = SdkInternalThreadLocal.get(CONCURRENT_TRACE_ID_KEY); + if (currentTraceId != null && !currentTraceId.equals(traceId)) { + throw new RuntimeException("SdkInternalThreadLocal trace ID changed during processing!"); + } + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + break; + } + } + } + + return threadName; + } + } + + @Mock + private LambdaRuntimeApiClientImpl runtimeClient; + + @Mock + private LambdaContextLogger lambdaLogger; + + @Mock + private EnvReader envReader; + + @Mock + private ConcurrencyConfig concurrencyConfig; + + private LambdaRequestHandler lambdaRequestHandler = new LambdaRequestHandler() { + private SampleHandler sHandler = new SampleHandler(); + + @Override + public ByteArrayOutputStream call(InvocationRequest request) throws Error, Exception { + HashMap eventMap = new HashMap(); + eventMap.put("id", request.getId()); + String outStr = sHandler.handleRequest(eventMap, null); + ByteArrayOutputStream output = new ByteArrayOutputStream(); + output.write(outStr.getBytes()); + return output; + } + }; + + private LambdaRequestHandler SdkInternalThreadLocalRequestHandler = new LambdaRequestHandler() { + private SdkInternalThreadLocalTraceIdHandler SdkInternalThreadLocalHandler = new SdkInternalThreadLocalTraceIdHandler(); + + @Override + public ByteArrayOutputStream call(InvocationRequest request) throws Error, Exception { + HashMap eventMap = new HashMap<>(); + eventMap.put("id", request.getId()); + String outStr = SdkInternalThreadLocalHandler.handleRequest(eventMap, null); + ByteArrayOutputStream output = new ByteArrayOutputStream(); + output.write(outStr.getBytes()); + return output; + } + }; + + private static InvocationRequest getFakeInvocationRequest(String id) { + InvocationRequest request = new InvocationRequest(); + request.setId(id); + request.setDeadlineTimeInMs(Long.MAX_VALUE); + request.setContent("".getBytes()); + return request; + } + + private static InvocationRequest getFakeInvocationRequest(String id, String traceId) { + InvocationRequest request = getFakeInvocationRequest(id); + request.setXrayTraceId(traceId); + return request; + } + + private static final LambdaRuntimeClientMaxRetriesExceededException fakelambdaRuntimeClientMaxRetriesExceededException = new LambdaRuntimeClientMaxRetriesExceededException("Fake max retries happened"); + + @BeforeEach + void setUp() { + MockitoAnnotations.openMocks(this); + SampleHandler.resetStaticFields(); + } + + /* + * com.amazonaws.services.lambda.runtime.api.client.util.SampleHandler contains static fields. These fields are expected to be shared if initialization is behaving as expected. + * After execution of the Runtime loops, we should see that the SampleHandler.globalCounter has been acted on by all the threads. + * The concurrent hashmap in SampleHandler.hashMap should also have all the correct count of Threads that ran. + * IMPORTANT: This test fails through only timeout. + */ + @Test + @Timeout(value = 1, unit = TimeUnit.MINUTES) + void testConcurrentRunWithPlatformThreads() throws Throwable { + when(concurrencyConfig.isMultiConcurrent()).thenReturn(true); + when(concurrencyConfig.getNumberOfPlatformThreads()).thenReturn(4); + + InvocationRequest successfullInvocationRequest = getFakeInvocationRequest(SampleHandler.ADD_ENTRY_TO_MAP_ID_OP_MODE); + + when(runtimeClient.nextInvocationWithExponentialBackoff(lambdaLogger)) + .thenReturn(successfullInvocationRequest) + .thenReturn(successfullInvocationRequest) + .thenReturn(successfullInvocationRequest) + .thenReturn(successfullInvocationRequest) + .thenReturn(successfullInvocationRequest) + .thenReturn(successfullInvocationRequest) + .thenReturn(successfullInvocationRequest) + .thenThrow(fakelambdaRuntimeClientMaxRetriesExceededException) + .thenThrow(fakelambdaRuntimeClientMaxRetriesExceededException) + .thenThrow(fakelambdaRuntimeClientMaxRetriesExceededException) + .thenThrow(fakelambdaRuntimeClientMaxRetriesExceededException); + + AWSLambda.startRuntimeLoops(lambdaRequestHandler, lambdaLogger, concurrencyConfig, runtimeClient); + + // Success Reports Must Equal number of tasks that ran successfully. + verify(runtimeClient, times(7)).reportInvocationSuccess(eq(SampleHandler.ADD_ENTRY_TO_MAP_ID_OP_MODE), any()); + // Hashmap keys should equal the number of threads (runtime loops). + assertEquals(4, SampleHandler.hashMap.size()); + // Hashmap total count should equal all tasks that ran * number of iterations per task + assertEquals(7 * SampleHandler.nOfIterations, SampleHandler.globalCounter.get()); + } + + @Test + @Timeout(value = 1, unit = TimeUnit.MINUTES) + void testConcurrentRunWithPlatformThreadsWithFailures() throws Throwable { + when(lambdaLogger.getLogFormat()).thenReturn(LogFormat.JSON); + when(concurrencyConfig.isMultiConcurrent()).thenReturn(true); + when(concurrencyConfig.getNumberOfPlatformThreads()).thenReturn(4); + + InvocationRequest successfullInvocationRequest = getFakeInvocationRequest(SampleHandler.ADD_ENTRY_TO_MAP_ID_OP_MODE); + InvocationRequest failImmediatelyRequest = getFakeInvocationRequest(SampleHandler.FAIL_IMMEDIATELY_OP_MODE); + InvocationRequest userFaultRequest = mock(InvocationRequest.class); + final String UserFaultID = "Injected Fault Request ID"; + when(userFaultRequest.getId()).thenThrow(UserFault.makeUserFault(new Exception("OH NO"), true)).thenReturn(UserFaultID); + + when(runtimeClient.nextInvocationWithExponentialBackoff(lambdaLogger)) + .thenReturn(failImmediatelyRequest) + .thenReturn(userFaultRequest) + .thenReturn(successfullInvocationRequest) + .thenReturn(successfullInvocationRequest) + .thenThrow(fakelambdaRuntimeClientMaxRetriesExceededException) + .thenThrow(fakelambdaRuntimeClientMaxRetriesExceededException) + .thenThrow(fakelambdaRuntimeClientMaxRetriesExceededException) + .thenThrow(fakelambdaRuntimeClientMaxRetriesExceededException); + + AWSLambda.startRuntimeLoops(lambdaRequestHandler, lambdaLogger, concurrencyConfig, runtimeClient); + + // One for each of failImmediatelyRequest and userFaultRequest in finally block + // Four for crashing the Four runtime loops in the outermost catch of the runtime loop after the Null responses. + // 2 + 4 = 6 + verify(lambdaLogger, times(6)).log(anyString(), eq(LogLevel.ERROR)); + + // Failed invokes should be reported. + verify(runtimeClient).reportInvocationError(eq(SampleHandler.FAIL_IMMEDIATELY_OP_MODE), any()); + verify(runtimeClient).reportInvocationError(eq(UserFaultID), any()); + + // Success Reports Must Equal number of tasks that ran successfully. + verify(runtimeClient, times(2)).reportInvocationSuccess(eq(SampleHandler.ADD_ENTRY_TO_MAP_ID_OP_MODE), any()); + + // Hashmap keys should equal the minumum between(number of threads (runtime loops) AND number of tasks that ran successfully). + assertEquals(2, SampleHandler.hashMap.size()); + + // Hashmap total count should equal all tasks that ran * number of iterations per task + assertEquals(2 * SampleHandler.nOfIterations, SampleHandler.globalCounter.get()); + } + + @Test + @Timeout(value = 1, unit = TimeUnit.MINUTES) + void testConcurrentModeLoopDoesNotExitExceptForLambdaRuntimeClientMaxRetriesExceededException() throws Throwable { + when(lambdaLogger.getLogFormat()).thenReturn(LogFormat.JSON); + when(concurrencyConfig.isMultiConcurrent()).thenReturn(true); + when(concurrencyConfig.getNumberOfPlatformThreads()).thenReturn(1); + + InvocationRequest successfullInvocationRequest = getFakeInvocationRequest(SampleHandler.ADD_ENTRY_TO_MAP_ID_OP_MODE); + InvocationRequest failImmediatelyRequest = getFakeInvocationRequest(SampleHandler.FAIL_IMMEDIATELY_OP_MODE); + + InvocationRequest userFaultRequest = mock(InvocationRequest.class); // unrecoverable in sequential but recoverable in multiconcurrent mode. + final String UserFaultID = "Injected Fault Request ID"; + when(userFaultRequest.getId()).thenThrow(UserFault.makeUserFault(new Exception("OH NO"), true)).thenReturn(UserFaultID); + + InvocationRequest virtualMachineErrorRequest = mock(InvocationRequest.class); // unrecoverable in sequential but recoverable in multiconcurrent mode. + final String IOErrorID = "ioerr1"; + when(virtualMachineErrorRequest.getId()).thenThrow(UserFault.makeUserFault(new IOError(new Throwable()), true)).thenReturn(IOErrorID); + + when(runtimeClient.nextInvocationWithExponentialBackoff(lambdaLogger)) + .thenReturn(failImmediatelyRequest) + .thenReturn(userFaultRequest) + .thenReturn(virtualMachineErrorRequest) + .thenReturn(successfullInvocationRequest) + .thenReturn(successfullInvocationRequest) + .thenThrow(fakelambdaRuntimeClientMaxRetriesExceededException) + .thenReturn(successfullInvocationRequest); + + AWSLambda.startRuntimeLoops(lambdaRequestHandler, lambdaLogger, concurrencyConfig, runtimeClient); + + // One for each of failImmediatelyRequest, userFaultRequest, and virtualMachineErrorRequest + One for the runtime loop thread crashing. + verify(lambdaLogger, times(4)).log(anyString(), eq(LogLevel.ERROR)); + + // Failed invokes should be reported. + verify(runtimeClient).reportInvocationError(eq(SampleHandler.FAIL_IMMEDIATELY_OP_MODE), any()); + verify(runtimeClient).reportInvocationError(eq(UserFaultID), any()); + verify(runtimeClient).reportInvocationError(eq(IOErrorID), any()); + + // Success Reports Must Equal number of tasks that ran successfully. + verify(runtimeClient, times(2)).reportInvocationSuccess(eq(SampleHandler.ADD_ENTRY_TO_MAP_ID_OP_MODE), any()); + + // Hashmap keys should equal the minumum between(number of threads (runtime loops) AND number of tasks that ran successfully). + assertEquals(1, SampleHandler.hashMap.size()); + + // Hashmap total count should equal all tasks that ran * number of iterations per task + assertEquals(2 * SampleHandler.nOfIterations, SampleHandler.globalCounter.get()); + } + + /* + * + * SdkInternalThreadLocal XRAY TRACE ID TESTS + * + */ + + @Test + @Timeout(value = 1, unit = TimeUnit.MINUTES) + void testSdkInternalThreadLocalTraceIdIsInheritable() throws Throwable { + ExecutorService parentExecutorPool = Executors.newFixedThreadPool(1000); + CountDownLatch cdl = new CountDownLatch(1000); + CountDownLatch childCdl = new CountDownLatch(1000); + AtomicReference error = new AtomicReference<>(); + + for (int i = 0; i < 1000; i++) { + final int threadIndex = i; + parentExecutorPool.submit(() -> { + try { + String traceValue = "Val from parent thread" + threadIndex; + SdkInternalThreadLocal.put(CONCURRENT_TRACE_ID_KEY, traceValue); + + cdl.countDown(); + cdl.await(); + + assertEquals(SdkInternalThreadLocal.get(CONCURRENT_TRACE_ID_KEY), traceValue); + + ExecutorService internalExecutorPool = Executors.newFixedThreadPool(2); + internalExecutorPool.submit(() -> { + try { + assertEquals(SdkInternalThreadLocal.get(CONCURRENT_TRACE_ID_KEY), traceValue); + } catch (Throwable t) { + error.set(t); + } finally { + childCdl.countDown(); + } + }); + } catch (Throwable t) { + error.set(t); + childCdl.countDown(); + } + }); + } + + childCdl.await(); + if (error.get() != null) { + throw error.get(); + } + assertEquals(SdkInternalThreadLocal.get(CONCURRENT_TRACE_ID_KEY), null); + } + + @Test + @Timeout(value = 1, unit = TimeUnit.MINUTES) + void testSdkInternalThreadLocalTraceIdIsCleared() throws Throwable { + when(concurrencyConfig.isMultiConcurrent()).thenReturn(true); + when(concurrencyConfig.getNumberOfPlatformThreads()).thenReturn(1); + + InvocationRequest requestWithTrace = getFakeInvocationRequest("req_with_traceID", "test-trace-123"); + InvocationRequest requestWithNoTrace = getFakeInvocationRequest("req_without_traceID"); + + when(runtimeClient.nextInvocationWithExponentialBackoff(any())) + .thenReturn(requestWithTrace) + .thenReturn(requestWithNoTrace) + .thenThrow(fakelambdaRuntimeClientMaxRetriesExceededException); + + AtomicReference error = new AtomicReference<>(); + LambdaRequestHandler traceCheckingHandler = new LambdaRequestHandler() { + @Override + public ByteArrayOutputStream call(InvocationRequest request) throws Error, Exception { + try { + if (request.getId().equals("req_without_traceID")) { + assertEquals(null, SdkInternalThreadLocal.get(CONCURRENT_TRACE_ID_KEY)); + } + else { + assertEquals("test-trace-123", SdkInternalThreadLocal.get(CONCURRENT_TRACE_ID_KEY)); + } + } catch (Throwable t) { + error.set(t); + } + + return new ByteArrayOutputStream(); + } + }; + + AWSLambda.startRuntimeLoops(traceCheckingHandler, lambdaLogger, concurrencyConfig, runtimeClient); + + if (error.get() != null) { + throw error.get(); + } + } + + @Test + @Timeout(value = 1, unit = TimeUnit.MINUTES) + void testSdkInternalThreadLocalTraceIdInConcurrentMode() throws Throwable { + SdkInternalThreadLocalTraceIdHandler.resetStaticFields(); + + // Create invocation requests with different trace IDs + int numOfThreads = 1000; + HashSet traceIds = new HashSet<>(); + ArrayList requests = new ArrayList<>(); + for (int i = 0; i < numOfThreads - 1; i++) { + String randTId = java.util.UUID.randomUUID().toString(); + traceIds.add(randTId); + requests.add(getFakeInvocationRequest(SdkInternalThreadLocalTraceIdHandler.CAPTURE_TRACE_ID_OP_MODE, randTId)); + } + + // Test Nulls as well. + requests.add(getFakeInvocationRequest(SdkInternalThreadLocalTraceIdHandler.CAPTURE_TRACE_ID_OP_MODE, null)); + + when(concurrencyConfig.isMultiConcurrent()).thenReturn(true); + when(concurrencyConfig.getNumberOfPlatformThreads()).thenReturn(numOfThreads); + AtomicInteger iAtomic = new AtomicInteger(); + when(runtimeClient.nextInvocationWithExponentialBackoff(lambdaLogger)) + .thenAnswer((o) -> { + if (iAtomic.get() < numOfThreads) { + return requests.get(iAtomic.getAndIncrement()); + } else { + throw fakelambdaRuntimeClientMaxRetriesExceededException; + } + }); + + Thread thread = new Thread(() -> { try { + AWSLambda.startRuntimeLoops(SdkInternalThreadLocalRequestHandler, lambdaLogger, concurrencyConfig, runtimeClient); + } catch (Exception e) { + } }); + + SdkInternalThreadLocalTraceIdHandler.readyLatch = new CountDownLatch(numOfThreads); + thread.start(); + SdkInternalThreadLocalTraceIdHandler.readyLatch.await(); + SdkInternalThreadLocalTraceIdHandler.cdl.countDown(); + thread.join(); + + for (String traceId : SdkInternalThreadLocalTraceIdHandler.capturedTraceIds.values()) { + traceIds.remove(traceId); + } + + assertTrue(traceIds.isEmpty()); + } + + /* + * + * NON-CONCURRENT-MODE TESTS + * + */ + + @Test + @Timeout(value = 1, unit = TimeUnit.MINUTES) + void testSequentialWithFatalUserFaultErrorStopsLoop() throws Throwable { + when(lambdaLogger.getLogFormat()).thenReturn(LogFormat.JSON); + when(concurrencyConfig.isMultiConcurrent()).thenReturn(false); + + InvocationRequest successfullInvocationRequest = getFakeInvocationRequest(SampleHandler.ADD_ENTRY_TO_MAP_ID_OP_MODE); + InvocationRequest failImmediatelyRequest = getFakeInvocationRequest(SampleHandler.FAIL_IMMEDIATELY_OP_MODE); // recoverable error in all modes. + + InvocationRequest userFaultRequest = mock(InvocationRequest.class); // unrecoverable in sequential but recoverable in multiconcurrent mode. + final String UserFaultID = "Injected Fault Request ID"; + when(userFaultRequest.getId()).thenThrow(UserFault.makeUserFault(new Exception("OH NO"), true)).thenReturn(UserFaultID); + + InvocationRequest virtualMachineErrorRequest = mock(InvocationRequest.class); // unrecoverable in sequential but recoverable in multiconcurrent mode. + final String IOErrorID = "ioerr1"; + when(virtualMachineErrorRequest.getId()).thenThrow(UserFault.makeUserFault(new IOError(new Throwable()), true)).thenReturn(IOErrorID); + + when(runtimeClient.nextInvocation()) + .thenReturn(successfullInvocationRequest) + .thenReturn(successfullInvocationRequest) + .thenReturn(failImmediatelyRequest) + .thenReturn(userFaultRequest) + // these two should not be even feltched since userFaultRequest is not recoverable. + .thenReturn(successfullInvocationRequest) + .thenReturn(virtualMachineErrorRequest); + + AWSLambda.startRuntimeLoops(lambdaRequestHandler, lambdaLogger, concurrencyConfig, runtimeClient); + + // One for failImmediatelyRequest and userFaultRequest in finally block. + verify(lambdaLogger, times(2)).log(anyString(), eq(LogLevel.ERROR)); + + // Failed invokes should be reported. + verify(runtimeClient).reportInvocationError(eq(SampleHandler.FAIL_IMMEDIATELY_OP_MODE), any()); + verify(runtimeClient).reportInvocationError(eq(UserFaultID), any()); + + // Success Reports Must Equal number of tasks that ran successfully. And only 2 Error reports for failImmediatelyRequest and userFaultRequest. + verify(runtimeClient, times(2)).reportInvocationSuccess(eq(SampleHandler.ADD_ENTRY_TO_MAP_ID_OP_MODE), any()); + verify(runtimeClient, times(2)).reportInvocationError(any(), any()); + + // Hashmap keys should equal one as it is not multithreaded. + assertEquals(1, SampleHandler.hashMap.size()); + + // Hashmap total count should equal all tasks that ran * number of iterations per task + assertEquals(2 * SampleHandler.nOfIterations, SampleHandler.globalCounter.get()); + } + + @Test + @Timeout(value = 1, unit = TimeUnit.MINUTES) + void testSequentialWithVirtualMachineErrorStopsLoop() throws Throwable { + when(lambdaLogger.getLogFormat()).thenReturn(LogFormat.JSON); + when(concurrencyConfig.isMultiConcurrent()).thenReturn(false); + + InvocationRequest successfullInvocationRequest = getFakeInvocationRequest(SampleHandler.ADD_ENTRY_TO_MAP_ID_OP_MODE); + InvocationRequest failImmediatelyRequest = getFakeInvocationRequest(SampleHandler.FAIL_IMMEDIATELY_OP_MODE); // recoverable error in all modes. + + InvocationRequest userFaultRequest = mock(InvocationRequest.class); // unrecoverable in sequential but recoverable in multiconcurrent mode. + final String UserFaultID = "Injected Fault Request ID"; + when(userFaultRequest.getId()).thenThrow(UserFault.makeUserFault(new Exception("OH NO"), true)).thenReturn(UserFaultID); + + InvocationRequest virtualMachineErrorRequest = mock(InvocationRequest.class); // unrecoverable in sequential but recoverable in multiconcurrent mode. + final String IOErrorID = "ioerr1"; + when(virtualMachineErrorRequest.getId()).thenThrow(UserFault.makeUserFault(new IOError(new Throwable()), true)).thenReturn(IOErrorID); + + when(runtimeClient.nextInvocation()) + .thenReturn(successfullInvocationRequest) + .thenReturn(successfullInvocationRequest) + .thenReturn(failImmediatelyRequest) + .thenReturn(virtualMachineErrorRequest) + // these two should not be even feltched since userFaultRequest is not recoverable. + .thenReturn(successfullInvocationRequest) + .thenReturn(userFaultRequest); + + AWSLambda.startRuntimeLoops(lambdaRequestHandler, lambdaLogger, concurrencyConfig, runtimeClient); + + // One for failImmediatelyRequest and userFaultRequest in finally block. + verify(lambdaLogger, times(2)).log(anyString(), eq(LogLevel.ERROR)); + + // Failed invokes should be reported. + verify(runtimeClient).reportInvocationError(eq(SampleHandler.FAIL_IMMEDIATELY_OP_MODE), any()); + verify(runtimeClient).reportInvocationError(eq(IOErrorID), any()); + + // Success Reports Must Equal number of tasks that ran successfully. And only 2 Error reports for failImmediatelyRequest and virtualMachineErrorRequest. + verify(runtimeClient, times(2)).reportInvocationSuccess(eq(SampleHandler.ADD_ENTRY_TO_MAP_ID_OP_MODE), any()); + verify(runtimeClient, times(2)).reportInvocationError(any(), any()); + + // Hashmap keys should equal one as it is not multithreaded. + assertEquals(1, SampleHandler.hashMap.size()); + + // Hashmap total count should equal all tasks that ran * number of iterations per task + assertEquals(2 * SampleHandler.nOfIterations, SampleHandler.globalCounter.get()); + } +} \ No newline at end of file diff --git a/aws-lambda-java-runtime-interface-client/src/test/java/com/amazonaws/services/lambda/runtime/api/client/EventHandlerLoaderTest.java b/aws-lambda-java-runtime-interface-client/src/test/java/com/amazonaws/services/lambda/runtime/api/client/EventHandlerLoaderTest.java index 76e6f0249..aae2f1afe 100644 --- a/aws-lambda-java-runtime-interface-client/src/test/java/com/amazonaws/services/lambda/runtime/api/client/EventHandlerLoaderTest.java +++ b/aws-lambda-java-runtime-interface-client/src/test/java/com/amazonaws/services/lambda/runtime/api/client/EventHandlerLoaderTest.java @@ -4,8 +4,16 @@ import org.junit.jupiter.api.Test; import java.io.ByteArrayOutputStream; +import java.util.ArrayList; +import java.util.List; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; +import java.util.concurrent.Future; +import java.util.concurrent.TimeUnit; import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertTrue; class EventHandlerLoaderTest { @@ -37,7 +45,6 @@ void PojoHandlerTest_oneParamEvent() throws Exception { assertSuccessfulInvocation(lambdaRequestHandler); } - @Test void PojoHandlerTest_oneParamContext() throws Exception { String handler = "test.lambda.handlers.POJOHanlderImpl::oneParamHandler_context"; @@ -74,4 +81,72 @@ private static InvocationRequest getTestInvocationRequest() { invocationRequest.setXrayTraceId("traceId"); return invocationRequest; } -} \ No newline at end of file + + // Multithreaded test methods + + @Test + void RequestHandlerTest_Multithreaded() throws Exception { + testHandlerConcurrency("test.lambda.handlers.RequestHandlerImpl"); + } + + @Test + void RequestStreamHandlerTest_Multithreaded() throws Exception { + testHandlerConcurrency("test.lambda.handlers.RequestStreamHandlerImpl"); + } + + @Test + void PojoHandlerTest_noParams_Multithreaded() throws Exception { + testHandlerConcurrency("test.lambda.handlers.POJOHanlderImpl::noParamsHandler"); + } + + @Test + void PojoHandlerTest_oneParamEvent_Multithreaded() throws Exception { + testHandlerConcurrency("test.lambda.handlers.POJOHanlderImpl::oneParamHandler_event"); + } + + @Test + void PojoHandlerTest_oneParamContext_Multithreaded() throws Exception { + testHandlerConcurrency("test.lambda.handlers.POJOHanlderImpl::oneParamHandler_context"); + } + + @Test + void PojoHandlerTest_twoParams_Multithreaded() throws Exception { + testHandlerConcurrency("test.lambda.handlers.POJOHanlderImpl::twoParamsHandler"); + } + + private void testHandlerConcurrency(String handlerName) throws Exception { + // Create one handler instance + LambdaRequestHandler handler = getLambdaRequestHandler(handlerName); + + int threadCount = 10; + ExecutorService executor = Executors.newFixedThreadPool(threadCount); + List> futures = new ArrayList<>(); + CountDownLatch startLatch = new CountDownLatch(1); + + try { + for (int i = 0; i < threadCount; i++) { + futures.add(executor.submit(() -> { + try { + InvocationRequest request = getTestInvocationRequest(); + startLatch.await(); + ByteArrayOutputStream result = handler.call(request); + return result.toString(); + } catch (Exception e) { + throw new RuntimeException(e); + } + })); + } + + // Release all threads simultaneously and Verify all invocations return the expected result + startLatch.countDown(); + + for (Future future : futures) { + String result = future.get(5, TimeUnit.SECONDS); + assertEquals("\"success\"", result); + } + } finally { + executor.shutdown(); + assertTrue(executor.awaitTermination(10, TimeUnit.SECONDS)); + } + } +} diff --git a/aws-lambda-java-runtime-interface-client/src/test/java/com/amazonaws/services/lambda/runtime/api/client/PojoSerializerLoaderTest.java b/aws-lambda-java-runtime-interface-client/src/test/java/com/amazonaws/services/lambda/runtime/api/client/PojoSerializerLoaderTest.java index c2c887973..4ebcf5d7e 100644 --- a/aws-lambda-java-runtime-interface-client/src/test/java/com/amazonaws/services/lambda/runtime/api/client/PojoSerializerLoaderTest.java +++ b/aws-lambda-java-runtime-interface-client/src/test/java/com/amazonaws/services/lambda/runtime/api/client/PojoSerializerLoaderTest.java @@ -7,8 +7,8 @@ import com.amazonaws.services.lambda.runtime.CustomPojoSerializer; import com.amazonaws.services.lambda.runtime.serialization.PojoSerializer; -import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; import org.mockito.Mock; diff --git a/aws-lambda-java-runtime-interface-client/src/test/java/com/amazonaws/services/lambda/runtime/api/client/api/LambdaContextTest.java b/aws-lambda-java-runtime-interface-client/src/test/java/com/amazonaws/services/lambda/runtime/api/client/api/LambdaContextTest.java index 58880be43..f7da76198 100644 --- a/aws-lambda-java-runtime-interface-client/src/test/java/com/amazonaws/services/lambda/runtime/api/client/api/LambdaContextTest.java +++ b/aws-lambda-java-runtime-interface-client/src/test/java/com/amazonaws/services/lambda/runtime/api/client/api/LambdaContextTest.java @@ -19,6 +19,7 @@ public class LambdaContextTest { private static final LambdaClientContext CLIENT_CONTEXT = new LambdaClientContext(); public static final int MEMORY_LIMIT = 128; public static final String TENANT_ID = "tenant-id"; + public static final String X_RAY_TRACE_ID = "x-ray-trace-id"; @Test public void getRemainingTimeInMillis() { @@ -55,6 +56,6 @@ public void getRemainingTimeInMillis_Deadline() throws InterruptedException { private LambdaContext createContextWithDeadline(long deadlineTimeInMs) { return new LambdaContext(MEMORY_LIMIT, deadlineTimeInMs, REQUEST_ID, LOG_GROUP_NAME, LOG_STREAM_NAME, - FUNCTION_NAME, IDENTITY, FUNCTION_VERSION, INVOKED_FUNCTION_ARN, TENANT_ID, CLIENT_CONTEXT); + FUNCTION_NAME, IDENTITY, FUNCTION_VERSION, INVOKED_FUNCTION_ARN, TENANT_ID, X_RAY_TRACE_ID, CLIENT_CONTEXT); } } diff --git a/aws-lambda-java-runtime-interface-client/src/test/java/com/amazonaws/services/lambda/runtime/api/client/logging/AbstractLambdaLoggerTest.java b/aws-lambda-java-runtime-interface-client/src/test/java/com/amazonaws/services/lambda/runtime/api/client/logging/AbstractLambdaLoggerTest.java index baeb4c242..3a5ee8d5f 100644 --- a/aws-lambda-java-runtime-interface-client/src/test/java/com/amazonaws/services/lambda/runtime/api/client/logging/AbstractLambdaLoggerTest.java +++ b/aws-lambda-java-runtime-interface-client/src/test/java/com/amazonaws/services/lambda/runtime/api/client/logging/AbstractLambdaLoggerTest.java @@ -5,10 +5,15 @@ import com.amazonaws.services.lambda.runtime.logging.LogFormat; import org.junit.jupiter.api.Test; +import java.nio.charset.StandardCharsets; import java.util.LinkedList; import java.util.List; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; +import com.amazonaws.lambda.thirdparty.org.json.JSONObject; import com.amazonaws.services.lambda.runtime.LambdaLogger; +import com.amazonaws.services.lambda.runtime.api.client.api.LambdaContext; import com.amazonaws.services.lambda.runtime.logging.LogLevel; @@ -20,12 +25,12 @@ public TestSink() { } @Override - public void log(byte[] message) { + public synchronized void log(byte[] message) { messages.add(message); } @Override - public void log(LogLevel logLevel, LogFormat logFormat, byte[] message) { + public synchronized void log(LogLevel logLevel, LogFormat logFormat, byte[] message) { messages.add(message); } @@ -62,6 +67,45 @@ public void testLoggingNullValuesWithoutLogLevelInText() { assertEquals("null", new String(sink.getMessages().get(1))); } + /* + * Makes Sure Logging Contexts are thread local. + * We start `setLambdaContext` operations using the **single** shared `logger` object on a fixed thread pool, differentiating them with thread IDs. + * We then start concurrent `log` operations which are scheduled using that fixed pool. + * It is then verified that a given log operation, which logs the thread ID it is running on, used a context that had the same thread ID. + */ + @Test + public void testMultiConcurrentLoggingWithoutLogLevelInJSON() { + TestSink sink = new TestSink(); + LambdaContextLogger logger = new LambdaContextLogger(sink, LogLevel.INFO, LogFormat.JSON); + + String someMessagePrefix = "Some Message from "; + String reqIDPrefix = "Thread ID as request# "; + + final int nThreads = 5; + ExecutorService es = Executors.newFixedThreadPool(nThreads); + for (int i = 0; i < nThreads; i++) { + es.submit(() -> logger.setLambdaContext(new LambdaContext(Integer.MAX_VALUE, Long.MAX_VALUE, reqIDPrefix + Thread.currentThread().getName(), "", "", "", null, "", "", "", null, null))); + } + + final int nMessages = 100_000; + for (int i = 0; i < nMessages; i++) { + es.submit(() -> logger.log(someMessagePrefix + Thread.currentThread().getName())); + } + + es.shutdown(); + while (!es.isTerminated()) { + ; + } + + assertEquals(nMessages, sink.getMessages().size()); + for (byte[] message : sink.getMessages()) { + JSONObject parsedLog = new JSONObject(new String(message, StandardCharsets.UTF_8)); + String parsedMessage = parsedLog.getString("message"); + String parsedReqID = parsedLog.getString("AWSRequestId"); + assertEquals(parsedMessage.substring(someMessagePrefix.length()), parsedReqID.substring(reqIDPrefix.length())); + } + } + @Test public void testLoggingNullValuesWithoutLogLevelInJSON() { TestSink sink = new TestSink(); diff --git a/aws-lambda-java-runtime-interface-client/src/test/java/com/amazonaws/services/lambda/runtime/api/client/logging/JsonLogFormatterTest.java b/aws-lambda-java-runtime-interface-client/src/test/java/com/amazonaws/services/lambda/runtime/api/client/logging/JsonLogFormatterTest.java index 531e9ca94..91ce9d2a3 100644 --- a/aws-lambda-java-runtime-interface-client/src/test/java/com/amazonaws/services/lambda/runtime/api/client/logging/JsonLogFormatterTest.java +++ b/aws-lambda-java-runtime-interface-client/src/test/java/com/amazonaws/services/lambda/runtime/api/client/logging/JsonLogFormatterTest.java @@ -30,6 +30,7 @@ void testFormattingWithLambdaContext() { null, "function-arn", null, + null, null ); assertFormatsString("test log", LogLevel.WARN, context); @@ -48,6 +49,7 @@ void testFormattingWithTenantIdInLambdaContext() { null, "function-arn", "tenant-id", + "xray-trace-id", null ); assertFormatsString("test log", LogLevel.WARN, context); diff --git a/aws-lambda-java-runtime-interface-client/src/test/java/com/amazonaws/services/lambda/runtime/api/client/runtimeapi/LambdaRuntimeApiClientImplTest.java b/aws-lambda-java-runtime-interface-client/src/test/java/com/amazonaws/services/lambda/runtime/api/client/runtimeapi/LambdaRuntimeApiClientImplTest.java index 473e2aef3..710c1565e 100644 --- a/aws-lambda-java-runtime-interface-client/src/test/java/com/amazonaws/services/lambda/runtime/api/client/runtimeapi/LambdaRuntimeApiClientImplTest.java +++ b/aws-lambda-java-runtime-interface-client/src/test/java/com/amazonaws/services/lambda/runtime/api/client/runtimeapi/LambdaRuntimeApiClientImplTest.java @@ -14,6 +14,8 @@ import static org.junit.jupiter.api.Assertions.assertNull; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertThrows; + +import com.amazonaws.services.lambda.runtime.api.client.logging.LambdaContextLogger; import com.amazonaws.services.lambda.runtime.api.client.runtimeapi.dto.ErrorRequest; import com.amazonaws.services.lambda.runtime.api.client.runtimeapi.dto.InvocationRequest; import com.amazonaws.services.lambda.runtime.api.client.runtimeapi.dto.StackElement; @@ -22,8 +24,18 @@ import java.util.ArrayList; import java.util.List; +import java.util.function.Function; +import java.util.function.Supplier; import static org.junit.jupiter.api.Assertions.fail; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyString; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + import okhttp3.HttpUrl; import static java.net.HttpURLConnection.HTTP_ACCEPTED; import static java.net.HttpURLConnection.HTTP_OK; @@ -36,6 +48,14 @@ @DisabledOnOs(OS.MAC) public class LambdaRuntimeApiClientImplTest { + @SuppressWarnings("rawtypes") + private final Supplier mockSupplier = mock(Supplier.class); + @SuppressWarnings("rawtypes") + private final Function mockExceptionMessageComposer = mock(Function.class); + private final LambdaContextLogger mockLambdaContextLogger = mock(LambdaContextLogger.class); + private final LambdaRuntimeClientMaxRetriesExceededException retriesExceededException = new LambdaRuntimeClientMaxRetriesExceededException("Testing Invocations"); + final String fakeExceptionMessage = "Something bad"; + MockWebServer mockWebServer; LambdaRuntimeApiClientImpl lambdaRuntimeApiClientImpl; @@ -43,7 +63,7 @@ public class LambdaRuntimeApiClientImplTest { ErrorRequest errorRequest = new ErrorRequest("testErrorMessage", "testErrorType", errorStackStrace); String requestId = "1234"; - + @BeforeEach void setUp() { mockWebServer = new MockWebServer(); @@ -51,6 +71,67 @@ void setUp() { lambdaRuntimeApiClientImpl = new LambdaRuntimeApiClientImpl(hostnamePort); } + @SuppressWarnings("unchecked") + @Test + public void testgetSupplierResultWithExponentialBackoffAllFailing() throws Exception { + + when(mockSupplier.get()).thenThrow(new RuntimeException(new Exception(fakeExceptionMessage))); + when(mockExceptionMessageComposer.apply(any())).thenReturn(fakeExceptionMessage); + + try { + LambdaRuntimeApiClientImpl.getSupplierResultWithExponentialBackoff(mockLambdaContextLogger, 5, 200, 5, mockSupplier, mockExceptionMessageComposer, retriesExceededException); + } catch (LambdaRuntimeClientMaxRetriesExceededException e) { } + + verify(mockSupplier, times(5)).get(); + verify(mockLambdaContextLogger).log(eq(fakeExceptionMessage + "\nRetrying."), any()); + verify(mockLambdaContextLogger).log(eq(fakeExceptionMessage + "\nRetrying in 5 ms."), any()); + verify(mockLambdaContextLogger).log(eq(fakeExceptionMessage + "\nRetrying in 10 ms."), any()); + verify(mockLambdaContextLogger).log(eq(fakeExceptionMessage + "\nRetrying in 20 ms."), any()); + verify(mockLambdaContextLogger).log(eq(fakeExceptionMessage), any()); + verify(mockLambdaContextLogger, times(5)).log(anyString(), any()); + } + + @SuppressWarnings("unchecked") + @Test + public void testgetSupplierResultWithExponentialBackoffTwoFailingThenSuccess() throws Exception { + InvocationRequest fakeRequest = new InvocationRequest(); + + when(mockExceptionMessageComposer.apply(any())).thenReturn(fakeExceptionMessage); + + when(mockSupplier.get()) + .thenThrow(new RuntimeException(new Exception(fakeExceptionMessage))) + .thenThrow(new RuntimeException(new Exception(fakeExceptionMessage))) + .thenReturn(fakeRequest); + + InvocationRequest invocationRequest = (InvocationRequest) LambdaRuntimeApiClientImpl.getSupplierResultWithExponentialBackoff(mockLambdaContextLogger, 5, 200, 5, mockSupplier, mockExceptionMessageComposer, retriesExceededException); + + assertEquals(fakeRequest, invocationRequest); + verify(mockSupplier, times(3)).get(); + verify(mockLambdaContextLogger).log(eq(fakeExceptionMessage + "\nRetrying."), any()); + verify(mockLambdaContextLogger).log(eq(fakeExceptionMessage + "\nRetrying in 5 ms."), any()); + verify(mockLambdaContextLogger, times(2)).log(anyString(), any()); + } + + @SuppressWarnings("unchecked") + @Test + public void testgetSupplierResultWithExponentialBackoffDoesntGoAboveMax() throws Exception { + + when(mockSupplier.get()).thenThrow(new RuntimeException(new Exception(fakeExceptionMessage))); + + when(mockExceptionMessageComposer.apply(any())).thenReturn(fakeExceptionMessage); + + try { + LambdaRuntimeApiClientImpl.getSupplierResultWithExponentialBackoff(mockLambdaContextLogger, 100, 200, 5, mockSupplier, mockExceptionMessageComposer, retriesExceededException); + } catch (LambdaRuntimeClientMaxRetriesExceededException e) { } + + verify(mockSupplier, times(5)).get(); + verify(mockLambdaContextLogger).log(eq(fakeExceptionMessage + "\nRetrying."), any()); + verify(mockLambdaContextLogger).log(eq(fakeExceptionMessage + "\nRetrying in 100 ms."), any()); + verify(mockLambdaContextLogger, times(2)).log(eq(fakeExceptionMessage + "\nRetrying in 200 ms."), any()); + verify(mockLambdaContextLogger).log(eq(fakeExceptionMessage), any()); + verify(mockLambdaContextLogger, times(5)).log(anyString(), any()); + } + @Test public void reportInitErrorTest() { try { diff --git a/aws-lambda-java-runtime-interface-client/src/test/java/com/amazonaws/services/lambda/runtime/api/client/util/ConcurrencyConfigTest.java b/aws-lambda-java-runtime-interface-client/src/test/java/com/amazonaws/services/lambda/runtime/api/client/util/ConcurrencyConfigTest.java new file mode 100644 index 000000000..b1284e90c --- /dev/null +++ b/aws-lambda-java-runtime-interface-client/src/test/java/com/amazonaws/services/lambda/runtime/api/client/util/ConcurrencyConfigTest.java @@ -0,0 +1,90 @@ +/* +Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +SPDX-License-Identifier: Apache-2.0 +*/ + +package com.amazonaws.services.lambda.runtime.api.client.util; + +import com.amazonaws.services.lambda.runtime.api.client.ReservedRuntimeEnvironmentVariables; +import com.amazonaws.services.lambda.runtime.api.client.logging.LambdaContextLogger; +import com.amazonaws.services.lambda.runtime.logging.LogFormat; +import com.amazonaws.services.lambda.runtime.logging.LogLevel; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; + +import static org.junit.Assert.assertThrows; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.mockito.ArgumentMatchers.contains; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.*; + +@ExtendWith(MockitoExtension.class) +class ConcurrencyConfigTest { + @Mock + private LambdaContextLogger lambdaLogger; + + @Mock + private EnvReader envReader; + + private static final String exitingRuntimeString = String.format("User configured %s is invalid.", ReservedRuntimeEnvironmentVariables.AWS_LAMBDA_MAX_CONCURRENCY); + + @Test + void testDefaultConfiguration() { + when(envReader.getEnv(ReservedRuntimeEnvironmentVariables.AWS_LAMBDA_MAX_CONCURRENCY)).thenReturn(null); + + ConcurrencyConfig config = new ConcurrencyConfig(lambdaLogger, envReader); + verifyNoInteractions(lambdaLogger); + assertEquals(0, config.getNumberOfPlatformThreads()); + assertEquals(false, config.isMultiConcurrent()); + } + + @Test + void testMinValidPlatformThreadsConfig() { + when(envReader.getEnv(ReservedRuntimeEnvironmentVariables.AWS_LAMBDA_MAX_CONCURRENCY)).thenReturn("1"); + + ConcurrencyConfig config = new ConcurrencyConfig(lambdaLogger, envReader); + verifyNoInteractions(lambdaLogger); + assertEquals(1, config.getNumberOfPlatformThreads()); + assertEquals(true, config.isMultiConcurrent()); + } + + @Test + void testValidPlatformThreadsConfig() { + when(envReader.getEnv(ReservedRuntimeEnvironmentVariables.AWS_LAMBDA_MAX_CONCURRENCY)).thenReturn("4"); + + ConcurrencyConfig config = new ConcurrencyConfig(lambdaLogger, envReader); + verifyNoInteractions(lambdaLogger); + assertEquals(4, config.getNumberOfPlatformThreads()); + assertEquals(true, config.isMultiConcurrent()); + } + + @Test + void testInvalidPlatformThreadsConfig() { + when(lambdaLogger.getLogFormat()).thenReturn(LogFormat.JSON); + when(envReader.getEnv(ReservedRuntimeEnvironmentVariables.AWS_LAMBDA_MAX_CONCURRENCY)).thenReturn("invalid"); + + assertThrows(NumberFormatException.class, () -> new ConcurrencyConfig(lambdaLogger, envReader)); + verify(lambdaLogger).log(contains(exitingRuntimeString), eq(LogLevel.ERROR)); + } + + @Test + void testGetConcurrencyConfigMessage() { + when(envReader.getEnv(ReservedRuntimeEnvironmentVariables.AWS_LAMBDA_MAX_CONCURRENCY)).thenReturn("4"); + + ConcurrencyConfig config = new ConcurrencyConfig(lambdaLogger, envReader); + String expectedMessage = "Starting 4 concurrent function handler threads."; + verifyNoInteractions(lambdaLogger); + assertEquals(expectedMessage, config.getConcurrencyConfigMessage()); + assertEquals(true, config.isMultiConcurrent()); + } + + @Test + void testGetConcurrencyConfigWithNoConcurrency() { + ConcurrencyConfig config = new ConcurrencyConfig(lambdaLogger, envReader); + verifyNoInteractions(lambdaLogger); + assertEquals(0, config.getNumberOfPlatformThreads()); + assertEquals(false, config.isMultiConcurrent()); + } +} diff --git a/aws-lambda-java-runtime-interface-client/test-handlers/EchoHandler.java b/aws-lambda-java-runtime-interface-client/test-handlers/EchoHandler.java new file mode 100644 index 000000000..cb324e7f7 --- /dev/null +++ b/aws-lambda-java-runtime-interface-client/test-handlers/EchoHandler.java @@ -0,0 +1,20 @@ +import com.amazonaws.services.lambda.runtime.Context; +import com.amazonaws.services.lambda.runtime.RequestHandler; +import java.util.Map; +import java.util.HashMap; + +public class EchoHandler implements RequestHandler, Map> { + + @Override + public Map handleRequest(Map event, Context context) { + context.getLogger().log("Processing event: " + event); + + Map response = new HashMap<>(event); + response.put("timestamp", System.currentTimeMillis()); + response.put("requestId", context.getAwsRequestId()); + response.put("functionName", context.getFunctionName()); + response.put("remainingTimeInMillis", context.getRemainingTimeInMillis()); + + return response; + } +} \ No newline at end of file diff --git a/aws-lambda-java-runtime-interface-client/test/integration/codebuild/buildspec.os.alpine.yml b/aws-lambda-java-runtime-interface-client/test/integration/codebuild/buildspec.os.alpine.yml index cdc27a655..2a71cb1b0 100644 --- a/aws-lambda-java-runtime-interface-client/test/integration/codebuild/buildspec.os.alpine.yml +++ b/aws-lambda-java-runtime-interface-client/test/integration/codebuild/buildspec.os.alpine.yml @@ -43,6 +43,7 @@ phases: # Install events (dependency of serialization) - (cd aws-lambda-java-events && mvn install) # Install serialization (dependency of RIC) + - (cd aws-lambda-java-core && mvn install) - (cd aws-lambda-java-serialization && mvn install) - (cd aws-lambda-java-runtime-interface-client && mvn install -DargLineForReflectionTestOnly="") - (cd aws-lambda-java-runtime-interface-client/test/integration/test-handler && mvn install) diff --git a/aws-lambda-java-runtime-interface-client/test/integration/codebuild/buildspec.os.amazoncorretto.yml b/aws-lambda-java-runtime-interface-client/test/integration/codebuild/buildspec.os.amazoncorretto.yml index 67dd7617d..db8bf2ba0 100644 --- a/aws-lambda-java-runtime-interface-client/test/integration/codebuild/buildspec.os.amazoncorretto.yml +++ b/aws-lambda-java-runtime-interface-client/test/integration/codebuild/buildspec.os.amazoncorretto.yml @@ -42,6 +42,7 @@ phases: # Install events (dependency of serialization) - (cd aws-lambda-java-events && mvn install) # Install serialization (dependency of RIC) + - (cd aws-lambda-java-core && mvn install) - (cd aws-lambda-java-serialization && mvn install) - (cd aws-lambda-java-runtime-interface-client && mvn install -DargLineForReflectionTestOnly="") - (cd aws-lambda-java-runtime-interface-client/test/integration/test-handler && mvn install) diff --git a/aws-lambda-java-runtime-interface-client/test/integration/codebuild/buildspec.os.amazonlinux.1.yml b/aws-lambda-java-runtime-interface-client/test/integration/codebuild/buildspec.os.amazonlinux.1.yml index 04c486a88..e3773cf82 100644 --- a/aws-lambda-java-runtime-interface-client/test/integration/codebuild/buildspec.os.amazonlinux.1.yml +++ b/aws-lambda-java-runtime-interface-client/test/integration/codebuild/buildspec.os.amazonlinux.1.yml @@ -37,6 +37,7 @@ phases: # Install events (dependency of serialization) - (cd aws-lambda-java-events && mvn install) # Install serialization (dependency of RIC) + - (cd aws-lambda-java-core && mvn install) - (cd aws-lambda-java-serialization && mvn install) - (cd aws-lambda-java-runtime-interface-client && mvn install -DmultiArch=false -DargLineForReflectionTestOnly="") - (cd aws-lambda-java-runtime-interface-client/test/integration/test-handler && mvn install) diff --git a/aws-lambda-java-runtime-interface-client/test/integration/codebuild/buildspec.os.amazonlinux.2.yml b/aws-lambda-java-runtime-interface-client/test/integration/codebuild/buildspec.os.amazonlinux.2.yml index 8222bb41a..a9836fc6f 100644 --- a/aws-lambda-java-runtime-interface-client/test/integration/codebuild/buildspec.os.amazonlinux.2.yml +++ b/aws-lambda-java-runtime-interface-client/test/integration/codebuild/buildspec.os.amazonlinux.2.yml @@ -41,6 +41,7 @@ phases: # Install events (dependency of serialization) - (cd aws-lambda-java-events && mvn install) # Install serialization (dependency of RIC) + - (cd aws-lambda-java-core && mvn install) - (cd aws-lambda-java-serialization && mvn install) - (cd aws-lambda-java-runtime-interface-client && mvn install -DargLineForReflectionTestOnly="") - (cd aws-lambda-java-runtime-interface-client/test/integration/test-handler && mvn install) diff --git a/aws-lambda-java-runtime-interface-client/test/integration/codebuild/buildspec.os.centos.yml b/aws-lambda-java-runtime-interface-client/test/integration/codebuild/buildspec.os.centos.yml index d718c2647..74d12b01d 100644 --- a/aws-lambda-java-runtime-interface-client/test/integration/codebuild/buildspec.os.centos.yml +++ b/aws-lambda-java-runtime-interface-client/test/integration/codebuild/buildspec.os.centos.yml @@ -41,6 +41,7 @@ phases: # Install events (dependency of serialization) - (cd aws-lambda-java-events && mvn install) # Install serialization (dependency of RIC) + - (cd aws-lambda-java-core && mvn install) - (cd aws-lambda-java-serialization && mvn install) - (cd aws-lambda-java-runtime-interface-client && mvn install) - (cd aws-lambda-java-runtime-interface-client/test/integration/test-handler && mvn install) diff --git a/aws-lambda-java-runtime-interface-client/test/integration/codebuild/buildspec.os.debian.yml b/aws-lambda-java-runtime-interface-client/test/integration/codebuild/buildspec.os.debian.yml index d2772fbfc..222d14a36 100644 --- a/aws-lambda-java-runtime-interface-client/test/integration/codebuild/buildspec.os.debian.yml +++ b/aws-lambda-java-runtime-interface-client/test/integration/codebuild/buildspec.os.debian.yml @@ -42,6 +42,7 @@ phases: # Install events (dependency of serialization) - (cd aws-lambda-java-events && mvn install) # Install serialization (dependency of RIC) + - (cd aws-lambda-java-core && mvn install) - (cd aws-lambda-java-serialization && mvn install) - (cd aws-lambda-java-runtime-interface-client && mvn install) - (cd aws-lambda-java-runtime-interface-client/test/integration/test-handler && mvn install) diff --git a/aws-lambda-java-runtime-interface-client/test/integration/codebuild/buildspec.os.ubuntu.yml b/aws-lambda-java-runtime-interface-client/test/integration/codebuild/buildspec.os.ubuntu.yml index 2a90017b3..ce153c547 100644 --- a/aws-lambda-java-runtime-interface-client/test/integration/codebuild/buildspec.os.ubuntu.yml +++ b/aws-lambda-java-runtime-interface-client/test/integration/codebuild/buildspec.os.ubuntu.yml @@ -44,6 +44,7 @@ phases: # Install events (dependency of serialization) - (cd aws-lambda-java-events && mvn install) # Install serialization (dependency of RIC) + - (cd aws-lambda-java-core && mvn install) - (cd aws-lambda-java-serialization && mvn install) - (cd aws-lambda-java-runtime-interface-client && mvn install) - (cd aws-lambda-java-runtime-interface-client/test/integration/test-handler && mvn install) diff --git a/aws-lambda-java-runtime-interface-client/test/integration/test-handler/pom.xml b/aws-lambda-java-runtime-interface-client/test/integration/test-handler/pom.xml index 2e240fe34..64893528b 100644 --- a/aws-lambda-java-runtime-interface-client/test/integration/test-handler/pom.xml +++ b/aws-lambda-java-runtime-interface-client/test/integration/test-handler/pom.xml @@ -15,7 +15,7 @@ com.amazonaws aws-lambda-java-runtime-interface-client - 2.6.0 + 2.10.1 @@ -50,4 +50,3 @@ - diff --git a/aws-lambda-java-serialization/RELEASE.CHANGELOG.md b/aws-lambda-java-serialization/RELEASE.CHANGELOG.md index 5ca416845..d68d7b1fe 100644 --- a/aws-lambda-java-serialization/RELEASE.CHANGELOG.md +++ b/aws-lambda-java-serialization/RELEASE.CHANGELOG.md @@ -1,3 +1,24 @@ +### March 26, 2026 +`1.4.0`: +- Update `jackson-databind` dependency from 2.15.4 to 2.18.6 +- Replace deprecated `PropertyNamingStrategy.PascalCaseStrategy` with `PropertyNamingStrategies.UpperCamelCaseStrategy` +- The regression reported in 1.3.1 was a false positive caused by a CI workflow bug (`mvn package install` running the shade plugin twice, corrupting the artifact). Fixed by using `mvn install` instead. + +### March 19, 2026 +`1.3.1`: +- Revert `jackson-databind` dependency from 2.18.6 to 2.15.4 +- Revert `PropertyNamingStrategies.UpperCamelCaseStrategy` to `PropertyNamingStrategy.PascalCaseStrategy` +- Note: reverted due to a suspected regression in Joda DateTime deserialization; later confirmed to be a false positive (see 1.4.0) + +### March 11, 2026 +`1.3.0`: +- Update `jackson-databind` dependency from 2.15.4 to 2.18.6 +- Replace deprecated `PropertyNamingStrategy.PascalCaseStrategy` with `PropertyNamingStrategies.UpperCamelCaseStrategy` + +### December 16, 2025 +`1.2.0`: +- Update `jackson-databind` dependency from 2.14.2 to 2.15.4 + ### December 1, 2023 `1.1.5`: - Add support for DynamodbEvent.DynamodbStreamRecord serialization diff --git a/aws-lambda-java-serialization/pom.xml b/aws-lambda-java-serialization/pom.xml index 07ccecc8c..d412fd765 100644 --- a/aws-lambda-java-serialization/pom.xml +++ b/aws-lambda-java-serialization/pom.xml @@ -1,10 +1,10 @@ + xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd"> 4.0.0 com.amazonaws aws-lambda-java-serialization - 1.1.5 + 1.4.0 jar AWS Lambda Java Runtime Serialization @@ -32,7 +32,7 @@ 1.8 1.8 com.amazonaws.lambda.thirdparty - 2.14.2 + 2.18.6 2.10.1 20231013 7.3.2 @@ -169,14 +169,12 @@ - org.sonatype.plugins - nexus-staging-maven-plugin - 1.6.3 + org.sonatype.central + central-publishing-maven-plugin + 0.8.0 true - sonatype-nexus-staging - https://aws.oss.sonatype.org/ - false + central @@ -196,7 +194,7 @@ org.apache.maven.plugins maven-shade-plugin - 3.2.1 + 3.6.1 package diff --git a/aws-lambda-java-serialization/src/main/java/com/amazonaws/services/lambda/runtime/serialization/events/LambdaEventSerializers.java b/aws-lambda-java-serialization/src/main/java/com/amazonaws/services/lambda/runtime/serialization/events/LambdaEventSerializers.java index 4173211e1..533bdcd49 100644 --- a/aws-lambda-java-serialization/src/main/java/com/amazonaws/services/lambda/runtime/serialization/events/LambdaEventSerializers.java +++ b/aws-lambda-java-serialization/src/main/java/com/amazonaws/services/lambda/runtime/serialization/events/LambdaEventSerializers.java @@ -19,6 +19,7 @@ import com.amazonaws.services.lambda.runtime.serialization.PojoSerializer; import com.amazonaws.services.lambda.runtime.serialization.util.ReflectUtil; import com.amazonaws.services.lambda.runtime.serialization.util.SerializeUtil; +import com.fasterxml.jackson.databind.PropertyNamingStrategies; import com.fasterxml.jackson.databind.PropertyNamingStrategy; import com.amazonaws.services.lambda.runtime.serialization.events.modules.DateModule; import com.amazonaws.services.lambda.runtime.serialization.events.modules.DateTimeModule; @@ -39,47 +40,52 @@ * * Option 1 (Preferred): * 1. Add Class name to SUPPORTED_EVENTS - * 2. Add Mixin Class to com.amazonaws.services.lambda.runtime.serialization.events.mixins package (if needed) + * 2. Add Mixin Class to + * com.amazonaws.services.lambda.runtime.serialization.events.mixins package (if + * needed) * 3. Add entries to MIXIN_MAP for event class and sub classes (if needed) - * 4. Add entries to NESTED_CLASS_MAP for event class and sub classes (if needed) - * 5. Add entry to NAMING_STRATEGY_MAP (if needed i.e. Could be used in place of a mixin) + * 4. Add entries to NESTED_CLASS_MAP for event class and sub classes (if + * needed) + * 5. Add entry to NAMING_STRATEGY_MAP (if needed i.e. Could be used in place of + * a mixin) * * Option 2 (longer - for event models that do not work with Jackson or GSON): * 1. Add Class name to SUPPORTED_EVENTS - * 2. Add serializer (using org.json) to com.amazonaws.services.lambda.runtime.serialization.events.serializers + * 2. Add serializer (using org.json) to + * com.amazonaws.services.lambda.runtime.serialization.events.serializers * 3. Add class name and serializer to SERIALIZER_MAP */ public class LambdaEventSerializers { - /** - * list of supported events - */ - private static final List SUPPORTED_EVENTS = Stream.of( - "com.amazonaws.services.lambda.runtime.events.APIGatewayProxyRequestEvent", - "com.amazonaws.services.lambda.runtime.events.APIGatewayProxyResponseEvent", - "com.amazonaws.services.lambda.runtime.events.CloudFormationCustomResourceEvent", - "com.amazonaws.services.lambda.runtime.events.CloudFrontEvent", - "com.amazonaws.services.lambda.runtime.events.CloudWatchLogsEvent", - "com.amazonaws.services.lambda.runtime.events.CodeCommitEvent", - "com.amazonaws.services.lambda.runtime.events.CognitoEvent", - "com.amazonaws.services.lambda.runtime.events.ConfigEvent", - "com.amazonaws.services.lambda.runtime.events.ConnectEvent", - "com.amazonaws.services.lambda.runtime.events.DynamodbEvent", - "com.amazonaws.services.lambda.runtime.events.DynamodbTimeWindowEvent", - "com.amazonaws.services.lambda.runtime.events.IoTButtonEvent", - "com.amazonaws.services.lambda.runtime.events.KinesisEvent", - "com.amazonaws.services.lambda.runtime.events.KinesisTimeWindowEvent", - "com.amazonaws.services.lambda.runtime.events.KinesisFirehoseEvent", - "com.amazonaws.services.lambda.runtime.events.LambdaDestinationEvent", - "com.amazonaws.services.lambda.runtime.events.LexEvent", - "com.amazonaws.services.lambda.runtime.events.ScheduledEvent", - "com.amazonaws.services.lambda.runtime.events.SecretsManagerRotationEvent", - "com.amazonaws.services.s3.event.S3EventNotification", - "com.amazonaws.services.lambda.runtime.events.models.s3.S3EventNotification", - "com.amazonaws.services.lambda.runtime.events.S3Event", - "com.amazonaws.services.lambda.runtime.events.SNSEvent", - "com.amazonaws.services.lambda.runtime.events.SQSEvent") - .collect(Collectors.toList()); + /** + * list of supported events + */ + private static final List SUPPORTED_EVENTS = Stream.of( + "com.amazonaws.services.lambda.runtime.events.APIGatewayProxyRequestEvent", + "com.amazonaws.services.lambda.runtime.events.APIGatewayProxyResponseEvent", + "com.amazonaws.services.lambda.runtime.events.CloudFormationCustomResourceEvent", + "com.amazonaws.services.lambda.runtime.events.CloudFrontEvent", + "com.amazonaws.services.lambda.runtime.events.CloudWatchLogsEvent", + "com.amazonaws.services.lambda.runtime.events.CodeCommitEvent", + "com.amazonaws.services.lambda.runtime.events.CognitoEvent", + "com.amazonaws.services.lambda.runtime.events.ConfigEvent", + "com.amazonaws.services.lambda.runtime.events.ConnectEvent", + "com.amazonaws.services.lambda.runtime.events.DynamodbEvent", + "com.amazonaws.services.lambda.runtime.events.DynamodbTimeWindowEvent", + "com.amazonaws.services.lambda.runtime.events.IoTButtonEvent", + "com.amazonaws.services.lambda.runtime.events.KinesisEvent", + "com.amazonaws.services.lambda.runtime.events.KinesisTimeWindowEvent", + "com.amazonaws.services.lambda.runtime.events.KinesisFirehoseEvent", + "com.amazonaws.services.lambda.runtime.events.LambdaDestinationEvent", + "com.amazonaws.services.lambda.runtime.events.LexEvent", + "com.amazonaws.services.lambda.runtime.events.ScheduledEvent", + "com.amazonaws.services.lambda.runtime.events.SecretsManagerRotationEvent", + "com.amazonaws.services.s3.event.S3EventNotification", + "com.amazonaws.services.lambda.runtime.events.models.s3.S3EventNotification", + "com.amazonaws.services.lambda.runtime.events.S3Event", + "com.amazonaws.services.lambda.runtime.events.SNSEvent", + "com.amazonaws.services.lambda.runtime.events.SQSEvent") + .collect(Collectors.toList()); /** * list of events incompatible with Jackson, with serializers explicitly defined @@ -118,6 +124,7 @@ public class LambdaEventSerializers { ConnectEventMixin.ContactDataMixin.class), new SimpleEntry<>("com.amazonaws.services.lambda.runtime.events.ConnectEvent$CustomerEndpoint", ConnectEventMixin.CustomerEndpointMixin.class), + new SimpleEntry<>("com.amazonaws.services.lambda.runtime.events.ConnectEvent$Queue", ConnectEventMixin.QueueMixin.class), new SimpleEntry<>("com.amazonaws.services.lambda.runtime.events.ConnectEvent$SystemEndpoint", ConnectEventMixin.SystemEndpointMixin.class), new SimpleEntry<>("com.amazonaws.services.lambda.runtime.events.DynamodbEvent", @@ -170,6 +177,7 @@ public class LambdaEventSerializers { new NestedClass("com.amazonaws.services.lambda.runtime.events.ConnectEvent$Details"), new NestedClass("com.amazonaws.services.lambda.runtime.events.ConnectEvent$ContactData"), new NestedClass("com.amazonaws.services.lambda.runtime.events.ConnectEvent$CustomerEndpoint"), + new NestedClass("com.amazonaws.services.lambda.runtime.events.ConnectEvent$Queue"), new NestedClass("com.amazonaws.services.lambda.runtime.events.ConnectEvent$SystemEndpoint"))), new SimpleEntry<>("com.amazonaws.services.lambda.runtime.events.DynamodbEvent", Arrays.asList( @@ -214,99 +222,112 @@ public class LambdaEventSerializers { */ private static final Map NAMING_STRATEGY_MAP = Stream.of( new SimpleEntry<>("com.amazonaws.services.lambda.runtime.events.SNSEvent", - new PropertyNamingStrategy.PascalCaseStrategy())) + new PropertyNamingStrategies.UpperCamelCaseStrategy()), + new SimpleEntry<>("com.amazonaws.services.lambda.runtime.events.ConnectEvent$Queue", + new PropertyNamingStrategies.UpperCamelCaseStrategy()) + ) .collect(Collectors.toMap(SimpleEntry::getKey, SimpleEntry::getValue)); - /** - * Returns whether the class name is a Lambda supported event model. - * @param className class name as string - * @return whether the event model is supported - */ - public static boolean isLambdaSupportedEvent(String className) { - return SUPPORTED_EVENTS.contains(className); - } - - /** - * Return a serializer for the event class - * @return a specific PojoSerializer or modified JacksonFactory instance with mixins and modules added in - */ - @SuppressWarnings({"unchecked"}) - public static PojoSerializer serializerFor(Class eventClass, ClassLoader classLoader) { - // if serializer specifically defined for event then use that - if (SERIALIZER_MAP.containsKey(eventClass.getName())) { - return SERIALIZER_MAP.get(eventClass.getName()).withClass(eventClass).withClassLoader(classLoader); - } - // else use a Jackson ObjectMapper instance - JacksonFactory factory = JacksonFactory.getInstance(); - // if mixins required for class, then apply - if (MIXIN_MAP.containsKey(eventClass.getName())) { - factory = factory.withMixin(eventClass, MIXIN_MAP.get(eventClass.getName())); + /** + * Returns whether the class name is a Lambda supported event model. + * + * @param className class name as string + * @return whether the event model is supported + */ + public static boolean isLambdaSupportedEvent(String className) { + return SUPPORTED_EVENTS.contains(className); } - // if event model has nested classes then load those classes and check if mixins apply - if (NESTED_CLASS_MAP.containsKey(eventClass.getName())) { - List nestedClasses = NESTED_CLASS_MAP.get(eventClass.getName()); - for (NestedClass nestedClass: nestedClasses) { - // if mixin exists for nested class then apply - if (MIXIN_MAP.containsKey(nestedClass.className)) { - factory = tryLoadingNestedClass(classLoader, factory, nestedClass); + + /** + * Return a serializer for the event class + * + * @return a specific PojoSerializer or modified JacksonFactory instance with + * mixins and modules added in + */ + @SuppressWarnings({ "unchecked" }) + public static PojoSerializer serializerFor(Class eventClass, ClassLoader classLoader) { + // if serializer specifically defined for event then use that + if (SERIALIZER_MAP.containsKey(eventClass.getName())) { + return SERIALIZER_MAP.get(eventClass.getName()).withClass(eventClass) + .withClassLoader(classLoader); } - } - } - // load DateModules - factory.getMapper().registerModules(new DateModule(), new DateTimeModule(classLoader)); - // load naming strategy if needed - if (NAMING_STRATEGY_MAP.containsKey(eventClass.getName())) { - factory = factory.withNamingStrategy(NAMING_STRATEGY_MAP.get(eventClass.getName())); + // else use a Jackson ObjectMapper instance + JacksonFactory factory = JacksonFactory.getInstance(); + // if mixins required for class, then apply + if (MIXIN_MAP.containsKey(eventClass.getName())) { + factory = factory.withMixin(eventClass, MIXIN_MAP.get(eventClass.getName())); + } + // if event model has nested classes then load those classes and check if mixins + // apply + if (NESTED_CLASS_MAP.containsKey(eventClass.getName())) { + List nestedClasses = NESTED_CLASS_MAP.get(eventClass.getName()); + for (NestedClass nestedClass : nestedClasses) { + // if mixin exists for nested class then apply + if (MIXIN_MAP.containsKey(nestedClass.className)) { + factory = tryLoadingNestedClass(classLoader, factory, nestedClass); + } + } + } + // load DateModules + factory.getMapper().registerModules(new DateModule(), new DateTimeModule(classLoader)); + // load naming strategy if needed + if (NAMING_STRATEGY_MAP.containsKey(eventClass.getName())) { + factory = factory.withNamingStrategy(NAMING_STRATEGY_MAP.get(eventClass.getName())); + } + return factory.getSerializer(eventClass); } - return factory.getSerializer(eventClass); - } - /** - * Tries to load a nested class with its defined mixin from {@link #MIXIN_MAP} into the {@link JacksonFactory} object. - * Will allow initial failure for {@link AlternateNestedClass} objects and try again with their alternate class name - * @return a modified JacksonFactory instance with mixins added in - */ - private static JacksonFactory tryLoadingNestedClass(ClassLoader classLoader, JacksonFactory factory, NestedClass nestedClass) { - Class eventClazz; - Class mixinClazz; - try { - eventClazz = SerializeUtil.loadCustomerClass(nestedClass.getClassName(), classLoader); - mixinClazz = MIXIN_MAP.get(nestedClass.getClassName()); - } catch (ReflectUtil.ReflectException e) { - if (nestedClass instanceof AlternateNestedClass) { - AlternateNestedClass alternateNestedClass = (AlternateNestedClass) nestedClass; - eventClazz = SerializeUtil.loadCustomerClass(alternateNestedClass.getAlternateClassName(), classLoader); - mixinClazz = MIXIN_MAP.get(alternateNestedClass.getAlternateClassName()); - } else { - throw e; - } - } + /** + * Tries to load a nested class with its defined mixin from {@link #MIXIN_MAP} + * into the {@link JacksonFactory} object. + * Will allow initial failure for {@link AlternateNestedClass} objects and try + * again with their alternate class name + * + * @return a modified JacksonFactory instance with mixins added in + */ + private static JacksonFactory tryLoadingNestedClass(ClassLoader classLoader, JacksonFactory factory, + NestedClass nestedClass) { + Class eventClazz; + Class mixinClazz; + try { + eventClazz = SerializeUtil.loadCustomerClass(nestedClass.getClassName(), classLoader); + mixinClazz = MIXIN_MAP.get(nestedClass.getClassName()); + } catch (ReflectUtil.ReflectException e) { + if (nestedClass instanceof AlternateNestedClass) { + AlternateNestedClass alternateNestedClass = (AlternateNestedClass) nestedClass; + eventClazz = SerializeUtil.loadCustomerClass( + alternateNestedClass.getAlternateClassName(), classLoader); + mixinClazz = MIXIN_MAP.get(alternateNestedClass.getAlternateClassName()); + } else { + throw e; + } + } - return factory.withMixin(eventClazz, mixinClazz); - } + return factory.withMixin(eventClazz, mixinClazz); + } - private static class NestedClass { - private final String className; + private static class NestedClass { + private final String className; - protected NestedClass(String className) { - this.className = className; - } + protected NestedClass(String className) { + this.className = className; + } - protected String getClassName() { - return className; + protected String getClassName() { + return className; + } } - } - private static class AlternateNestedClass extends NestedClass { - private final String alternateClassName; + private static class AlternateNestedClass extends NestedClass { + private final String alternateClassName; - private AlternateNestedClass(String className, String alternateClassName) { - super(className); - this.alternateClassName = alternateClassName; - } + private AlternateNestedClass(String className, String alternateClassName) { + super(className); + this.alternateClassName = alternateClassName; + } - private String getAlternateClassName() { - return alternateClassName; + private String getAlternateClassName() { + return alternateClassName; + } } - } } diff --git a/aws-lambda-java-serialization/src/main/java/com/amazonaws/services/lambda/runtime/serialization/events/mixins/ConnectEventMixin.java b/aws-lambda-java-serialization/src/main/java/com/amazonaws/services/lambda/runtime/serialization/events/mixins/ConnectEventMixin.java index 529a33b39..1645fdaee 100644 --- a/aws-lambda-java-serialization/src/main/java/com/amazonaws/services/lambda/runtime/serialization/events/mixins/ConnectEventMixin.java +++ b/aws-lambda-java-serialization/src/main/java/com/amazonaws/services/lambda/runtime/serialization/events/mixins/ConnectEventMixin.java @@ -65,8 +65,8 @@ public abstract class ContactDataMixin { @JsonProperty("PreviousContactId") abstract void setPreviousContactId(String previousContactId); // needed because Jackson expects "queue" instead of "Queue" - @JsonProperty("Queue") abstract String getQueue(); - @JsonProperty("Queue") abstract void setQueue(String queue); + @JsonProperty("Queue") abstract Map getQueue(); + @JsonProperty("Queue") abstract void setQueue(Map queue); // needed because Jackson expects "systemEndpoint" instead of "SystemEndpoint" @JsonProperty("SystemEndpoint") abstract Map getSystemEndpoint(); @@ -95,4 +95,9 @@ public abstract class SystemEndpointMixin { @JsonProperty("Type") abstract String getType(); @JsonProperty("Type") abstract void setType(String type); } + + public abstract class QueueMixin { + @JsonProperty("Name") abstract String getName(); + @JsonProperty("Name") abstract void setName(String name); + } } diff --git a/aws-lambda-java-serialization/src/main/java/com/amazonaws/services/lambda/runtime/serialization/events/modules/DateModule.java b/aws-lambda-java-serialization/src/main/java/com/amazonaws/services/lambda/runtime/serialization/events/modules/DateModule.java index 8a6954e34..acc8bde2a 100644 --- a/aws-lambda-java-serialization/src/main/java/com/amazonaws/services/lambda/runtime/serialization/events/modules/DateModule.java +++ b/aws-lambda-java-serialization/src/main/java/com/amazonaws/services/lambda/runtime/serialization/events/modules/DateModule.java @@ -15,10 +15,17 @@ import com.fasterxml.jackson.databind.module.SimpleModule; /** - * The AWS API represents a date as a double, which specifies the fractional - * number of seconds since the epoch. Java's Date, however, represents a date as - * a long, which specifies the number of milliseconds since the epoch. This - * class is used to translate between these two formats. + * The AWS API represents a date as a double (fractional seconds since epoch). + * Java's Date uses a long (milliseconds since epoch). This module translates + * between the two formats. + * + *

+ * Round-trip caveats: The serializer always writes via + * {@link JsonGenerator#writeNumber(double)}, so integer epochs + * (e.g. {@code 1428537600}) round-trip as decimal ({@code 1.4285376E9}). + * Sub-millisecond precision is lost because {@link java.util.Date} + * has milliseconds precision. + *

* * This class is copied from LambdaEventBridgeservice * com.amazon.aws.lambda.stream.ddb.DateModule diff --git a/aws-lambda-java-tests/RELEASE.CHANGELOG.md b/aws-lambda-java-tests/RELEASE.CHANGELOG.md index 76965b8fd..0b4bd2510 100644 --- a/aws-lambda-java-tests/RELEASE.CHANGELOG.md +++ b/aws-lambda-java-tests/RELEASE.CHANGELOG.md @@ -1,3 +1,9 @@ +### March 27, 2026 +`1.1.3`: +- Add serialization round-trip tests covering 66 event classes +- Bumped `aws-lambda-java-serialization` to version `1.4.0` (Jackson `2.15.x` → `2.18.6`) +- Bumped `aws-lambda-java-events` to version `3.16.1` + ### August 26, 2021 `1.1.1`: - Bumped `aws-lambda-java-events` to version `3.11.0` diff --git a/aws-lambda-java-tests/pom.xml b/aws-lambda-java-tests/pom.xml index 0ab074cce..e63e529a2 100644 --- a/aws-lambda-java-tests/pom.xml +++ b/aws-lambda-java-tests/pom.xml @@ -1,10 +1,11 @@ + + xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd"> 4.0.0 com.amazonaws aws-lambda-java-tests - 1.1.1 + 1.1.3 jar AWS Lambda Java Tests @@ -32,20 +33,29 @@ 1.8 1.8 UTF-8 + 5.9.2 0.8.7 + 1.4.0 + 3.16.1 + 3.18.0 + 3.27.7 com.amazonaws aws-lambda-java-serialization - 1.1.5 + ${aws-lambda-java-serialization.version} com.amazonaws aws-lambda-java-events - 3.16.0 + ${aws-lambda-java-events.version} org.junit.jupiter @@ -65,13 +75,13 @@ org.apache.commons commons-lang3 - 3.12.0 + ${commons-lang3.version} org.assertj assertj-core - 3.24.2 + ${assertj-core.version} test @@ -220,14 +230,12 @@
- org.sonatype.plugins - nexus-staging-maven-plugin - 1.6.3 + org.sonatype.central + central-publishing-maven-plugin + 0.8.0 true - sonatype-nexus-staging - https://aws.oss.sonatype.org/ - false + central @@ -240,7 +248,7 @@ org.apache.maven.plugins maven-compiler-plugin - 3.8.1 + 3.11.0 ${maven.compiler.source} ${maven.compiler.target} @@ -251,7 +259,10 @@ org.apache.maven.plugins maven-surefire-plugin 2.22.2 + + true + -
+ \ No newline at end of file diff --git a/aws-lambda-java-tests/src/main/java/com/amazonaws/services/lambda/runtime/tests/JsonNodeUtils.java b/aws-lambda-java-tests/src/main/java/com/amazonaws/services/lambda/runtime/tests/JsonNodeUtils.java new file mode 100644 index 000000000..f9f4e1eb6 --- /dev/null +++ b/aws-lambda-java-tests/src/main/java/com/amazonaws/services/lambda/runtime/tests/JsonNodeUtils.java @@ -0,0 +1,110 @@ +package com.amazonaws.services.lambda.runtime.tests; + +import com.amazonaws.lambda.thirdparty.com.fasterxml.jackson.databind.JsonNode; +import com.amazonaws.lambda.thirdparty.com.fasterxml.jackson.databind.node.ObjectNode; +import java.util.Iterator; +import java.util.List; +import java.util.TreeSet; +import java.util.regex.Pattern; + +import org.joda.time.DateTime; + +/** + * Utility methods for working with shaded Jackson {@link JsonNode} trees. + * + *

+ * Package-private — not part of the public API. + *

+ */ +class JsonNodeUtils { + + private static final Pattern ISO_DATE_REGEX = Pattern.compile("\\d{4}-\\d{2}-\\d{2}T.+"); + + private JsonNodeUtils() { + } + + /** + * Recursively removes all fields whose value is {@code null} from the + * tree. This mirrors the serializer's {@code Include.NON_NULL} behaviour + * so that explicit nulls in the fixture don't cause false-positive diffs. + */ + static JsonNode stripNulls(JsonNode node) { + if (node.isObject()) { + ObjectNode obj = (ObjectNode) node; + Iterator fieldNames = obj.fieldNames(); + while (fieldNames.hasNext()) { + String field = fieldNames.next(); + if (obj.get(field).isNull()) { + fieldNames.remove(); + } else { + stripNulls(obj.get(field)); + } + } + } else if (node.isArray()) { + for (JsonNode element : node) { + stripNulls(element); + } + } + return node; + } + + /** + * Recursively walks both trees and collects human-readable diff lines. + */ + static void diffNodes(String path, JsonNode expected, JsonNode actual, List diffs) { + if (expected.equals(actual)) + return; + + // Compares two datetime strings by parsed instant, because DateTimeModule + // normalizes the format on serialization (e.g. "+0000" → "Z", "Z" → ".000Z") + if (areSameDateTime(expected.textValue(), actual.textValue())) { + return; + } + + if (expected.isObject() && actual.isObject()) { + TreeSet allKeys = new TreeSet<>(); + expected.fieldNames().forEachRemaining(allKeys::add); + actual.fieldNames().forEachRemaining(allKeys::add); + for (String key : allKeys) { + diffChild(path + "." + key, expected.get(key), actual.get(key), diffs); + } + } else if (expected.isArray() && actual.isArray()) { + for (int i = 0; i < Math.max(expected.size(), actual.size()); i++) { + diffChild(path + "[" + i + "]", expected.get(i), actual.get(i), diffs); + } + } else { + diffs.add("CHANGED " + path + " : " + summarize(expected) + " -> " + summarize(actual)); + } + } + + /** + * Compares two strings by parsed instant when both look like ISO-8601 dates, + * because DateTimeModule normalizes format on serialization + * (e.g. "+0000" → "Z", "Z" → ".000Z"). + */ + private static boolean areSameDateTime(String expected, String actual) { + if (expected == null || actual == null + || !ISO_DATE_REGEX.matcher(expected).matches() + || !ISO_DATE_REGEX.matcher(actual).matches()) { + return false; + } + return DateTime.parse(expected).equals(DateTime.parse(actual)); + } + + private static void diffChild(String path, JsonNode expected, JsonNode actual, List diffs) { + if (expected == null) + diffs.add("ADDED " + path + " = " + summarize(actual)); + else if (actual == null) + diffs.add("MISSING " + path + " (was " + summarize(expected) + ")"); + else + diffNodes(path, expected, actual, diffs); + } + + private static String summarize(JsonNode node) { + if (node == null) { + return ""; + } + String text = node.toString(); + return text.length() > 80 ? text.substring(0, 77) + "..." : text; + } +} diff --git a/aws-lambda-java-tests/src/main/java/com/amazonaws/services/lambda/runtime/tests/LambdaEventAssert.java b/aws-lambda-java-tests/src/main/java/com/amazonaws/services/lambda/runtime/tests/LambdaEventAssert.java new file mode 100644 index 000000000..f8d7e106d --- /dev/null +++ b/aws-lambda-java-tests/src/main/java/com/amazonaws/services/lambda/runtime/tests/LambdaEventAssert.java @@ -0,0 +1,142 @@ +package com.amazonaws.services.lambda.runtime.tests; + +import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.io.InputStream; +import java.io.UncheckedIOException; +import com.amazonaws.services.lambda.runtime.serialization.PojoSerializer; +import com.amazonaws.services.lambda.runtime.serialization.events.LambdaEventSerializers; +import com.amazonaws.lambda.thirdparty.com.fasterxml.jackson.databind.JsonNode; +import com.amazonaws.lambda.thirdparty.com.fasterxml.jackson.databind.ObjectMapper; + +import java.util.ArrayList; +import java.util.List; + +/** + * Framework-agnostic assertion utilities for verifying Lambda event + * serialization. + * + *

+ * When opentest4j is on the classpath (e.g. JUnit 5.x / JUnit Platform), + * assertion failures are reported as + * {@code org.opentest4j.AssertionFailedError} + * which enables rich diff support in IDEs. Otherwise, falls back to plain + * {@link AssertionError}. + *

+ * + */ +public class LambdaEventAssert { + + private static final ObjectMapper MAPPER = new ObjectMapper(); + + /** + * Round-trip using the registered {@link LambdaEventSerializers} path + * (Jackson + mixins + DateModule + DateTimeModule + naming strategies). + * + *

+ * The check performs two consecutive round-trips + * (JSON → POJO → JSON → POJO → JSON) and compares the + * original JSON tree against the final output tree. A single structural + * comparison catches both: + *

+ *
    + *
  • Fields silently dropped during deserialization
  • + *
  • Non-idempotent serialization (output changes across round-trips)
  • + *
+ * + * @param fileName classpath resource name (must end with {@code .json}) + * @param targetClass the event class to deserialize into + * @throws AssertionError if the original and final JSON trees differ + */ + public static void assertSerializationRoundTrip(String fileName, Class targetClass) { + PojoSerializer serializer = LambdaEventSerializers.serializerFor(targetClass, + ClassLoader.getSystemClassLoader()); + + if (!fileName.endsWith(".json")) { + throw new IllegalArgumentException("File " + fileName + " must have json extension"); + } + + byte[] originalBytes; + try (InputStream stream = Thread.currentThread().getContextClassLoader().getResourceAsStream(fileName)) { + if (stream == null) { + throw new IllegalArgumentException("Could not load resource '" + fileName + "' from classpath"); + } + originalBytes = toBytes(stream); + } catch (IOException e) { + throw new UncheckedIOException("Failed to read resource " + fileName, e); + } + + // Two round-trips: original → POJO → JSON → POJO → JSON + // We are doing 2 passes so we can check instability problems + // like UnstablePojo in LambdaEventAssertTest + ByteArrayOutputStream firstOutput = roundTrip(new ByteArrayInputStream(originalBytes), serializer); + ByteArrayOutputStream secondOutput = roundTrip( + new ByteArrayInputStream(firstOutput.toByteArray()), serializer); + + // Compare original tree against final tree. + // Strip explicit nulls from the original because the serializer is + // configured with Include.NON_NULL — null fields are intentionally + // omitted and that is not a data-loss bug. + try { + JsonNode originalTree = JsonNodeUtils.stripNulls(MAPPER.readTree(originalBytes)); + JsonNode finalTree = MAPPER.readTree(secondOutput.toByteArray()); + + if (!originalTree.equals(finalTree)) { + List diffs = new ArrayList<>(); + JsonNodeUtils.diffNodes("", originalTree, finalTree, diffs); + + if (!diffs.isEmpty()) { + StringBuilder msg = new StringBuilder(); + msg.append("Serialization round-trip failure for ") + .append(targetClass.getSimpleName()) + .append(" (").append(diffs.size()).append(" difference(s)):\n"); + for (String diff : diffs) { + msg.append(" ").append(diff).append('\n'); + } + + String expected = MAPPER.writerWithDefaultPrettyPrinter().writeValueAsString(originalTree); + String actual = MAPPER.writerWithDefaultPrettyPrinter().writeValueAsString(finalTree); + throw buildAssertionError(msg.toString(), expected, actual); + } + } + } catch (IOException e) { + throw new UncheckedIOException("Failed to parse JSON for tree comparison", e); + } + } + + private static ByteArrayOutputStream roundTrip(InputStream stream, PojoSerializer serializer) { + T event = serializer.fromJson(stream); + ByteArrayOutputStream outputStream = new ByteArrayOutputStream(); + serializer.toJson(event, outputStream); + return outputStream; + } + + private static byte[] toBytes(InputStream stream) throws IOException { + ByteArrayOutputStream buffer = new ByteArrayOutputStream(); + byte[] chunk = new byte[4096]; + int n; + while ((n = stream.read(chunk)) != -1) { + buffer.write(chunk, 0, n); + } + return buffer.toByteArray(); + } + + /** + * Tries to create an opentest4j AssertionFailedError for rich IDE diff + * support. Falls back to plain AssertionError if opentest4j is not on + * the classpath. + */ + private static AssertionError buildAssertionError(String message, String expected, String actual) { + try { + // opentest4j is provided by JUnit Platform (5.x) and enables + // IDE diff viewers to show expected vs actual side-by-side. + Class cls = Class.forName("org.opentest4j.AssertionFailedError"); + return (AssertionError) cls + .getConstructor(String.class, Object.class, Object.class) + .newInstance(message, expected, actual); + } catch (ReflectiveOperationException e) { + return new AssertionError(message + "\nExpected:\n" + expected + "\nActual:\n" + actual); + } + } +} diff --git a/aws-lambda-java-tests/src/test/java/com/amazonaws/services/lambda/runtime/tests/EventLoaderTest.java b/aws-lambda-java-tests/src/test/java/com/amazonaws/services/lambda/runtime/tests/EventLoaderTest.java index 86ad73228..43030bbca 100644 --- a/aws-lambda-java-tests/src/test/java/com/amazonaws/services/lambda/runtime/tests/EventLoaderTest.java +++ b/aws-lambda-java-tests/src/test/java/com/amazonaws/services/lambda/runtime/tests/EventLoaderTest.java @@ -81,6 +81,12 @@ public void testLoadAPIGatewayV2CustomAuthorizerEvent() { assertThat(event).isNotNull(); assertThat(event.getRequestContext().getHttp().getMethod()).isEqualTo("POST"); + // getTime() converts the raw string "12/Mar/2020:19:03:58 +0000" into a DateTime object; + // Jackson then serializes it as ISO-8601 "2020-03-12T19:03:58.000Z" + assertThat(event.getRequestContext().getTime().toInstant().getMillis()) + .isEqualTo(DateTime.parse("2020-03-12T19:03:58.000Z").toInstant().getMillis()); + // getTimeEpoch() converts the raw long into an Instant; + // Jackson then serializes it as a decimal seconds value assertThat(event.getRequestContext().getTimeEpoch()).isEqualTo(Instant.ofEpochMilli(1583348638390L)); } @@ -136,6 +142,9 @@ public void testLoadLexEvent() { assertThat(event.getCurrentIntent().getName()).isEqualTo("BookHotel"); assertThat(event.getCurrentIntent().getSlots()).hasSize(4); assertThat(event.getBot().getName()).isEqualTo("BookTrip"); + // Jackson leniently coerces the JSON number for "Nights" into a String + // because slots is typed as Map + assertThat(event.getCurrentIntent().getSlots().get("Nights")).isInstanceOf(String.class); } @Test @@ -159,6 +168,10 @@ public void testLoadMSKFirehoseEvent() { assertThat(event.getRecords().get(0).getKafkaRecordValue().array()).asString().isEqualTo("{\"Name\":\"Hello World\"}"); assertThat(event.getRecords().get(0).getApproximateArrivalTimestamp()).asString().isEqualTo("1716369573887"); assertThat(event.getRecords().get(0).getMskRecordMetadata()).asString().isEqualTo("{offset=0, partitionId=1, approximateArrivalTimestamp=1716369573887}"); + // Jackson leniently coerces the JSON number in mskRecordMetadata into a String + // because the map is typed as Map + Map metadata = event.getRecords().get(0).getMskRecordMetadata(); + assertThat(metadata.get("approximateArrivalTimestamp")).isInstanceOf(String.class); } @Test @@ -333,6 +346,14 @@ public void testLoadConnectEvent() { assertThat(contactData.getSystemEndpoint()) .returns("+21234567890",from(ConnectEvent.SystemEndpoint::getAddress)) .returns("TELEPHONE_NUMBER",from(ConnectEvent.SystemEndpoint::getType)); + + assertThat(contactData.getQueue()) + .isNotNull() + .returns("SampleQueue", from(ConnectEvent.Queue::getName)) + .returns("arn:aws:connect:eu-central-1:123456789012:instance/9308c2a1-9bc6-4cea-8290-6c0b4a6d38fa", + from(ConnectEvent.Queue::getARN) + ); + } @Test @@ -400,6 +421,8 @@ public void testLoadRabbitMQEvent() { .returns("AIDACKCEVSQ6C2EXAMPLE", from(RabbitMQEvent.BasicProperties::getUserId)) .returns(80, from(RabbitMQEvent.BasicProperties::getBodySize)) .returns("Jan 1, 1970, 12:33:41 AM", from(RabbitMQEvent.BasicProperties::getTimestamp)); + // Jackson leniently coerces the JSON string "60000" for expiration into int + // because the model field is typed as int Map headers = basicProperties.getHeaders(); assertThat(headers).hasSize(3); diff --git a/aws-lambda-java-tests/src/test/java/com/amazonaws/services/lambda/runtime/tests/JsonNodeUtilsTest.java b/aws-lambda-java-tests/src/test/java/com/amazonaws/services/lambda/runtime/tests/JsonNodeUtilsTest.java new file mode 100644 index 000000000..ec5798dca --- /dev/null +++ b/aws-lambda-java-tests/src/test/java/com/amazonaws/services/lambda/runtime/tests/JsonNodeUtilsTest.java @@ -0,0 +1,155 @@ +/* Copyright 2020 Amazon.com, Inc. or its affiliates. All Rights Reserved. */ +package com.amazonaws.services.lambda.runtime.tests; + +import com.amazonaws.lambda.thirdparty.com.fasterxml.jackson.databind.JsonNode; +import com.amazonaws.lambda.thirdparty.com.fasterxml.jackson.databind.ObjectMapper; +import org.junit.jupiter.api.Test; + +import java.util.ArrayList; +import java.util.List; + +import static org.junit.jupiter.api.Assertions.*; + +class JsonNodeUtilsTest { + + private static final ObjectMapper MAPPER = new ObjectMapper(); + + // --- stripNulls --- + + @Test + void stripNulls_removesTopLevelNulls() throws Exception { + JsonNode node = MAPPER.readTree("{\"a\":1,\"b\":null,\"c\":\"hello\"}"); + JsonNode result = JsonNodeUtils.stripNulls(node); + assertEquals(MAPPER.readTree("{\"a\":1,\"c\":\"hello\"}"), result); + } + + @Test + void stripNulls_removesNestedNulls() throws Exception { + JsonNode node = MAPPER.readTree("{\"outer\":{\"keep\":true,\"drop\":null}}"); + JsonNode result = JsonNodeUtils.stripNulls(node); + assertEquals(MAPPER.readTree("{\"outer\":{\"keep\":true}}"), result); + } + + @Test + void stripNulls_leavesArrayElementsIntact() throws Exception { + // Nulls inside arrays are kept (they're positional) + JsonNode node = MAPPER.readTree("{\"arr\":[1,null,3]}"); + JsonNode result = JsonNodeUtils.stripNulls(node); + assertEquals(MAPPER.readTree("{\"arr\":[1,null,3]}"), result); + } + + @Test + void stripNulls_removesNullsInsideArrayObjects() throws Exception { + JsonNode node = MAPPER.readTree("[{\"a\":1,\"b\":null},{\"c\":null}]"); + JsonNode result = JsonNodeUtils.stripNulls(node); + assertEquals(MAPPER.readTree("[{\"a\":1},{}]"), result); + } + + @Test + void stripNulls_noOpOnCleanTree() throws Exception { + JsonNode node = MAPPER.readTree("{\"a\":1,\"b\":\"two\"}"); + JsonNode result = JsonNodeUtils.stripNulls(node); + assertEquals(MAPPER.readTree("{\"a\":1,\"b\":\"two\"}"), result); + } + + // --- diffNodes --- + + @Test + void diffNodes_identicalTrees_noDiffs() throws Exception { + JsonNode a = MAPPER.readTree("{\"x\":1,\"y\":\"hello\"}"); + List diffs = new ArrayList<>(); + JsonNodeUtils.diffNodes("", a, a.deepCopy(), diffs); + assertTrue(diffs.isEmpty()); + } + + @Test + void diffNodes_changedValue() throws Exception { + JsonNode expected = MAPPER.readTree("{\"x\":1}"); + JsonNode actual = MAPPER.readTree("{\"x\":2}"); + List diffs = new ArrayList<>(); + JsonNodeUtils.diffNodes("", expected, actual, diffs); + assertEquals(1, diffs.size()); + assertTrue(diffs.get(0).startsWith("CHANGED .x")); + } + + @Test + void diffNodes_missingField() throws Exception { + JsonNode expected = MAPPER.readTree("{\"a\":1,\"b\":2}"); + JsonNode actual = MAPPER.readTree("{\"a\":1}"); + List diffs = new ArrayList<>(); + JsonNodeUtils.diffNodes("", expected, actual, diffs); + assertEquals(1, diffs.size()); + assertTrue(diffs.get(0).contains("MISSING") && diffs.get(0).contains(".b"), "got: " + diffs.get(0)); + } + + @Test + void diffNodes_addedField() throws Exception { + JsonNode expected = MAPPER.readTree("{\"a\":1}"); + JsonNode actual = MAPPER.readTree("{\"a\":1,\"b\":2}"); + List diffs = new ArrayList<>(); + JsonNodeUtils.diffNodes("", expected, actual, diffs); + assertEquals(1, diffs.size(), "diffs: " + diffs); + assertTrue(diffs.get(0).contains("ADDED") && diffs.get(0).contains(".b"), "got: " + diffs.get(0)); + } + + @Test + void diffNodes_nestedObjectDiff() throws Exception { + JsonNode expected = MAPPER.readTree("{\"outer\":{\"inner\":\"old\"}}"); + JsonNode actual = MAPPER.readTree("{\"outer\":{\"inner\":\"new\"}}"); + List diffs = new ArrayList<>(); + JsonNodeUtils.diffNodes("", expected, actual, diffs); + assertEquals(1, diffs.size()); + assertTrue(diffs.get(0).contains(".outer.inner")); + } + + @Test + void diffNodes_arrayElementDiff() throws Exception { + JsonNode expected = MAPPER.readTree("{\"arr\":[1,2,3]}"); + JsonNode actual = MAPPER.readTree("{\"arr\":[1,99,3]}"); + List diffs = new ArrayList<>(); + JsonNodeUtils.diffNodes("", expected, actual, diffs); + assertEquals(1, diffs.size()); + assertTrue(diffs.get(0).contains("[1]")); + } + + @Test + void diffNodes_arrayLengthMismatch() throws Exception { + JsonNode expected = MAPPER.readTree("{\"arr\":[1,2]}"); + JsonNode actual = MAPPER.readTree("{\"arr\":[1,2,3]}"); + List diffs = new ArrayList<>(); + JsonNodeUtils.diffNodes("", expected, actual, diffs); + assertEquals(1, diffs.size()); + assertTrue(diffs.get(0).contains("ADDED"), "got: " + diffs.get(0)); + } + + // --- areSameDateTime (tested indirectly via diffNodes) --- + + @Test + void diffNodes_equivalentDateTimes_noDiff() throws Exception { + // "+0000" vs "Z" — same instant, different format + JsonNode expected = MAPPER.readTree("{\"t\":\"2020-03-12T19:03:58.000+0000\"}"); + JsonNode actual = MAPPER.readTree("{\"t\":\"2020-03-12T19:03:58.000Z\"}"); + List diffs = new ArrayList<>(); + JsonNodeUtils.diffNodes("", expected, actual, diffs); + assertTrue(diffs.isEmpty(), "Expected no diffs for equivalent datetimes, got: " + diffs); + } + + @Test + void diffNodes_differentDateTimes_hasDiff() throws Exception { + JsonNode expected = MAPPER.readTree("{\"t\":\"2020-03-12T19:03:58.000Z\"}"); + JsonNode actual = MAPPER.readTree("{\"t\":\"2021-01-01T00:00:00.000Z\"}"); + List diffs = new ArrayList<>(); + JsonNodeUtils.diffNodes("", expected, actual, diffs); + assertEquals(1, diffs.size()); + } + + @Test + void diffNodes_nonDateStrings_notTreatedAsDates() throws Exception { + JsonNode expected = MAPPER.readTree("{\"s\":\"hello\"}"); + JsonNode actual = MAPPER.readTree("{\"s\":\"world\"}"); + List diffs = new ArrayList<>(); + JsonNodeUtils.diffNodes("", expected, actual, diffs); + assertEquals(1, diffs.size()); + assertTrue(diffs.get(0).startsWith("CHANGED .s")); + } +} diff --git a/aws-lambda-java-tests/src/test/java/com/amazonaws/services/lambda/runtime/tests/LambdaEventAssertTest.java b/aws-lambda-java-tests/src/test/java/com/amazonaws/services/lambda/runtime/tests/LambdaEventAssertTest.java new file mode 100644 index 000000000..63067f8b2 --- /dev/null +++ b/aws-lambda-java-tests/src/test/java/com/amazonaws/services/lambda/runtime/tests/LambdaEventAssertTest.java @@ -0,0 +1,71 @@ +package com.amazonaws.services.lambda.runtime.tests; + +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.*; + +public class LambdaEventAssertTest { + /** + * Demonstrates the completeness check: the fixture has a field + * ({@code unknownField}) that {@code PartialPojo} does not capture, + * so it gets silently dropped during deserialization. + */ + @Test + void shouldFailWhenFieldIsDropped() { + AssertionError error = assertThrows(AssertionError.class, + () -> LambdaEventAssert.assertSerializationRoundTrip( + "partial_pojo.json", PartialPojo.class)); + + assertTrue(error.getMessage().contains("PartialPojo"), + "Error message should name the failing class"); + } + + /** + * Demonstrates the stability check: the getter mutates state on each + * call, so the first and second round-trips produce different JSON. + */ + @Test + void shouldFailWhenSerializationIsUnstable() { + AssertionError error = assertThrows(AssertionError.class, + () -> LambdaEventAssert.assertSerializationRoundTrip( + "unstable_pojo.json", UnstablePojo.class)); + + assertTrue(error.getMessage().contains("UnstablePojo"), + "Error message should name the failing class"); + } + + /** POJO that only captures {@code name}, silently dropping any other fields. */ + public static class PartialPojo { + private String name; + + public PartialPojo() { + } + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + } + + /** + * POJO with a getter that appends a suffix, making serialization + * non-idempotent. + */ + public static class UnstablePojo { + private String name; + + public UnstablePojo() { + } + + public String getName() { + return name == null ? null : name + "_x"; + } + + public void setName(String name) { + this.name = name; + } + } +} diff --git a/aws-lambda-java-tests/src/test/java/com/amazonaws/services/lambda/runtime/tests/ResponseEventSerializationRoundTripTest.java b/aws-lambda-java-tests/src/test/java/com/amazonaws/services/lambda/runtime/tests/ResponseEventSerializationRoundTripTest.java new file mode 100644 index 000000000..e700b0d01 --- /dev/null +++ b/aws-lambda-java-tests/src/test/java/com/amazonaws/services/lambda/runtime/tests/ResponseEventSerializationRoundTripTest.java @@ -0,0 +1,59 @@ +/* Copyright 2020 Amazon.com, Inc. or its affiliates. All Rights Reserved. */ +package com.amazonaws.services.lambda.runtime.tests; + +import com.amazonaws.services.lambda.runtime.events.*; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.Arguments; +import org.junit.jupiter.params.provider.MethodSource; + +import java.util.stream.Stream; + +/** + * Verifies serialization round-trip fidelity for Lambda response event types. + * + *

Response events are POJOs that Lambda functions return to the RIC, which + * serializes them to JSON via {@code EventHandlerLoader.getSerializer()}. + * None of these types are registered in {@code SUPPORTED_EVENTS}, so they + * go through the bare Jackson path ({@code JacksonFactory.getInstance() + * .getSerializer(type)}).

+ * + *

Although the RIC only calls {@code toJson()} on response types (never + * {@code fromJson()}), the round-trip test is a stricter check: if a response + * type survives JSON → POJO → JSON → POJO → JSON, it + * certainly survives the production POJO → JSON path.

+ * + * @see SerializationRoundTripTest for registered input events + * @see UnregisteredEventSerializationRoundTripTest for unregistered input events + */ +@SuppressWarnings("deprecation") // APIGatewayV2ProxyResponseEvent is deprecated +public class ResponseEventSerializationRoundTripTest { + + @ParameterizedTest(name = "{0}") + @MethodSource("passingCases") + void roundTrip(String displayName, String fixture, Class eventClass) { + LambdaEventAssert.assertSerializationRoundTrip(fixture, eventClass); + } + + private static Stream passingCases() { + return Stream.of( + // API Gateway responses + args(APIGatewayProxyResponseEvent.class, "response/apigw_proxy_response.json"), + args(APIGatewayV2HTTPResponse.class, "response/apigw_v2_http_response.json"), + args(APIGatewayV2WebSocketResponse.class, "response/apigw_v2_websocket_response.json"), + args(APIGatewayV2ProxyResponseEvent.class, "response/apigw_v2_websocket_response.json"), + // ALB response + args(ApplicationLoadBalancerResponseEvent.class, "response/alb_response.json"), + // S3 Batch response + args(S3BatchResponse.class, "response/s3_batch_response.json"), + // SQS Batch response + args(SQSBatchResponse.class, "response/sqs_batch_response.json"), + // Simple IAM Policy response (HTTP API) + args(SimpleIAMPolicyResponse.class, "response/simple_iam_policy_response.json"), + // MSK Firehose response + args(MSKFirehoseResponse.class, "response/msk_firehose_response.json")); + } + + private static Arguments args(Class clazz, String fixture) { + return Arguments.of(clazz.getSimpleName(), fixture, clazz); + } +} diff --git a/aws-lambda-java-tests/src/test/java/com/amazonaws/services/lambda/runtime/tests/SerializationRoundTripTest.java b/aws-lambda-java-tests/src/test/java/com/amazonaws/services/lambda/runtime/tests/SerializationRoundTripTest.java new file mode 100644 index 000000000..3eab8fec0 --- /dev/null +++ b/aws-lambda-java-tests/src/test/java/com/amazonaws/services/lambda/runtime/tests/SerializationRoundTripTest.java @@ -0,0 +1,108 @@ +/* Copyright 2020 Amazon.com, Inc. or its affiliates. All Rights Reserved. */ +package com.amazonaws.services.lambda.runtime.tests; + +import com.amazonaws.services.lambda.runtime.events.*; +import com.amazonaws.services.lambda.runtime.events.models.s3.S3EventNotification; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.Arguments; +import org.junit.jupiter.params.provider.MethodSource; + +import java.util.stream.Stream; + +import static org.junit.jupiter.api.Assertions.*; + +/** + * Verifies serialization round-trip fidelity for events that are registered in + * {@code LambdaEventSerializers.SUPPORTED_EVENTS}. + * + *

Registered events go through the full customized serialization path in the + * Runtime Interface Client (RIC): {@code EventHandlerLoader.getSerializer()} + * detects them via {@code isLambdaSupportedEvent()} and delegates to + * {@code LambdaEventSerializers.serializerFor()}, which applies Jackson mixins, + * {@code DateModule}/{@code DateTimeModule}, and naming strategies.

+ * + *

Each case feeds a JSON fixture through + * {@link LambdaEventAssert#assertSerializationRoundTrip} which performs two + * consecutive round-trips and compares the original JSON tree against the + * final output.

+ * + * @see UnregisteredEventSerializationRoundTripTest for events not in SUPPORTED_EVENTS + */ +public class SerializationRoundTripTest { + + @ParameterizedTest(name = "{0}") + @MethodSource("passingCases") + void roundTrip(String displayName, String fixture, Class eventClass) { + LambdaEventAssert.assertSerializationRoundTrip(fixture, eventClass); + } + + @ParameterizedTest(name = "{0} (known failure)") + @MethodSource("knownFailureCases") + void roundTripKnownFailures(String displayName, String fixture, Class eventClass) { + assertThrows(Throwable.class, + () -> LambdaEventAssert.assertSerializationRoundTrip(fixture, eventClass), + displayName + " was expected to fail but passed — move it to passingCases()"); + } + + private static Stream passingCases() { + return Stream.of( + args(CloudFormationCustomResourceEvent.class, "cloudformation_event.json"), + args(CloudWatchLogsEvent.class, "cloudwatchlogs_event.json"), + args(CodeCommitEvent.class, "codecommit_event.json"), + args(ConfigEvent.class, "config_event.json"), + args(DynamodbEvent.class, "ddb/dynamo_event_roundtrip.json"), + args(KinesisEvent.class, "kinesis/kinesis_event_roundtrip.json"), + args(KinesisFirehoseEvent.class, "firehose_event.json"), + args(LambdaDestinationEvent.class, "lambda_destination_event.json"), + args(ScheduledEvent.class, "cloudwatch_event.json"), + args(SecretsManagerRotationEvent.class, "secrets_rotation_event.json"), + args(SNSEvent.class, "sns_event.json"), + args(LexEvent.class, "lex_event_roundtrip.json"), + args(ConnectEvent.class, "connect_event.json"), + args(SQSEvent.class, "sqs/sqs_event_nobody.json"), + args(APIGatewayProxyRequestEvent.class, "apigw_rest_event.json"), + args(CloudFrontEvent.class, "cloudfront_event.json"), + args(S3Event.class, "s3_event.json"), + args(S3EventNotification.class, "s3_event.json"), + args(APIGatewayV2HTTPEvent.class, "apigw_http_event.json"), + args(APIGatewayCustomAuthorizerEvent.class, "apigw_auth.json"), + args(ApplicationLoadBalancerRequestEvent.class, "elb_event.json"), + args(CloudWatchCompositeAlarmEvent.class, "cloudwatch_composite_alarm.json"), + args(CloudWatchMetricAlarmEvent.class, "cloudwatch_metric_alarm.json"), + args(CognitoUserPoolPreTokenGenerationEventV2.class, "cognito_user_pool_pre_token_generation_event_v2.json"), + args(KafkaEvent.class, "kafka_event_roundtrip.json"), + args(MSKFirehoseEvent.class, "msk_firehose_event_roundtrip.json"), + args(RabbitMQEvent.class, "rabbitmq_event_roundtrip.json"), + args(S3BatchEventV2.class, "s3_batch_event_v2.json"), + args(IoTButtonEvent.class, "iot_button_event.json"), + args(CognitoEvent.class, "cognito_sync_event.json"), + args(DynamodbTimeWindowEvent.class, "ddb/dynamo_time_window_event.json"), + args(KinesisTimeWindowEvent.class, "kinesis/kinesis_time_window_event.json")); + } + + private static Stream knownFailureCases() { + return Stream.of( + // APIGatewayV2CustomAuthorizerEvent has two serialization issues: + // 1. getTime() parses the raw string "12/Mar/2020:19:03:58 +0000" into a + // DateTime via dd/MMM/yyyy formatter. Jackson serializes as ISO-8601, but + // the formatter cannot parse ISO-8601 back on the second round-trip. + // The time field is effectively mandatory (getTime() throws NPE if null), + // and the date format change is inherent to how the serialization works. + // 2. getTimeEpoch() converts long to Instant; Jackson serializes as decimal + // seconds (e.g. 1583348638.390000000) instead of the original long. + // Both transformations are lossy; coercion captured in EventLoaderTest. + args(APIGatewayV2CustomAuthorizerEvent.class, "apigw_auth_v2.json"), + // ActiveMQEvent has one serialization issue: + // Destination.physicalName (camelCase) vs JSON "physicalname" (lowercase) — + // ACCEPT_CASE_INSENSITIVE_PROPERTIES is disabled in JacksonFactory so the + // field is silently dropped during deserialization. + // Fix: create an ActiveMQEventMixin with a DestinationMixin that maps + // @JsonProperty("physicalname") to getPhysicalName()/setPhysicalName(), + // then register it in LambdaEventSerializers MIXIN_MAP and NESTED_CLASS_MAP. + args(ActiveMQEvent.class, "mq_event.json")); + } + + private static Arguments args(Class clazz, String fixture) { + return Arguments.of(clazz.getSimpleName(), fixture, clazz); + } +} diff --git a/aws-lambda-java-tests/src/test/java/com/amazonaws/services/lambda/runtime/tests/UnregisteredEventSerializationRoundTripTest.java b/aws-lambda-java-tests/src/test/java/com/amazonaws/services/lambda/runtime/tests/UnregisteredEventSerializationRoundTripTest.java new file mode 100644 index 000000000..12ad818e4 --- /dev/null +++ b/aws-lambda-java-tests/src/test/java/com/amazonaws/services/lambda/runtime/tests/UnregisteredEventSerializationRoundTripTest.java @@ -0,0 +1,90 @@ +/* Copyright 2020 Amazon.com, Inc. or its affiliates. All Rights Reserved. */ +package com.amazonaws.services.lambda.runtime.tests; + +import com.amazonaws.services.lambda.runtime.events.*; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.Arguments; +import org.junit.jupiter.params.provider.MethodSource; + +import java.util.stream.Stream; + +import static org.junit.jupiter.api.Assertions.*; + +/** + * Verifies serialization round-trip fidelity for events that are NOT registered + * in {@code LambdaEventSerializers.SUPPORTED_EVENTS}. + * + *

In the Runtime Interface Client (RIC), when a handler's event type is not + * in {@code SUPPORTED_EVENTS}, {@code EventHandlerLoader.getSerializer()} falls + * through to {@code JacksonFactory.getInstance().getSerializer(type)} — a bare + * {@code PojoSerializer} backed by Jackson without any mixins or naming + * strategies. However, {@code LambdaEventSerializers.serializerFor()} (used by + * this test) unconditionally registers {@code DateModule} and + * {@code DateTimeModule}, so Joda/java.time types are still handled. For most + * unregistered events this makes no practical difference because they don't + * contain Joda DateTime fields.

+ * + * @see SerializationRoundTripTest for events registered in SUPPORTED_EVENTS + */ +@SuppressWarnings("deprecation") // APIGatewayV2ProxyRequestEvent is deprecated +public class UnregisteredEventSerializationRoundTripTest { + + @ParameterizedTest(name = "{0}") + @MethodSource("passingCases") + void roundTrip(String displayName, String fixture, Class eventClass) { + LambdaEventAssert.assertSerializationRoundTrip(fixture, eventClass); + } + + @ParameterizedTest(name = "{0} (known failure)") + @MethodSource("knownFailureCases") + void roundTripKnownFailures(String displayName, String fixture, Class eventClass) { + assertThrows(Throwable.class, + () -> LambdaEventAssert.assertSerializationRoundTrip(fixture, eventClass), + displayName + " was expected to fail but passed — move it to passingCases()"); + } + + private static Stream passingCases() { + return Stream.of( + // S3 Batch + args(S3BatchEvent.class, "s3_batch_event.json"), + // AppSync + args(AppSyncLambdaAuthorizerEvent.class, "appsync_authorizer_event.json"), + args(AppSyncLambdaAuthorizerResponse.class, "appsync_authorizer_response.json"), + // TimeWindow response + args(TimeWindowEventResponse.class, "time_window_event_response.json"), + // Cognito UserPool triggers + args(CognitoUserPoolPreSignUpEvent.class, "cognito/cognito_userpool_presignup.json"), + args(CognitoUserPoolPostConfirmationEvent.class, "cognito/cognito_userpool_postconfirmation.json"), + args(CognitoUserPoolPreAuthenticationEvent.class, "cognito/cognito_userpool_preauthentication.json"), + args(CognitoUserPoolPostAuthenticationEvent.class, "cognito/cognito_userpool_postauthentication.json"), + args(CognitoUserPoolDefineAuthChallengeEvent.class, "cognito/cognito_userpool_define_auth_challenge.json"), + args(CognitoUserPoolCreateAuthChallengeEvent.class, "cognito/cognito_userpool_create_auth_challenge.json"), + args(CognitoUserPoolVerifyAuthChallengeResponseEvent.class, "cognito/cognito_userpool_verify_auth_challenge.json"), + args(CognitoUserPoolMigrateUserEvent.class, "cognito/cognito_userpool_migrate_user.json"), + args(CognitoUserPoolCustomMessageEvent.class, "cognito/cognito_userpool_custom_message.json"), + args(CognitoUserPoolPreTokenGenerationEvent.class, "cognito/cognito_userpool_pre_token_generation.json"), + // Kinesis Analytics + args(KinesisAnalyticsFirehoseInputPreprocessingEvent.class, "kinesis/kinesis_analytics_firehose_input_preprocessing.json"), + args(KinesisAnalyticsStreamsInputPreprocessingEvent.class, "kinesis/kinesis_analytics_streams_input_preprocessing.json"), + args(KinesisAnalyticsInputPreprocessingResponse.class, "kinesis/kinesis_analytics_input_preprocessing_response.json"), + args(KinesisAnalyticsOutputDeliveryEvent.class, "kinesis/kinesis_analytics_output_delivery.json"), + args(KinesisAnalyticsOutputDeliveryResponse.class, "kinesis/kinesis_analytics_output_delivery_response.json"), + // API Gateway V2 WebSocket + args(APIGatewayV2WebSocketEvent.class, "apigw_websocket_event.json"), + args(APIGatewayV2ProxyRequestEvent.class, "apigw_websocket_event.json")); + } + + private static Stream knownFailureCases() { + return Stream.of( + // S3ObjectLambdaEvent: Lombok generates getXAmzRequestId() for field + // "xAmzRequestId". With USE_STD_BEAN_NAMING, Jackson derives the property + // name as "XAmzRequestId" (capital X), so the original "xAmzRequestId" key + // is silently dropped during deserialization. + // Fix: add @JsonProperty("xAmzRequestId") on the field or getter. + args(S3ObjectLambdaEvent.class, "s3_object_lambda_event.json")); + } + + private static Arguments args(Class clazz, String fixture) { + return Arguments.of(clazz.getSimpleName(), fixture, clazz); + } +} diff --git a/aws-lambda-java-tests/src/test/resources/apigw_http_event.json b/aws-lambda-java-tests/src/test/resources/apigw_http_event.json index 88f4e5b4b..91954656c 100644 --- a/aws-lambda-java-tests/src/test/resources/apigw_http_event.json +++ b/aws-lambda-java-tests/src/test/resources/apigw_http_event.json @@ -18,18 +18,6 @@ "requestContext": { "accountId": "123456789012", "apiId": "api-id", - "authentication": { - "clientCert": { - "clientCertPem": "CERT_CONTENT", - "subjectDN": "www.example.com", - "issuerDN": "Example issuer", - "serialNumber": "a1:a1:a1:a1:a1:a1:a1:a1:a1:a1:a1:a1:a1:a1:a1:a1", - "validity": { - "notBefore": "May 28 12:30:02 2019 GMT", - "notAfter": "Aug 5 09:36:04 2021 GMT" - } - } - }, "authorizer": { "jwt": { "claims": { diff --git a/aws-lambda-java-tests/src/test/resources/apigw_rest_event.json b/aws-lambda-java-tests/src/test/resources/apigw_rest_event.json index 28f10c221..a139ccbe8 100644 --- a/aws-lambda-java-tests/src/test/resources/apigw_rest_event.json +++ b/aws-lambda-java-tests/src/test/resources/apigw_rest_event.json @@ -14,19 +14,22 @@ "Header2": [ "value1", "value2" + ], + "Header3": [ + "value1,value2" ] }, "queryStringParameters": { "parameter1": "value1", - "parameter2": "value" + "parameter2": "value2" }, "multiValueQueryStringParameters": { "parameter1": [ - "value1", - "value2" + "value1" ], "parameter2": [ - "value" + "value1", + "value2" ] }, "requestContext": { @@ -52,17 +55,7 @@ "sourceIp": "IP", "user": null, "userAgent": "user-agent", - "userArn": null, - "clientCert": { - "clientCertPem": "CERT_CONTENT", - "subjectDN": "www.example.com", - "issuerDN": "Example issuer", - "serialNumber": "a1:a1:a1:a1:a1:a1:a1:a1:a1:a1:a1:a1:a1:a1:a1:a1", - "validity": { - "notBefore": "May 28 12:30:02 2019 GMT", - "notAfter": "Aug 5 09:36:04 2021 GMT" - } - } + "userArn": null }, "path": "/my/path", "protocol": "HTTP/1.1", @@ -76,5 +69,5 @@ "pathParameters": null, "stageVariables": null, "body": "Hello from Lambda!", - "isBase64Encoded": true + "isBase64Encoded": false } \ No newline at end of file diff --git a/aws-lambda-java-tests/src/test/resources/apigw_websocket_event.json b/aws-lambda-java-tests/src/test/resources/apigw_websocket_event.json new file mode 100644 index 000000000..a47fc3a94 --- /dev/null +++ b/aws-lambda-java-tests/src/test/resources/apigw_websocket_event.json @@ -0,0 +1,88 @@ +{ + "resource": "/", + "path": "/", + "httpMethod": "GET", + "headers": { + "Host": "abcdef1234.execute-api.us-east-1.amazonaws.com", + "Sec-WebSocket-Extensions": "permessage-deflate", + "Sec-WebSocket-Key": "dGhlIHNhbXBsZSBub25jZQ==", + "Sec-WebSocket-Version": "13", + "X-Forwarded-For": "192.0.2.1", + "X-Forwarded-Port": "443", + "X-Forwarded-Proto": "https" + }, + "multiValueHeaders": { + "Host": [ + "abcdef1234.execute-api.us-east-1.amazonaws.com" + ], + "Sec-WebSocket-Extensions": [ + "permessage-deflate" + ], + "Sec-WebSocket-Key": [ + "dGhlIHNhbXBsZSBub25jZQ==" + ], + "Sec-WebSocket-Version": [ + "13" + ], + "X-Forwarded-For": [ + "192.0.2.1" + ], + "X-Forwarded-Port": [ + "443" + ], + "X-Forwarded-Proto": [ + "https" + ] + }, + "queryStringParameters": { + "param1": "value1" + }, + "multiValueQueryStringParameters": { + "param1": [ + "value1" + ] + }, + "pathParameters": { + "proxy": "path/to/resource" + }, + "stageVariables": { + "stageVar1": "value1" + }, + "requestContext": { + "accountId": "123456789012", + "resourceId": "abcdef", + "stage": "prod", + "requestId": "abc-def-ghi", + "identity": { + "cognitoIdentityPoolId": "us-east-1:id-pool", + "accountId": "123456789012", + "cognitoIdentityId": "us-east-1:identity-id", + "caller": "caller-id", + "apiKey": "api-key-id", + "sourceIp": "192.0.2.1", + "cognitoAuthenticationType": "authenticated", + "cognitoAuthenticationProvider": "provider", + "userArn": "arn:aws:iam::123456789012:user/testuser", + "userAgent": "Mozilla/5.0", + "user": "testuser", + "accessKey": "AKIAIOSFODNN7EXAMPLE" + }, + "resourcePath": "/", + "httpMethod": "GET", + "apiId": "abcdef1234", + "connectedAt": 1583348638390, + "connectionId": "abc123=", + "domainName": "abcdef1234.execute-api.us-east-1.amazonaws.com", + "eventType": "CONNECT", + "extendedRequestId": "abc123=", + "integrationLatency": "100", + "messageDirection": "IN", + "messageId": "msg-001", + "requestTime": "09/Apr/2020:18:03:58 +0000", + "requestTimeEpoch": 1583348638390, + "routeKey": "$connect", + "status": "200" + }, + "body": "request body", + "isBase64Encoded": false +} \ No newline at end of file diff --git a/aws-lambda-java-tests/src/test/resources/appsync_authorizer_event.json b/aws-lambda-java-tests/src/test/resources/appsync_authorizer_event.json new file mode 100644 index 000000000..494a500f3 --- /dev/null +++ b/aws-lambda-java-tests/src/test/resources/appsync_authorizer_event.json @@ -0,0 +1,15 @@ +{ + "authorizationToken": "BE9DC5E3-D410-4733-AF76-70178092E681", + "requestContext": { + "apiId": "giy7kumfmvcqvbedntjwjvagii", + "accountId": "254688921111", + "requestId": "b80ed838-14c6-4500-b4c3-b694c7bef086", + "queryDocument": "mutation MyNewTask($desc: String!) {\n createTask(description: $desc) {\n id\n }\n}\n", + "operationName": "MyNewTask", + "variables": {} + }, + "requestHeaders": { + "host": "giy7kumfmvcqvbedntjwjvagii.appsync-api.us-east-1.amazonaws.com", + "content-type": "application/json" + } +} \ No newline at end of file diff --git a/aws-lambda-java-tests/src/test/resources/appsync_authorizer_response.json b/aws-lambda-java-tests/src/test/resources/appsync_authorizer_response.json new file mode 100644 index 000000000..1216ad51d --- /dev/null +++ b/aws-lambda-java-tests/src/test/resources/appsync_authorizer_response.json @@ -0,0 +1,11 @@ +{ + "isAuthorized": true, + "resolverContext": { + "name": "Foo Man", + "balance": "100" + }, + "deniedFields": [ + "Mutation.createEvent" + ], + "ttlOverride": 15 +} \ No newline at end of file diff --git a/aws-lambda-java-tests/src/test/resources/cloudfront_event.json b/aws-lambda-java-tests/src/test/resources/cloudfront_event.json index 7485310e9..bf4625d06 100644 --- a/aws-lambda-java-tests/src/test/resources/cloudfront_event.json +++ b/aws-lambda-java-tests/src/test/resources/cloudfront_event.json @@ -7,7 +7,6 @@ }, "request": { "uri": "/test", - "querystring": "auth=test&foo=bar", "method": "GET", "clientIp": "2001:cdba::3257:9652", "headers": { diff --git a/aws-lambda-java-tests/src/test/resources/cognito/cognito_userpool_create_auth_challenge.json b/aws-lambda-java-tests/src/test/resources/cognito/cognito_userpool_create_auth_challenge.json new file mode 100644 index 000000000..495b41475 --- /dev/null +++ b/aws-lambda-java-tests/src/test/resources/cognito/cognito_userpool_create_auth_challenge.json @@ -0,0 +1,37 @@ +{ + "version": "1", + "triggerSource": "CreateAuthChallenge_Authentication", + "region": "us-east-1", + "userPoolId": "us-east-1_uPoolId", + "userName": "testuser", + "callerContext": { + "awsSdkVersion": "2.0.0", + "clientId": "abcdefg1234567" + }, + "request": { + "userAttributes": { + "email": "user@example.com" + }, + "clientMetadata": { + "meta1": "value1" + }, + "challengeName": "CUSTOM_CHALLENGE", + "session": [ + { + "challengeName": "PASSWORD_VERIFIER", + "challengeResult": true, + "challengeMetadata": "metadata1" + } + ], + "userNotFound": false + }, + "response": { + "publicChallengeParameters": { + "captchaUrl": "url/123.jpg" + }, + "privateChallengeParameters": { + "answer": "5" + }, + "challengeMetadata": "CAPTCHA_CHALLENGE" + } +} \ No newline at end of file diff --git a/aws-lambda-java-tests/src/test/resources/cognito/cognito_userpool_custom_message.json b/aws-lambda-java-tests/src/test/resources/cognito/cognito_userpool_custom_message.json new file mode 100644 index 000000000..aa7a53a83 --- /dev/null +++ b/aws-lambda-java-tests/src/test/resources/cognito/cognito_userpool_custom_message.json @@ -0,0 +1,28 @@ +{ + "version": "1", + "triggerSource": "CustomMessage_SignUp", + "region": "us-east-1", + "userPoolId": "us-east-1_uPoolId", + "userName": "testuser", + "callerContext": { + "awsSdkVersion": "2.0.0", + "clientId": "abcdefg1234567" + }, + "request": { + "userAttributes": { + "email": "user@example.com", + "phone_number_verified": "true", + "email_verified": "true" + }, + "clientMetadata": { + "meta1": "value1" + }, + "codeParameter": "####", + "usernameParameter": "testuser" + }, + "response": { + "smsMessage": "Your code is ####", + "emailMessage": "Your code is ####", + "emailSubject": "Welcome" + } +} \ No newline at end of file diff --git a/aws-lambda-java-tests/src/test/resources/cognito/cognito_userpool_define_auth_challenge.json b/aws-lambda-java-tests/src/test/resources/cognito/cognito_userpool_define_auth_challenge.json new file mode 100644 index 000000000..e320b71d1 --- /dev/null +++ b/aws-lambda-java-tests/src/test/resources/cognito/cognito_userpool_define_auth_challenge.json @@ -0,0 +1,32 @@ +{ + "version": "1", + "triggerSource": "DefineAuthChallenge_Authentication", + "region": "us-east-1", + "userPoolId": "us-east-1_uPoolId", + "userName": "testuser", + "callerContext": { + "awsSdkVersion": "2.0.0", + "clientId": "abcdefg1234567" + }, + "request": { + "userAttributes": { + "email": "user@example.com" + }, + "clientMetadata": { + "meta1": "value1" + }, + "session": [ + { + "challengeName": "PASSWORD_VERIFIER", + "challengeResult": true, + "challengeMetadata": "metadata1" + } + ], + "userNotFound": false + }, + "response": { + "challengeName": "CUSTOM_CHALLENGE", + "issueTokens": false, + "failAuthentication": false + } +} \ No newline at end of file diff --git a/aws-lambda-java-tests/src/test/resources/cognito/cognito_userpool_migrate_user.json b/aws-lambda-java-tests/src/test/resources/cognito/cognito_userpool_migrate_user.json new file mode 100644 index 000000000..2897ae063 --- /dev/null +++ b/aws-lambda-java-tests/src/test/resources/cognito/cognito_userpool_migrate_user.json @@ -0,0 +1,35 @@ +{ + "version": "1", + "triggerSource": "UserMigration_Authentication", + "region": "us-east-1", + "userPoolId": "us-east-1_uPoolId", + "userName": "testuser", + "callerContext": { + "awsSdkVersion": "2.0.0", + "clientId": "abcdefg1234567" + }, + "request": { + "userAttributes": { + "email": "user@example.com" + }, + "validationData": { + "key1": "val1" + }, + "clientMetadata": { + "meta1": "value1" + }, + "userName": "testuser", + "password": "test-password" + }, + "response": { + "userAttributes": { + "email": "user@example.com" + }, + "finalUserStatus": "CONFIRMED", + "messageAction": "SUPPRESS", + "desiredDeliveryMediums": [ + "EMAIL" + ], + "forceAliasCreation": false + } +} \ No newline at end of file diff --git a/aws-lambda-java-tests/src/test/resources/cognito/cognito_userpool_postauthentication.json b/aws-lambda-java-tests/src/test/resources/cognito/cognito_userpool_postauthentication.json new file mode 100644 index 000000000..f41084d54 --- /dev/null +++ b/aws-lambda-java-tests/src/test/resources/cognito/cognito_userpool_postauthentication.json @@ -0,0 +1,20 @@ +{ + "version": "1", + "triggerSource": "PostAuthentication_Authentication", + "region": "us-east-1", + "userPoolId": "us-east-1_uPoolId", + "userName": "testuser", + "callerContext": { + "awsSdkVersion": "2.0.0", + "clientId": "abcdefg1234567" + }, + "request": { + "userAttributes": { + "email": "user@example.com" + }, + "clientMetadata": { + "meta1": "value1" + }, + "newDeviceUsed": false + } +} \ No newline at end of file diff --git a/aws-lambda-java-tests/src/test/resources/cognito/cognito_userpool_postconfirmation.json b/aws-lambda-java-tests/src/test/resources/cognito/cognito_userpool_postconfirmation.json new file mode 100644 index 000000000..ecf63c7d3 --- /dev/null +++ b/aws-lambda-java-tests/src/test/resources/cognito/cognito_userpool_postconfirmation.json @@ -0,0 +1,20 @@ +{ + "version": "1", + "triggerSource": "PostConfirmation_ConfirmSignUp", + "region": "us-east-1", + "userPoolId": "us-east-1_uPoolId", + "userName": "testuser", + "callerContext": { + "awsSdkVersion": "2.0.0", + "clientId": "abcdefg1234567" + }, + "request": { + "userAttributes": { + "email": "user@example.com", + "email_verified": "true" + }, + "clientMetadata": { + "meta1": "value1" + } + } +} \ No newline at end of file diff --git a/aws-lambda-java-tests/src/test/resources/cognito/cognito_userpool_pre_token_generation.json b/aws-lambda-java-tests/src/test/resources/cognito/cognito_userpool_pre_token_generation.json new file mode 100644 index 000000000..f81ffb902 --- /dev/null +++ b/aws-lambda-java-tests/src/test/resources/cognito/cognito_userpool_pre_token_generation.json @@ -0,0 +1,48 @@ +{ + "version": "1", + "triggerSource": "TokenGeneration_HostedAuth", + "region": "us-east-1", + "userPoolId": "us-east-1_uPoolId", + "userName": "testuser", + "callerContext": { + "awsSdkVersion": "2.0.0", + "clientId": "abcdefg1234567" + }, + "request": { + "userAttributes": { + "email": "user@example.com" + }, + "clientMetadata": { + "meta1": "value1" + }, + "groupConfiguration": { + "groupsToOverride": [ + "group1", + "group2" + ], + "iamRolesToOverride": [ + "arn:aws:iam::123456789012:role/role1" + ], + "preferredRole": "arn:aws:iam::123456789012:role/role1" + } + }, + "response": { + "claimsOverrideDetails": { + "claimsToAddOrOverride": { + "custom:myattr": "myvalue" + }, + "claimsToSuppress": [ + "email" + ], + "groupOverrideDetails": { + "groupsToOverride": [ + "group1" + ], + "iamRolesToOverride": [ + "arn:aws:iam::123456789012:role/role1" + ], + "preferredRole": "arn:aws:iam::123456789012:role/role1" + } + } + } +} \ No newline at end of file diff --git a/aws-lambda-java-tests/src/test/resources/cognito/cognito_userpool_preauthentication.json b/aws-lambda-java-tests/src/test/resources/cognito/cognito_userpool_preauthentication.json new file mode 100644 index 000000000..1402c4684 --- /dev/null +++ b/aws-lambda-java-tests/src/test/resources/cognito/cognito_userpool_preauthentication.json @@ -0,0 +1,20 @@ +{ + "version": "1", + "triggerSource": "PreAuthentication_Authentication", + "region": "us-east-1", + "userPoolId": "us-east-1_uPoolId", + "userName": "testuser", + "callerContext": { + "awsSdkVersion": "2.0.0", + "clientId": "abcdefg1234567" + }, + "request": { + "userAttributes": { + "email": "user@example.com" + }, + "validationData": { + "key1": "val1" + }, + "userNotFound": false + } +} \ No newline at end of file diff --git a/aws-lambda-java-tests/src/test/resources/cognito/cognito_userpool_presignup.json b/aws-lambda-java-tests/src/test/resources/cognito/cognito_userpool_presignup.json new file mode 100644 index 000000000..0d1f0936a --- /dev/null +++ b/aws-lambda-java-tests/src/test/resources/cognito/cognito_userpool_presignup.json @@ -0,0 +1,27 @@ +{ + "version": "1", + "triggerSource": "PreSignUp_SignUp", + "region": "us-east-1", + "userPoolId": "us-east-1_uPoolId", + "userName": "testuser", + "callerContext": { + "awsSdkVersion": "2.0.0", + "clientId": "abcdefg1234567" + }, + "request": { + "userAttributes": { + "email": "user@example.com" + }, + "validationData": { + "key1": "val1" + }, + "clientMetadata": { + "meta1": "value1" + } + }, + "response": { + "autoConfirmUser": false, + "autoVerifyPhone": false, + "autoVerifyEmail": false + } +} \ No newline at end of file diff --git a/aws-lambda-java-tests/src/test/resources/cognito/cognito_userpool_verify_auth_challenge.json b/aws-lambda-java-tests/src/test/resources/cognito/cognito_userpool_verify_auth_challenge.json new file mode 100644 index 000000000..ef14c4ddf --- /dev/null +++ b/aws-lambda-java-tests/src/test/resources/cognito/cognito_userpool_verify_auth_challenge.json @@ -0,0 +1,27 @@ +{ + "version": "1", + "triggerSource": "VerifyAuthChallengeResponse_Authentication", + "region": "us-east-1", + "userPoolId": "us-east-1_uPoolId", + "userName": "testuser", + "callerContext": { + "awsSdkVersion": "2.0.0", + "clientId": "abcdefg1234567" + }, + "request": { + "userAttributes": { + "email": "user@example.com" + }, + "clientMetadata": { + "meta1": "value1" + }, + "privateChallengeParameters": { + "answer": "5" + }, + "challengeAnswer": "5", + "userNotFound": false + }, + "response": { + "answerCorrect": true + } +} \ No newline at end of file diff --git a/aws-lambda-java-tests/src/test/resources/cognito_sync_event.json b/aws-lambda-java-tests/src/test/resources/cognito_sync_event.json new file mode 100644 index 000000000..6edf1c246 --- /dev/null +++ b/aws-lambda-java-tests/src/test/resources/cognito_sync_event.json @@ -0,0 +1,20 @@ +{ + "version": 2, + "eventType": "SyncTrigger", + "region": "us-east-1", + "identityPoolId": "us-east-1:example-identity-pool-id", + "identityId": "us-east-1:example-identity-id", + "datasetName": "SampleDataset", + "datasetRecords": { + "SampleKey1": { + "oldValue": "oldValue1", + "newValue": "newValue1", + "op": "replace" + }, + "SampleKey2": { + "oldValue": "oldValue2", + "newValue": "newValue2", + "op": "replace" + } + } +} \ No newline at end of file diff --git a/aws-lambda-java-tests/src/test/resources/connect_event.json b/aws-lambda-java-tests/src/test/resources/connect_event.json index a9e04f7f8..4ce17a657 100644 --- a/aws-lambda-java-tests/src/test/resources/connect_event.json +++ b/aws-lambda-java-tests/src/test/resources/connect_event.json @@ -12,17 +12,11 @@ "InitialContactId": "6ca32fbd-8f92-46af-92a5-6b0f970f0efe", "InitiationMethod": "API", "InstanceARN": "arn:aws:connect:eu-central-1:123456789012:instance/9308c2a1-9bc6-4cea-8290-6c0b4a6d38fa", - "MediaStreams": { - "Customer": { - "Audio": { - "StartFragmentNumber": "91343852333181432392682062622220590765191907586", - "StartTimestamp": "1565781909613", - "StreamARN": "arn:aws:kinesisvideo:eu-central-1:123456789012:stream/connect-contact-a3d73b84-ce0e-479a-a9dc-5637c9d30ac9/1565272947806" - } - } - }, "PreviousContactId": "4ca32fbd-8f92-46af-92a5-6b0f970f0efe", - "Queue": null, + "Queue": { + "Name": "SampleQueue", + "ARN": "arn:aws:connect:eu-central-1:123456789012:instance/9308c2a1-9bc6-4cea-8290-6c0b4a6d38fa" + }, "SystemEndpoint": { "Address": "+21234567890", "Type": "TELEPHONE_NUMBER" diff --git a/aws-lambda-java-tests/src/test/resources/ddb/dynamo_event_roundtrip.json b/aws-lambda-java-tests/src/test/resources/ddb/dynamo_event_roundtrip.json new file mode 100644 index 000000000..10d963c3c --- /dev/null +++ b/aws-lambda-java-tests/src/test/resources/ddb/dynamo_event_roundtrip.json @@ -0,0 +1,97 @@ +{ + "Records": [ + { + "eventID": "c4ca4238a0b923820dcc509a6f75849b", + "eventName": "INSERT", + "eventVersion": "1.1", + "eventSource": "aws:dynamodb", + "awsRegion": "eu-central-1", + "dynamodb": { + "Keys": { + "Id": { + "N": "101" + } + }, + "NewImage": { + "Message": { + "S": "New item!" + }, + "Id": { + "N": "101" + } + }, + "ApproximateCreationDateTime": 1.4285376E9, + "SequenceNumber": "4421584500000000017450439091", + "SizeBytes": 26, + "StreamViewType": "NEW_AND_OLD_IMAGES" + }, + "eventSourceARN": "arn:aws:dynamodb:eu-central-1:123456789012:table/ExampleTableWithStream/stream/2015-06-27T00:48:05.899", + "userIdentity": { + "principalId": "dynamodb.amazonaws.com", + "type": "Service" + } + }, + { + "eventID": "c81e728d9d4c2f636f067f89cc14862c", + "eventName": "MODIFY", + "eventVersion": "1.1", + "eventSource": "aws:dynamodb", + "awsRegion": "eu-central-1", + "dynamodb": { + "Keys": { + "Id": { + "N": "101" + } + }, + "NewImage": { + "Message": { + "S": "This item has changed" + }, + "Id": { + "N": "101" + } + }, + "OldImage": { + "Message": { + "S": "New item!" + }, + "Id": { + "N": "101" + } + }, + "ApproximateCreationDateTime": 1.635734407123E9, + "SequenceNumber": "4421584500000000017450439092", + "SizeBytes": 59, + "StreamViewType": "NEW_AND_OLD_IMAGES" + }, + "eventSourceARN": "arn:aws:dynamodb:eu-central-1:123456789012:table/ExampleTableWithStream/stream/2015-06-27T00:48:05.899" + }, + { + "eventID": "eccbc87e4b5ce2fe28308fd9f2a7baf3", + "eventName": "REMOVE", + "eventVersion": "1.1", + "eventSource": "aws:dynamodb", + "awsRegion": "eu-central-1", + "dynamodb": { + "Keys": { + "Id": { + "N": "101" + } + }, + "OldImage": { + "Message": { + "S": "This item has changed" + }, + "Id": { + "N": "101" + } + }, + "ApproximateCreationDateTime": 1.4285376E9, + "SequenceNumber": "4421584500000000017450439093", + "SizeBytes": 38, + "StreamViewType": "NEW_AND_OLD_IMAGES" + }, + "eventSourceARN": "arn:aws:dynamodb:eu-central-1:123456789012:table/ExampleTableWithStream/stream/2015-06-27T00:48:05.899" + } + ] +} \ No newline at end of file diff --git a/aws-lambda-java-tests/src/test/resources/ddb/dynamo_time_window_event.json b/aws-lambda-java-tests/src/test/resources/ddb/dynamo_time_window_event.json new file mode 100644 index 000000000..d931acb80 --- /dev/null +++ b/aws-lambda-java-tests/src/test/resources/ddb/dynamo_time_window_event.json @@ -0,0 +1,75 @@ +{ + "Records": [ + { + "eventID": "1", + "eventName": "INSERT", + "eventVersion": "1.0", + "eventSource": "aws:dynamodb", + "awsRegion": "us-east-1", + "dynamodb": { + "Keys": { + "Id": { + "N": "101" + } + }, + "NewImage": { + "Message": { + "S": "New item!" + }, + "Id": { + "N": "101" + } + }, + "SequenceNumber": "111", + "SizeBytes": 26, + "StreamViewType": "NEW_AND_OLD_IMAGES" + }, + "eventSourceARN": "arn:aws:dynamodb:us-east-1:123456789012:table/ExampleTableWithStream/stream/2015-06-27T00:48:05.899" + }, + { + "eventID": "2", + "eventName": "MODIFY", + "eventVersion": "1.0", + "eventSource": "aws:dynamodb", + "awsRegion": "us-east-1", + "dynamodb": { + "Keys": { + "Id": { + "N": "101" + } + }, + "NewImage": { + "Message": { + "S": "This item has changed" + }, + "Id": { + "N": "101" + } + }, + "OldImage": { + "Message": { + "S": "New item!" + }, + "Id": { + "N": "101" + } + }, + "SequenceNumber": "222", + "SizeBytes": 59, + "StreamViewType": "NEW_AND_OLD_IMAGES" + }, + "eventSourceARN": "arn:aws:dynamodb:us-east-1:123456789012:table/ExampleTableWithStream/stream/2015-06-27T00:48:05.899" + } + ], + "window": { + "start": "2020-07-30T17:00:00Z", + "end": "2020-07-30T17:05:00Z" + }, + "state": { + "1": "state1" + }, + "shardId": "shard123456789", + "eventSourceARN": "arn:aws:dynamodb:us-east-1:123456789012:table/ExampleTableWithStream/stream/2015-06-27T00:48:05.899", + "isFinalInvokeForWindow": false, + "isWindowTerminatedEarly": false +} \ No newline at end of file diff --git a/aws-lambda-java-tests/src/test/resources/iot_button_event.json b/aws-lambda-java-tests/src/test/resources/iot_button_event.json new file mode 100644 index 000000000..8dc82826b --- /dev/null +++ b/aws-lambda-java-tests/src/test/resources/iot_button_event.json @@ -0,0 +1,5 @@ +{ + "serialNumber": "G030JF055364XVRB", + "clickType": "SINGLE", + "batteryVoltage": "2000mV" +} \ No newline at end of file diff --git a/aws-lambda-java-tests/src/test/resources/kafka_event_roundtrip.json b/aws-lambda-java-tests/src/test/resources/kafka_event_roundtrip.json new file mode 100644 index 000000000..d9f682e5f --- /dev/null +++ b/aws-lambda-java-tests/src/test/resources/kafka_event_roundtrip.json @@ -0,0 +1,22 @@ +{ + "eventSource": "aws:kafka", + "eventSourceArn": "arn:aws:kafka:us-east-1:123456789012:cluster/vpc-3432434/4834-3547-3455-9872-7929", + "bootstrapServers": "b-2.demo-cluster-1.a1bcde.c1.kafka.us-east-1.amazonaws.com:9092,b-1.demo-cluster-1.a1bcde.c1.kafka.us-east-1.amazonaws.com:9092", + "records": { + "mytopic-01": [ + { + "topic": "mytopic", + "partition": 0, + "offset": 15, + "timestamp": 1596480920837, + "timestampType": "CREATE_TIME", + "value": "SGVsbG8gZnJvbSBLYWZrYSAhIQ==", + "headers": [ + { + "headerKey": "aGVhZGVyVmFsdWU=" + } + ] + } + ] + } +} \ No newline at end of file diff --git a/aws-lambda-java-tests/src/test/resources/kinesis/kinesis_analytics_firehose_input_preprocessing.json b/aws-lambda-java-tests/src/test/resources/kinesis/kinesis_analytics_firehose_input_preprocessing.json new file mode 100644 index 000000000..8c6cfe514 --- /dev/null +++ b/aws-lambda-java-tests/src/test/resources/kinesis/kinesis_analytics_firehose_input_preprocessing.json @@ -0,0 +1,14 @@ +{ + "invocationId": "invocationIdExample", + "applicationArn": "arn:aws:kinesisanalytics:us-east-1:123456789012:application/my-app", + "streamArn": "arn:aws:firehose:us-east-1:123456789012:deliverystream/my-stream", + "records": [ + { + "recordId": "49546986683135544286507457936321625675700192471156785154", + "kinesisFirehoseRecordMetadata": { + "approximateArrivalTimestamp": 1583348638390 + }, + "data": "SGVsbG8gV29ybGQ=" + } + ] +} \ No newline at end of file diff --git a/aws-lambda-java-tests/src/test/resources/kinesis/kinesis_analytics_input_preprocessing_response.json b/aws-lambda-java-tests/src/test/resources/kinesis/kinesis_analytics_input_preprocessing_response.json new file mode 100644 index 000000000..f5be190ec --- /dev/null +++ b/aws-lambda-java-tests/src/test/resources/kinesis/kinesis_analytics_input_preprocessing_response.json @@ -0,0 +1,9 @@ +{ + "records": [ + { + "recordId": "49546986683135544286507457936321625675700192471156785154", + "result": "Ok", + "data": "SGVsbG8gV29ybGQ=" + } + ] +} \ No newline at end of file diff --git a/aws-lambda-java-tests/src/test/resources/kinesis/kinesis_analytics_output_delivery.json b/aws-lambda-java-tests/src/test/resources/kinesis/kinesis_analytics_output_delivery.json new file mode 100644 index 000000000..573b6baba --- /dev/null +++ b/aws-lambda-java-tests/src/test/resources/kinesis/kinesis_analytics_output_delivery.json @@ -0,0 +1,13 @@ +{ + "invocationId": "invocationIdExample", + "applicationArn": "arn:aws:kinesisanalytics:us-east-1:123456789012:application/my-app", + "records": [ + { + "recordId": "49546986683135544286507457936321625675700192471156785154", + "lambdaDeliveryRecordMetadata": { + "retryHint": 0 + }, + "data": "SGVsbG8gV29ybGQ=" + } + ] +} \ No newline at end of file diff --git a/aws-lambda-java-tests/src/test/resources/kinesis/kinesis_analytics_output_delivery_response.json b/aws-lambda-java-tests/src/test/resources/kinesis/kinesis_analytics_output_delivery_response.json new file mode 100644 index 000000000..56ddaa194 --- /dev/null +++ b/aws-lambda-java-tests/src/test/resources/kinesis/kinesis_analytics_output_delivery_response.json @@ -0,0 +1,8 @@ +{ + "records": [ + { + "recordId": "49546986683135544286507457936321625675700192471156785154", + "result": "Ok" + } + ] +} \ No newline at end of file diff --git a/aws-lambda-java-tests/src/test/resources/kinesis/kinesis_analytics_streams_input_preprocessing.json b/aws-lambda-java-tests/src/test/resources/kinesis/kinesis_analytics_streams_input_preprocessing.json new file mode 100644 index 000000000..4ae8f3705 --- /dev/null +++ b/aws-lambda-java-tests/src/test/resources/kinesis/kinesis_analytics_streams_input_preprocessing.json @@ -0,0 +1,17 @@ +{ + "invocationId": "invocationIdExample", + "applicationArn": "arn:aws:kinesisanalytics:us-east-1:123456789012:application/my-app", + "streamArn": "arn:aws:kinesis:us-east-1:123456789012:stream/my-stream", + "records": [ + { + "recordId": "49546986683135544286507457936321625675700192471156785154", + "kinesisStreamRecordMetadata": { + "sequenceNumber": "49546986683135544286507457936321625675700192471156785154", + "partitionKey": "partKey", + "shardId": "shardId-000000000000", + "approximateArrivalTimestamp": 1583348638390 + }, + "data": "SGVsbG8gV29ybGQ=" + } + ] +} \ No newline at end of file diff --git a/aws-lambda-java-tests/src/test/resources/kinesis/kinesis_event_roundtrip.json b/aws-lambda-java-tests/src/test/resources/kinesis/kinesis_event_roundtrip.json new file mode 100644 index 000000000..e2081ef2b --- /dev/null +++ b/aws-lambda-java-tests/src/test/resources/kinesis/kinesis_event_roundtrip.json @@ -0,0 +1,21 @@ +{ + "Records": [ + { + "kinesis": { + "partitionKey": "partitionKey-03", + "kinesisSchemaVersion": "1.0", + "data": "SGVsbG8sIHRoaXMgaXMgYSB0ZXN0IDEyMy4=", + "sequenceNumber": "49545115243490985018280067714973144582180062593244200961", + "approximateArrivalTimestamp": 1.4285376E9, + "encryptionType": "NONE" + }, + "eventSource": "aws:kinesis", + "eventID": "shardId-000000000000:49545115243490985018280067714973144582180062593244200961", + "invokeIdentityArn": "arn:aws:iam::EXAMPLE", + "eventVersion": "1.0", + "eventName": "aws:kinesis:record", + "eventSourceARN": "arn:aws:kinesis:EXAMPLE", + "awsRegion": "eu-central-1" + } + ] +} \ No newline at end of file diff --git a/aws-lambda-java-tests/src/test/resources/kinesis/kinesis_time_window_event.json b/aws-lambda-java-tests/src/test/resources/kinesis/kinesis_time_window_event.json new file mode 100644 index 000000000..2d6283c58 --- /dev/null +++ b/aws-lambda-java-tests/src/test/resources/kinesis/kinesis_time_window_event.json @@ -0,0 +1,32 @@ +{ + "Records": [ + { + "kinesis": { + "kinesisSchemaVersion": "1.0", + "partitionKey": "1", + "sequenceNumber": "49590338271490256608559692538361571095921575989136588898", + "data": "SGVsbG8sIHRoaXMgaXMgYSB0ZXN0Lg==", + "approximateArrivalTimestamp": 1.607497475E9 + }, + "eventSource": "aws:kinesis", + "eventVersion": "1.0", + "eventID": "shardId-000000000006:49590338271490256608559692538361571095921575989136588898", + "eventName": "aws:kinesis:record", + "invokeIdentityArn": "arn:aws:iam::123456789012:role/lambda-kinesis-role", + "awsRegion": "us-east-1", + "eventSourceARN": "arn:aws:kinesis:us-east-1:123456789012:stream/lambda-stream" + } + ], + "window": { + "start": "2020-12-09T07:04:00Z", + "end": "2020-12-09T07:06:00Z" + }, + "state": { + "1": "282", + "2": "715" + }, + "shardId": "shardId-000000000006", + "eventSourceARN": "arn:aws:kinesis:us-east-1:123456789012:stream/lambda-stream", + "isFinalInvokeForWindow": false, + "isWindowTerminatedEarly": false +} \ No newline at end of file diff --git a/aws-lambda-java-tests/src/test/resources/lex_event_roundtrip.json b/aws-lambda-java-tests/src/test/resources/lex_event_roundtrip.json new file mode 100644 index 000000000..be065eb3f --- /dev/null +++ b/aws-lambda-java-tests/src/test/resources/lex_event_roundtrip.json @@ -0,0 +1,24 @@ +{ + "messageVersion": "1.0", + "invocationSource": "DialogCodeHook", + "userId": "John", + "sessionAttributes": { + "key": "value" + }, + "bot": { + "name": "BookTrip", + "alias": "$LATEST", + "version": "$LATEST" + }, + "outputDialogMode": "Text", + "currentIntent": { + "name": "BookHotel", + "slots": { + "Location": "Chicago", + "CheckInDate": "2030-11-08", + "Nights": "4", + "RoomType": "queen" + }, + "confirmationStatus": "None" + } +} \ No newline at end of file diff --git a/aws-lambda-java-tests/src/test/resources/mq_event.json b/aws-lambda-java-tests/src/test/resources/mq_event.json index 6505a22d4..8b12af72e 100644 --- a/aws-lambda-java-tests/src/test/resources/mq_event.json +++ b/aws-lambda-java-tests/src/test/resources/mq_event.json @@ -5,15 +5,17 @@ { "messageID": "ID:b-9bcfa592-423a-4942-879d-eb284b418fc8-1.mq.us-west-2.amazonaws.com-37557-1234520418293-4:1:1:1:1", "messageType": "jms/text-message", - "data": "QUJDOkFBQUE=", - "connectionId": "myJMSCoID", + "timestamp": 1598827811958, + "deliveryMode": 0, "redelivered": false, + "expiration": 0, + "priority": 0, + "data": "QUJDOkFBQUE=", + "brokerInTime": 1598827811958, + "brokerOutTime": 1598827811959, "destination": { "physicalname": "testQueue" }, - "timestamp": 1598827811958, - "brokerInTime": 1598827811958, - "brokerOutTime": 1598827811959, "properties": { "testKey": "testValue" } @@ -21,15 +23,17 @@ { "messageID": "ID:b-8bcfa572-428a-4642-879d-eb284b418fc8-1.mq.us-west-2.amazonaws.com-37557-1234520418293-4:1:1:1:1", "messageType": "jms/bytes-message", + "timestamp": 1598827811958, + "deliveryMode": 0, + "redelivered": false, + "expiration": 0, + "priority": 0, "data": "3DTOOW7crj51prgVLQaGQ82S48k=", - "connectionId": "myJMSCoID1", - "persistent": false, + "brokerInTime": 1598827811958, + "brokerOutTime": 1598827811959, "destination": { "physicalname": "testQueue" }, - "timestamp": 1598827811958, - "brokerInTime": 1598827811958, - "brokerOutTime": 1598827811959, "properties": { "testKey": "testValue" } diff --git a/aws-lambda-java-tests/src/test/resources/msk_firehose_event.json b/aws-lambda-java-tests/src/test/resources/msk_firehose_event.json index 6b839912d..140908250 100644 --- a/aws-lambda-java-tests/src/test/resources/msk_firehose_event.json +++ b/aws-lambda-java-tests/src/test/resources/msk_firehose_event.json @@ -15,4 +15,4 @@ "kafkaRecordValue": "eyJOYW1lIjoiSGVsbG8gV29ybGQifQ==" } ] -} +} \ No newline at end of file diff --git a/aws-lambda-java-tests/src/test/resources/msk_firehose_event_roundtrip.json b/aws-lambda-java-tests/src/test/resources/msk_firehose_event_roundtrip.json new file mode 100644 index 000000000..81b0a9c81 --- /dev/null +++ b/aws-lambda-java-tests/src/test/resources/msk_firehose_event_roundtrip.json @@ -0,0 +1,18 @@ +{ + "invocationId": "12345621-4787-0000-a418-36e56Example", + "sourceMSKArn": "arn:aws:kafka:EXAMPLE", + "deliveryStreamArn": "arn:aws:firehose:EXAMPLE", + "region": "us-east-1", + "records": [ + { + "recordId": "00000000000000000000000000000000000000000000000000000000000000", + "approximateArrivalTimestamp": 1716369573887, + "mskRecordMetadata": { + "offset": "0", + "partitionId": "1", + "approximateArrivalTimestamp": "1716369573887" + }, + "kafkaRecordValue": "eyJOYW1lIjoiSGVsbG8gV29ybGQifQ==" + } + ] +} \ No newline at end of file diff --git a/aws-lambda-java-tests/src/test/resources/partial_pojo.json b/aws-lambda-java-tests/src/test/resources/partial_pojo.json new file mode 100644 index 000000000..398218039 --- /dev/null +++ b/aws-lambda-java-tests/src/test/resources/partial_pojo.json @@ -0,0 +1,4 @@ +{ + "name": "test", + "unknownField": "this will be dropped" +} \ No newline at end of file diff --git a/aws-lambda-java-tests/src/test/resources/rabbitmq_event_roundtrip.json b/aws-lambda-java-tests/src/test/resources/rabbitmq_event_roundtrip.json new file mode 100644 index 000000000..44edf2f0a --- /dev/null +++ b/aws-lambda-java-tests/src/test/resources/rabbitmq_event_roundtrip.json @@ -0,0 +1,51 @@ +{ + "eventSource": "aws:rmq", + "eventSourceArn": "arn:aws:mq:us-west-2:112556298976:broker:test:b-9bcfa592-423a-4942-879d-eb284b418fc8", + "rmqMessagesByQueue": { + "test::/": [ + { + "basicProperties": { + "contentType": "text/plain", + "contentEncoding": null, + "headers": { + "header1": { + "bytes": [ + 118, + 97, + 108, + 117, + 101, + 49 + ] + }, + "header2": { + "bytes": [ + 118, + 97, + 108, + 117, + 101, + 50 + ] + }, + "numberInHeader": 10 + }, + "deliveryMode": 1, + "priority": 34, + "correlationId": null, + "replyTo": null, + "expiration": 60000, + "messageId": null, + "timestamp": "Jan 1, 1970, 12:33:41 AM", + "type": null, + "userId": "AIDACKCEVSQ6C2EXAMPLE", + "appId": null, + "clusterId": null, + "bodySize": 80 + }, + "redelivered": false, + "data": "eyJ0aW1lb3V0IjowLCJkYXRhIjoiQ1pybWYwR3c4T3Y0YnFMUXhENEUifQ==" + } + ] + } +} \ No newline at end of file diff --git a/aws-lambda-java-tests/src/test/resources/response/alb_response.json b/aws-lambda-java-tests/src/test/resources/response/alb_response.json new file mode 100644 index 000000000..355eb193a --- /dev/null +++ b/aws-lambda-java-tests/src/test/resources/response/alb_response.json @@ -0,0 +1,15 @@ +{ + "statusCode": 200, + "statusDescription": "200 OK", + "headers": { + "Content-Type": "text/html" + }, + "multiValueHeaders": { + "Set-Cookie": [ + "cookie1=value1", + "cookie2=value2" + ] + }, + "body": "Hello", + "isBase64Encoded": false +} \ No newline at end of file diff --git a/aws-lambda-java-tests/src/test/resources/response/apigw_proxy_response.json b/aws-lambda-java-tests/src/test/resources/response/apigw_proxy_response.json new file mode 100644 index 000000000..640ccdc5c --- /dev/null +++ b/aws-lambda-java-tests/src/test/resources/response/apigw_proxy_response.json @@ -0,0 +1,15 @@ +{ + "statusCode": 200, + "headers": { + "Content-Type": "application/json", + "X-Custom-Header": "custom-value" + }, + "multiValueHeaders": { + "Set-Cookie": [ + "cookie1=value1", + "cookie2=value2" + ] + }, + "body": "{\"message\":\"Hello from Lambda\"}", + "isBase64Encoded": false +} \ No newline at end of file diff --git a/aws-lambda-java-tests/src/test/resources/response/apigw_v2_http_response.json b/aws-lambda-java-tests/src/test/resources/response/apigw_v2_http_response.json new file mode 100644 index 000000000..c39236650 --- /dev/null +++ b/aws-lambda-java-tests/src/test/resources/response/apigw_v2_http_response.json @@ -0,0 +1,16 @@ +{ + "statusCode": 200, + "headers": { + "Content-Type": "application/json" + }, + "multiValueHeaders": { + "Set-Cookie": [ + "cookie1=value1" + ] + }, + "cookies": [ + "session=abc123; Secure; HttpOnly" + ], + "body": "{\"message\":\"OK\"}", + "isBase64Encoded": false +} \ No newline at end of file diff --git a/aws-lambda-java-tests/src/test/resources/response/apigw_v2_websocket_response.json b/aws-lambda-java-tests/src/test/resources/response/apigw_v2_websocket_response.json new file mode 100644 index 000000000..08392e890 --- /dev/null +++ b/aws-lambda-java-tests/src/test/resources/response/apigw_v2_websocket_response.json @@ -0,0 +1,14 @@ +{ + "statusCode": 200, + "headers": { + "Content-Type": "application/json" + }, + "multiValueHeaders": { + "X-Custom": [ + "val1", + "val2" + ] + }, + "body": "{\"action\":\"sendmessage\",\"data\":\"hello\"}", + "isBase64Encoded": false +} \ No newline at end of file diff --git a/aws-lambda-java-tests/src/test/resources/response/msk_firehose_response.json b/aws-lambda-java-tests/src/test/resources/response/msk_firehose_response.json new file mode 100644 index 000000000..9ac497624 --- /dev/null +++ b/aws-lambda-java-tests/src/test/resources/response/msk_firehose_response.json @@ -0,0 +1,9 @@ +{ + "records": [ + { + "recordId": "record-1", + "result": "Ok", + "kafkaRecordValue": "dHJhbnNmb3JtZWQgZGF0YQ==" + } + ] +} \ No newline at end of file diff --git a/aws-lambda-java-tests/src/test/resources/response/s3_batch_response.json b/aws-lambda-java-tests/src/test/resources/response/s3_batch_response.json new file mode 100644 index 000000000..e63439d84 --- /dev/null +++ b/aws-lambda-java-tests/src/test/resources/response/s3_batch_response.json @@ -0,0 +1,12 @@ +{ + "invocationSchemaVersion": "1.0", + "treatMissingKeysAs": "PermanentFailure", + "invocationId": "YXNkbGZqYWRmaiBhc2RmdW9hZHNmZGpmaGFzbGtkaGZza2RmaAo", + "results": [ + { + "taskId": "dGFza2lkZ29lc2hlcmUK", + "resultCode": "Succeeded", + "resultString": "Successfully processed" + } + ] +} \ No newline at end of file diff --git a/aws-lambda-java-tests/src/test/resources/response/simple_iam_policy_response.json b/aws-lambda-java-tests/src/test/resources/response/simple_iam_policy_response.json new file mode 100644 index 000000000..5f23b6405 --- /dev/null +++ b/aws-lambda-java-tests/src/test/resources/response/simple_iam_policy_response.json @@ -0,0 +1,7 @@ +{ + "isAuthorized": true, + "context": { + "userId": "user-123", + "scope": "read:all" + } +} \ No newline at end of file diff --git a/aws-lambda-java-tests/src/test/resources/response/sqs_batch_response.json b/aws-lambda-java-tests/src/test/resources/response/sqs_batch_response.json new file mode 100644 index 000000000..5ef2de697 --- /dev/null +++ b/aws-lambda-java-tests/src/test/resources/response/sqs_batch_response.json @@ -0,0 +1,7 @@ +{ + "batchItemFailures": [ + { + "itemIdentifier": "059f36b4-87a3-44ab-83d2-661975830a7d" + } + ] +} \ No newline at end of file diff --git a/aws-lambda-java-tests/src/test/resources/s3_batch_event.json b/aws-lambda-java-tests/src/test/resources/s3_batch_event.json new file mode 100644 index 000000000..a70af0fdd --- /dev/null +++ b/aws-lambda-java-tests/src/test/resources/s3_batch_event.json @@ -0,0 +1,15 @@ +{ + "invocationSchemaVersion": "1.0", + "invocationId": "YXNkbGZqYWRmaiBhc2RmdW9hZHNmZGpmaGFzbGtkaGZza2RmaAo", + "job": { + "id": "f3cc4f60-61f6-4a2b-8a21-d07600c373ce" + }, + "tasks": [ + { + "taskId": "dGFza2lkZ29lc2hlcmUK", + "s3Key": "customerImage1.jpg", + "s3VersionId": "1", + "s3BucketArn": "arn:aws:s3:::amzn-s3-demo-bucket" + } + ] +} \ No newline at end of file diff --git a/aws-lambda-java-tests/src/test/resources/s3_event.json b/aws-lambda-java-tests/src/test/resources/s3_event.json index 89e0bd312..73f59d072 100644 --- a/aws-lambda-java-tests/src/test/resources/s3_event.json +++ b/aws-lambda-java-tests/src/test/resources/s3_event.json @@ -28,8 +28,10 @@ }, "object": { "key": "test/key", + "urlDecodedKey": "test/key", "size": 1024, "eTag": "0123456789abcdef0123456789abcdef", + "versionId": "", "sequencer": "0A1B2C3D4E5F678901" } } diff --git a/aws-lambda-java-tests/src/test/resources/s3_object_lambda_event.json b/aws-lambda-java-tests/src/test/resources/s3_object_lambda_event.json new file mode 100644 index 000000000..db996e71c --- /dev/null +++ b/aws-lambda-java-tests/src/test/resources/s3_object_lambda_event.json @@ -0,0 +1,29 @@ +{ + "xAmzRequestId": "requestId", + "getObjectContext": { + "inputS3Url": "https://my-s3-ap-111122223333.s3-accesspoint.us-east-1.amazonaws.com/example?X-Amz-Security-Token=snip", + "outputRoute": "io-use1-001", + "outputToken": "OutputToken" + }, + "configuration": { + "accessPointArn": "arn:aws:s3-object-lambda:us-east-1:111122223333:accesspoint/example-object-lambda-ap", + "supportingAccessPointArn": "arn:aws:s3:us-east-1:111122223333:accesspoint/example-ap", + "payload": "{}" + }, + "userRequest": { + "url": "https://object-lambda-111122223333.s3-object-lambda.us-east-1.amazonaws.com/example", + "headers": { + "Host": "object-lambda-111122223333.s3-object-lambda.us-east-1.amazonaws.com", + "Accept-Encoding": "identity", + "X-Amz-Content-SHA256": "e3b0c44298fc1example" + } + }, + "userIdentity": { + "type": "AssumedRole", + "principalId": "principalId", + "arn": "arn:aws:sts::111122223333:assumed-role/Admin/example", + "accountId": "111122223333", + "accessKeyId": "accessKeyId" + }, + "protocolVersion": "1.00" +} \ No newline at end of file diff --git a/aws-lambda-java-tests/src/test/resources/time_window_event_response.json b/aws-lambda-java-tests/src/test/resources/time_window_event_response.json new file mode 100644 index 000000000..3c77b3784 --- /dev/null +++ b/aws-lambda-java-tests/src/test/resources/time_window_event_response.json @@ -0,0 +1,10 @@ +{ + "state": { + "totalAmount": "500" + }, + "batchItemFailures": [ + { + "itemIdentifier": "49590338271490256608559692538361571095921575989136588898" + } + ] +} \ No newline at end of file diff --git a/aws-lambda-java-tests/src/test/resources/unstable_pojo.json b/aws-lambda-java-tests/src/test/resources/unstable_pojo.json new file mode 100644 index 000000000..19db9a1cc --- /dev/null +++ b/aws-lambda-java-tests/src/test/resources/unstable_pojo.json @@ -0,0 +1,3 @@ +{ + "name": "test" +} \ No newline at end of file diff --git a/experimental/aws-lambda-java-profiler/README.md b/experimental/aws-lambda-java-profiler/README.md index ccc66399e..c15c22791 100644 --- a/experimental/aws-lambda-java-profiler/README.md +++ b/experimental/aws-lambda-java-profiler/README.md @@ -83,6 +83,25 @@ When the agent is constructed, it starts the profiler and registers itself as a A new thread is created to handle calling `/next` and uploading the results of the profiler to S3. The bucket to upload the result to is configurable using an environment variable. +### Custom Parameters for the Profiler + +Users can configure the profiler output by setting environment variables. + +``` +# Example: Output as JFR format instead of HTML +AWS_LAMBDA_PROFILER_START_COMMAND="start,event=wall,interval=1us,file=/tmp/profile.jfr" +AWS_LAMBDA_PROFILER_STOP_COMMAND="stop,file=%s" +``` + +Defaults are the following: + +``` +AWS_LAMBDA_PROFILER_START_COMMAND="start,event=wall,interval=1us" +AWS_LAMBDA_PROFILER_STOP_COMMAND="stop,file=%s,include=*AWSLambda.main,include=start_thread" +``` + +See [async-profiler's ProfilerOptions](https://github.com/async-profiler/async-profiler/blob/master/docs/ProfilerOptions.md) for all available profiler parameters. + ### Troubleshooting - Ensure the Lambda function execution role has the necessary permissions to write to the S3 bucket. diff --git a/experimental/aws-lambda-java-profiler/examples/cdk/pom.xml b/experimental/aws-lambda-java-profiler/examples/cdk/pom.xml index 01bbf0d67..4b46f4e2b 100644 --- a/experimental/aws-lambda-java-profiler/examples/cdk/pom.xml +++ b/experimental/aws-lambda-java-profiler/examples/cdk/pom.xml @@ -11,7 +11,7 @@ UTF-8 2.155.0 [10.0.0,11.0.0) - 5.7.1 + 5.12.2 diff --git a/experimental/aws-lambda-java-profiler/examples/function/profiling-example/pom.xml b/experimental/aws-lambda-java-profiler/examples/function/profiling-example/pom.xml index c7465bfd8..ac1001009 100644 --- a/experimental/aws-lambda-java-profiler/examples/function/profiling-example/pom.xml +++ b/experimental/aws-lambda-java-profiler/examples/function/profiling-example/pom.xml @@ -46,7 +46,7 @@ org.apache.maven.plugins maven-shade-plugin - 3.2.4 + 3.6.1 diff --git a/experimental/aws-lambda-java-profiler/extension/src/main/java/com/amazonaws/services/lambda/extension/Constants.java b/experimental/aws-lambda-java-profiler/extension/src/main/java/com/amazonaws/services/lambda/extension/Constants.java new file mode 100644 index 000000000..f9ca3010c --- /dev/null +++ b/experimental/aws-lambda-java-profiler/extension/src/main/java/com/amazonaws/services/lambda/extension/Constants.java @@ -0,0 +1,29 @@ +package com.amazonaws.services.lambda.extension; + +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +public class Constants { + + private static final String DEFAULT_AWS_LAMBDA_PROFILER_START_COMMAND = + "start,event=wall,interval=1us"; + private static final String DEFAULT_AWS_LAMBDA_PROFILER_STOP_COMMAND = + "stop,file=%s,include=*AWSLambda.main,include=start_thread"; + public static final String PROFILER_START_COMMAND = + System.getenv().getOrDefault( + "AWS_LAMBDA_PROFILER_START_COMMAND", + DEFAULT_AWS_LAMBDA_PROFILER_START_COMMAND + ); + public static final String PROFILER_STOP_COMMAND = + System.getenv().getOrDefault( + "AWS_LAMBDA_PROFILER_STOP_COMMAND", + DEFAULT_AWS_LAMBDA_PROFILER_STOP_COMMAND + ); + + public static String getFilePathFromEnv(){ + Pattern pattern = Pattern.compile("file=([^,]+)"); + Matcher matcher = pattern.matcher(PROFILER_START_COMMAND); + + return matcher.find() ? matcher.group(1) : "/tmp/profiling-data-%s.html"; + } +} diff --git a/experimental/aws-lambda-java-profiler/extension/src/main/java/com/amazonaws/services/lambda/extension/PreMain.java b/experimental/aws-lambda-java-profiler/extension/src/main/java/com/amazonaws/services/lambda/extension/PreMain.java index c0522641a..2a84eb641 100644 --- a/experimental/aws-lambda-java-profiler/extension/src/main/java/com/amazonaws/services/lambda/extension/PreMain.java +++ b/experimental/aws-lambda-java-profiler/extension/src/main/java/com/amazonaws/services/lambda/extension/PreMain.java @@ -2,49 +2,57 @@ // SPDX-License-Identifier: MIT-0 package com.amazonaws.services.lambda.extension; +import com.sun.net.httpserver.HttpExchange; +import com.sun.net.httpserver.HttpHandler; +import com.sun.net.httpserver.HttpServer; import java.io.File; import java.io.IOException; import java.io.OutputStream; import java.lang.instrument.Instrumentation; import java.net.InetSocketAddress; import java.nio.charset.StandardCharsets; +import one.profiler.AsyncProfiler; -import com.sun.net.httpserver.HttpExchange; -import com.sun.net.httpserver.HttpHandler; -import com.sun.net.httpserver.HttpServer; +import static com.amazonaws.services.lambda.extension.Constants.PROFILER_START_COMMAND; +import static com.amazonaws.services.lambda.extension.Constants.PROFILER_STOP_COMMAND; + +public class PreMain { -import one.profiler.AsyncProfiler; -public class PreMain { + private static final String INTERNAL_COMMUNICATION_PORT = + System.getenv().getOrDefault( + "AWS_LAMBDA_PROFILER_COMMUNICATION_PORT", + "1234" + ); - private static final String DEFAULT_AWS_LAMBDA_PROFILER_START_COMMAND = "start,event=wall,interval=1us"; - private static final String DEFAULT_AWS_LAMBDA_PROFILER_STOP_COMMAND = "stop,file=%s,include=*AWSLambda.main,include=start_thread"; - private static final String PROFILER_START_COMMAND = System.getenv().getOrDefault("AWS_LAMBDA_PROFILER_START_COMMAND", DEFAULT_AWS_LAMBDA_PROFILER_START_COMMAND); - private static final String PROFILER_STOP_COMMAND = System.getenv().getOrDefault("AWS_LAMBDA_PROFILER_STOP_COMMAND", DEFAULT_AWS_LAMBDA_PROFILER_STOP_COMMAND); - private static final String INTERNAL_COMMUNICATION_PORT = System.getenv().getOrDefault("AWS_LAMBDA_PROFILER_COMMUNICATION_PORT", "1234"); + + private String filepath; public static void premain(String agentArgs, Instrumentation inst) { Logger.debug("premain is starting"); - if(!createFileIfNotExist("/tmp/aws-lambda-java-profiler")) { + if (!createFileIfNotExist("/tmp/aws-lambda-java-profiler")) { Logger.debug("starting the profiler for coldstart"); startProfiler(); registerShutdownHook(); try { Integer port = Integer.parseInt(INTERNAL_COMMUNICATION_PORT); Logger.debug("using profile communication port = " + port); - HttpServer server = HttpServer.create(new InetSocketAddress(port), 0); + HttpServer server = HttpServer.create( + new InetSocketAddress(port), + 0 + ); server.createContext("/profiler/start", new StartProfiler()); server.createContext("/profiler/stop", new StopProfiler()); server.setExecutor(null); // Use the default executor server.start(); - } catch(Exception e) { + } catch (Exception e) { e.printStackTrace(); } } } private static boolean createFileIfNotExist(String filePath) { - File file = new File(filePath); + File file = new File(filePath); try { return file.createNewFile(); } catch (IOException e) { @@ -54,10 +62,13 @@ private static boolean createFileIfNotExist(String filePath) { } public static class StopProfiler implements HttpHandler { + @Override public void handle(HttpExchange exchange) throws IOException { Logger.debug("hit /profiler/stop"); - final String fileName = exchange.getRequestHeaders().getFirst(ExtensionMain.HEADER_NAME); + final String fileName = exchange + .getRequestHeaders() + .getFirst(ExtensionMain.HEADER_NAME); stopProfiler(fileName); String response = "ok"; exchange.sendResponseHeaders(200, response.length()); @@ -68,6 +79,7 @@ public void handle(HttpExchange exchange) throws IOException { } public static class StartProfiler implements HttpHandler { + @Override public void handle(HttpExchange exchange) throws IOException { Logger.debug("hit /profiler/start"); @@ -80,13 +92,19 @@ public void handle(HttpExchange exchange) throws IOException { } } - public static void stopProfiler(String fileNameSuffix) { try { - final String fileName = String.format("/tmp/profiling-data-%s.html", fileNameSuffix); - Logger.debug("stopping the profiler with filename = " + fileName + " with command = " + PROFILER_STOP_COMMAND); - AsyncProfiler.getInstance().execute(String.format(PROFILER_STOP_COMMAND, fileName)); - } catch(Exception e) { + final String fileName = String.format( + Constants.getFilePathFromEnv(), + fileNameSuffix + ); + Logger.debug( + "stopping the profiler with filename = " + fileName + ); + AsyncProfiler.getInstance().execute( + String.format(PROFILER_STOP_COMMAND, fileName) + ); + } catch (Exception e) { Logger.error("could not stop the profiler"); e.printStackTrace(); } @@ -94,7 +112,9 @@ public static void stopProfiler(String fileNameSuffix) { public static void startProfiler() { try { - Logger.debug("staring the profiler with command = " + PROFILER_START_COMMAND); + Logger.debug( + "starting the profiler with command = " + PROFILER_START_COMMAND + ); AsyncProfiler.getInstance().execute(PROFILER_START_COMMAND); } catch (IOException e) { throw new RuntimeException(e); @@ -102,9 +122,10 @@ public static void startProfiler() { } public static void registerShutdownHook() { - Logger.debug("registering shutdown hook"); - Thread shutdownHook = new Thread(new ShutdownHook(PROFILER_STOP_COMMAND)); + Logger.debug("registering shutdown hook wit command = " + PROFILER_STOP_COMMAND); + Thread shutdownHook = new Thread( + new ShutdownHook(PROFILER_STOP_COMMAND) + ); Runtime.getRuntime().addShutdownHook(shutdownHook); } - -} \ No newline at end of file +} diff --git a/experimental/aws-lambda-java-profiler/extension/src/main/java/com/amazonaws/services/lambda/extension/S3Manager.java b/experimental/aws-lambda-java-profiler/extension/src/main/java/com/amazonaws/services/lambda/extension/S3Manager.java index 3b55984c5..0e31a2421 100644 --- a/experimental/aws-lambda-java-profiler/extension/src/main/java/com/amazonaws/services/lambda/extension/S3Manager.java +++ b/experimental/aws-lambda-java-profiler/extension/src/main/java/com/amazonaws/services/lambda/extension/S3Manager.java @@ -4,7 +4,6 @@ import java.io.File; import java.time.format.DateTimeFormatter; -import java.time.Instant; import java.time.LocalDate; import software.amazon.awssdk.core.sync.RequestBody; @@ -39,7 +38,7 @@ public void upload(String fileName, boolean isShutDownEvent) { .bucket(bucketName) .key(key) .build(); - File file = new File(String.format("/tmp/profiling-data-%s.html", suffix)); + File file = new File(String.format(Constants.getFilePathFromEnv(), suffix)); if (file.exists()) { Logger.debug("file size is " + file.length()); RequestBody requestBody = RequestBody.fromFile(file); diff --git a/experimental/aws-lambda-java-profiler/integration_tests/create_function.sh b/experimental/aws-lambda-java-profiler/integration_tests/create_function.sh index 114909d09..12ba1cb2b 100755 --- a/experimental/aws-lambda-java-profiler/integration_tests/create_function.sh +++ b/experimental/aws-lambda-java-profiler/integration_tests/create_function.sh @@ -2,6 +2,7 @@ # Set variables FUNCTION_NAME="aws-lambda-java-profiler-function-${GITHUB_RUN_ID}" +FUNCTION_NAME_CUSTOM_PROFILER_OPTIONS="aws-lambda-java-profiler-function-custom-${GITHUB_RUN_ID}" ROLE_NAME="aws-lambda-java-profiler-role-${GITHUB_RUN_ID}" HANDLER="helloworld.Handler::handleRequest" RUNTIME="java21" @@ -9,6 +10,8 @@ LAYER_ARN=$(cat /tmp/layer_arn) JAVA_TOOL_OPTIONS="-XX:+UnlockDiagnosticVMOptions -XX:+DebugNonSafepoints -javaagent:/opt/profiler-extension.jar" AWS_LAMBDA_PROFILER_RESULTS_BUCKET_NAME="aws-lambda-java-profiler-bucket-${GITHUB_RUN_ID}" +AWS_LAMBDA_PROFILER_START_COMMAND="start,event=wall,interval=1us,file=/tmp/profile.jfr" +AWS_LAMBDA_PROFILER_STOP_COMMAND="stop,file=%s" # Compile the Hello World project cd integration_tests/helloworld @@ -63,6 +66,19 @@ aws lambda create-function \ --environment "Variables={JAVA_TOOL_OPTIONS='$JAVA_TOOL_OPTIONS',AWS_LAMBDA_PROFILER_RESULTS_BUCKET_NAME='$AWS_LAMBDA_PROFILER_RESULTS_BUCKET_NAME',AWS_LAMBDA_PROFILER_DEBUG='true'}" \ --layers "$LAYER_ARN" + +# Create Lambda function custom profiler options +aws lambda create-function \ + --function-name "$FUNCTION_NAME_CUSTOM_PROFILER_OPTIONS" \ + --runtime "$RUNTIME" \ + --role "$ROLE_ARN" \ + --handler "$HANDLER" \ + --timeout 30 \ + --memory-size 512 \ + --zip-file fileb://integration_tests/helloworld/build/distributions/code.zip \ + --environment "Variables={JAVA_TOOL_OPTIONS='$JAVA_TOOL_OPTIONS',AWS_LAMBDA_PROFILER_RESULTS_BUCKET_NAME='$AWS_LAMBDA_PROFILER_RESULTS_BUCKET_NAME',AWS_LAMBDA_PROFILER_DEBUG='true',AWS_LAMBDA_PROFILER_START_COMMAND='$AWS_LAMBDA_PROFILER_START_COMMAND',AWS_LAMBDA_PROFILER_STOP_COMMAND='$AWS_LAMBDA_PROFILER_STOP_COMMAND'}" \ + --layers "$LAYER_ARN" + echo "Lambda function '$FUNCTION_NAME' created successfully with Java 21 runtime" echo "Waiting the function to be ready so we can invoke it..." diff --git a/experimental/aws-lambda-java-profiler/integration_tests/helloworld/build.gradle b/experimental/aws-lambda-java-profiler/integration_tests/helloworld/build.gradle index 927317f8f..79ffa030a 100644 --- a/experimental/aws-lambda-java-profiler/integration_tests/helloworld/build.gradle +++ b/experimental/aws-lambda-java-profiler/integration_tests/helloworld/build.gradle @@ -1,11 +1,15 @@ -apply plugin: 'java' +plugins { + id 'java' +} repositories { mavenCentral() } -sourceCompatibility = 21 -targetCompatibility = 21 +java { + sourceCompatibility = JavaVersion.VERSION_21 + targetCompatibility = JavaVersion.VERSION_21 +} dependencies { implementation ( @@ -24,4 +28,5 @@ task buildZip(type: Zip) { } } + build.dependsOn buildZip \ No newline at end of file diff --git a/experimental/aws-lambda-java-profiler/integration_tests/invoke_function.sh b/experimental/aws-lambda-java-profiler/integration_tests/invoke_function.sh index 741eec140..39b0dd885 100755 --- a/experimental/aws-lambda-java-profiler/integration_tests/invoke_function.sh +++ b/experimental/aws-lambda-java-profiler/integration_tests/invoke_function.sh @@ -32,8 +32,8 @@ fi echo "Function output:" cat output.json -echo "$LOG_RESULT" | base64 --decode | grep "starting the profiler for coldstart" || exit 1 -echo "$LOG_RESULT" | base64 --decode | grep -v "uploading" || exit 1 +echo "$LOG_RESULT" | base64 --decode | grep "starting the profiler for coldstart" || { echo "ERROR: Profiler did not start for coldstart"; exit 1; } +echo "$LOG_RESULT" | base64 --decode | grep -v "uploading" || { echo "ERROR: Unexpected upload detected on cold start"; exit 1; } # Clean up the output file rm output.json @@ -68,7 +68,7 @@ fi echo "Function output:" cat output.json -echo "$LOG_RESULT" | base64 --decode | grep "uploading" || exit 1 +echo "$LOG_RESULT" | base64 --decode | grep "uploading" || { echo "ERROR: Upload not detected on warm start"; exit 1; } # Clean up the output file rm output.json diff --git a/experimental/aws-lambda-java-profiler/integration_tests/invoke_function_custom_options.sh b/experimental/aws-lambda-java-profiler/integration_tests/invoke_function_custom_options.sh new file mode 100755 index 000000000..6cf927ae0 --- /dev/null +++ b/experimental/aws-lambda-java-profiler/integration_tests/invoke_function_custom_options.sh @@ -0,0 +1,86 @@ +#!/bin/bash + +# Set variables +FUNCTION_NAME_CUSTOM_PROFILER_OPTIONS="aws-lambda-java-profiler-function-custom-${GITHUB_RUN_ID}" +PAYLOAD='{"key": "value"}' + +# Expected profiler commands (should match create_function.sh) +EXPECTED_START_COMMAND="start,event=wall,interval=1us,file=/tmp/profile.jfr" +EXPECTED_STOP_COMMAND="stop,file=%s" + +echo "Invoking Lambda function with custom profiler options: $FUNCTION_NAME_CUSTOM_PROFILER_OPTIONS" + +# Invoke the Lambda function synchronously and capture the response +RESPONSE=$(aws lambda invoke \ + --function-name "$FUNCTION_NAME_CUSTOM_PROFILER_OPTIONS" \ + --payload "$PAYLOAD" \ + --cli-binary-format raw-in-base64-out \ + --log-type Tail \ + output.json) + +# Extract the status code and log result from the response +STATUS_CODE=$(echo "$RESPONSE" | jq -r '.StatusCode') +LOG_RESULT=$(echo "$RESPONSE" | jq -r '.LogResult') + +echo "Function invocation completed with status code: $STATUS_CODE" + +# Decode and display the logs +if [ -n "$LOG_RESULT" ]; then + echo "Function logs:" + echo "$LOG_RESULT" | base64 --decode +else + echo "No logs available." +fi + +# Display the function output +echo "Function output:" +cat output.json + +# Verify profiler started +echo "$LOG_RESULT" | base64 --decode | grep "starting the profiler for coldstart" || { echo "ERROR: Profiler did not start for coldstart"; exit 1; } + +# Verify custom start command is being used +echo "$LOG_RESULT" | base64 --decode | grep "$EXPECTED_START_COMMAND" || { echo "ERROR: Expected start command not found: $EXPECTED_START_COMMAND"; exit 1; } +echo "$LOG_RESULT" | base64 --decode | grep "$EXPECTED_STOP_COMMAND" || { echo "ERROR: Expected stop command not found: $EXPECTED_STOP_COMMAND"; exit 1; } + +# Verify no upload on cold start +echo "$LOG_RESULT" | base64 --decode | grep -v "uploading" || { echo "ERROR: Unexpected upload detected on cold start"; exit 1; } + +# Clean up the output file +rm output.json + + +# Invoke it a second time for warm start +echo "Invoking Lambda function (warm start): $FUNCTION_NAME_CUSTOM_PROFILER_OPTIONS" + +# Invoke the Lambda function synchronously and capture the response +RESPONSE=$(aws lambda invoke \ + --function-name "$FUNCTION_NAME_CUSTOM_PROFILER_OPTIONS" \ + --payload "$PAYLOAD" \ + --cli-binary-format raw-in-base64-out \ + --log-type Tail \ + output.json) + +# Extract the status code and log result from the response +STATUS_CODE=$(echo "$RESPONSE" | jq -r '.StatusCode') +LOG_RESULT=$(echo "$RESPONSE" | jq -r '.LogResult') + +echo "Function invocation completed with status code: $STATUS_CODE" + +# Decode and display the logs +if [ -n "$LOG_RESULT" ]; then + echo "Function logs:" + echo "$LOG_RESULT" | base64 --decode +else + echo "No logs available." +fi + +# Display the function output +echo "Function output:" +cat output.json + +# Verify upload happens on warm start +echo "$LOG_RESULT" | base64 --decode | grep "uploading" || { echo "ERROR: Upload not detected on warm start"; exit 1; } + +# Clean up the output file +rm output.json diff --git a/samples/custom-serialization/fastJson/HelloWorldFunction/pom.xml b/samples/custom-serialization/fastJson/HelloWorldFunction/pom.xml index 7d3c44246..2a963ca21 100644 --- a/samples/custom-serialization/fastJson/HelloWorldFunction/pom.xml +++ b/samples/custom-serialization/fastJson/HelloWorldFunction/pom.xml @@ -35,7 +35,7 @@ org.apache.maven.plugins maven-shade-plugin - 3.2.4 + 3.6.1 diff --git a/samples/custom-serialization/gson/HelloWorldFunction/pom.xml b/samples/custom-serialization/gson/HelloWorldFunction/pom.xml index fd4271824..47d04926a 100644 --- a/samples/custom-serialization/gson/HelloWorldFunction/pom.xml +++ b/samples/custom-serialization/gson/HelloWorldFunction/pom.xml @@ -34,7 +34,7 @@ org.apache.maven.plugins maven-shade-plugin - 3.2.4 + 3.6.1 diff --git a/samples/custom-serialization/jackson-jr/HelloWorldFunction/build.gradle b/samples/custom-serialization/jackson-jr/HelloWorldFunction/build.gradle index 71c89b7ac..480abfded 100644 --- a/samples/custom-serialization/jackson-jr/HelloWorldFunction/build.gradle +++ b/samples/custom-serialization/jackson-jr/HelloWorldFunction/build.gradle @@ -14,5 +14,7 @@ dependencies { implementation 'com.fasterxml.jackson.core:jackson-annotations:2.15.2' } -sourceCompatibility = 21 -targetCompatibility = 21 +java { + sourceCompatibility = JavaVersion.VERSION_21 + targetCompatibility = JavaVersion.VERSION_21 +} diff --git a/samples/custom-serialization/moshi/HelloWorldFunction/pom.xml b/samples/custom-serialization/moshi/HelloWorldFunction/pom.xml index 2773ef1f1..60277f10b 100644 --- a/samples/custom-serialization/moshi/HelloWorldFunction/pom.xml +++ b/samples/custom-serialization/moshi/HelloWorldFunction/pom.xml @@ -35,7 +35,7 @@ org.apache.maven.plugins maven-shade-plugin - 3.2.4 + 3.6.1 diff --git a/samples/custom-serialization/request-stream-handler/HelloWorldFunction/pom.xml b/samples/custom-serialization/request-stream-handler/HelloWorldFunction/pom.xml index f6ca21bf7..15e16439d 100644 --- a/samples/custom-serialization/request-stream-handler/HelloWorldFunction/pom.xml +++ b/samples/custom-serialization/request-stream-handler/HelloWorldFunction/pom.xml @@ -34,7 +34,7 @@ org.apache.maven.plugins maven-shade-plugin - 3.2.4 + 3.6.1 diff --git a/samples/kinesis-firehose-event-handler/pom.xml b/samples/kinesis-firehose-event-handler/pom.xml index 56c959244..0db8ed83a 100644 --- a/samples/kinesis-firehose-event-handler/pom.xml +++ b/samples/kinesis-firehose-event-handler/pom.xml @@ -35,6 +35,9 @@ 1.8 1.8 UTF-8 + 5.12.2 + 3.5.4 + @@ -52,7 +55,7 @@ org.junit.jupiter junit-jupiter - RELEASE + ${junit-jupiter.version} test @@ -68,7 +71,10 @@ org.apache.maven.plugins maven-surefire-plugin - 2.22.2 + ${maven-surefire-plugin.version} + + true +