diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md index ab40d21d7..d9fdc8b1d 100644 --- a/.github/PULL_REQUEST_TEMPLATE.md +++ b/.github/PULL_REQUEST_TEMPLATE.md @@ -2,5 +2,7 @@ *Description of changes:* +*Target (OCI, Managed Runtime, both):* + By submitting this pull request, I confirm that my contribution is made under the terms of the Apache 2.0 license. diff --git a/.github/dependabot.yml b/.github/dependabot.yml new file mode 100644 index 000000000..88f18ea29 --- /dev/null +++ b/.github/dependabot.yml @@ -0,0 +1,10 @@ +version: 2 +updates: + - package-ecosystem: "maven" + directory: "/aws-lambda-java-runtime-interface" + schedule: + interval: "weekly" + - package-ecosystem: "github-actions" + directory: "/" + schedule: + interval: "weekly" \ No newline at end of file diff --git a/.github/workflows/aws-lambda-java-core.yml b/.github/workflows/aws-lambda-java-core.yml index 18918a3b3..b1bed919f 100644 --- a/.github/workflows/aws-lambda-java-core.yml +++ b/.github/workflows/aws-lambda-java-core.yml @@ -5,13 +5,17 @@ name: Java CI aws-lambda-java-core on: push: - branches: [ master ] + branches: [ main ] paths: - - 'aws-lambda-java-core/**' + - 'aws-lambda-java-core/**' pull_request: branches: [ '*' ] paths: - - 'aws-lambda-java-core/**' + - 'aws-lambda-java-core/**' + - '.github/workflows/aws-lambda-java-core.yml' + +permissions: + contents: read jobs: build: @@ -19,11 +23,12 @@ jobs: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v2 + - uses: actions/checkout@v5 - name: Set up JDK 1.8 - uses: actions/setup-java@v1 + uses: actions/setup-java@v4 with: - java-version: 1.8 + java-version: 8 + distribution: corretto # Install base module - name: Install core with Maven @@ -37,3 +42,5 @@ jobs: - 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 15954e6f5..1f1f08870 100644 --- a/.github/workflows/aws-lambda-java-events-sdk-transformer.yml +++ b/.github/workflows/aws-lambda-java-events-sdk-transformer.yml @@ -5,13 +5,17 @@ name: Java CI aws-lambda-java-events-sdk-transformer on: push: - branches: [ master ] + branches: [ main ] paths: - - 'aws-lambda-java-events-sdk-transformer/**' + - 'aws-lambda-java-events-sdk-transformer/**' pull_request: branches: [ '*' ] paths: - - 'aws-lambda-java-events-sdk-transformer/**' + - 'aws-lambda-java-events-sdk-transformer/**' + - '.github/workflows/aws-lambda-java-events-sdk-transformer.yml' + +permissions: + contents: read jobs: build: @@ -19,11 +23,12 @@ jobs: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v2 + - uses: actions/checkout@v5 - name: Set up JDK 1.8 - uses: actions/setup-java@v1 + uses: actions/setup-java@v4 with: - java-version: 1.8 + java-version: 8 + distribution: corretto # Install base module - name: Install events with Maven diff --git a/.github/workflows/aws-lambda-java-events.yml b/.github/workflows/aws-lambda-java-events.yml index c8dc731bb..2d101018d 100644 --- a/.github/workflows/aws-lambda-java-events.yml +++ b/.github/workflows/aws-lambda-java-events.yml @@ -5,13 +5,17 @@ name: Java CI aws-lambda-java-events on: push: - branches: [ master ] + branches: [ main ] paths: - - 'aws-lambda-java-events/**' + - 'aws-lambda-java-events/**' pull_request: branches: [ '*' ] paths: - - 'aws-lambda-java-events/**' + - 'aws-lambda-java-events/**' + - '.github/workflows/aws-lambda-java-events.yml' + +permissions: + contents: read jobs: build: @@ -19,11 +23,12 @@ jobs: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v2 + - uses: actions/checkout@v5 - name: Set up JDK 1.8 - uses: actions/setup-java@v1 + uses: actions/setup-java@v4 with: - java-version: 1.8 + java-version: 8 + distribution: corretto # Install base module - name: Install events with Maven diff --git a/.github/workflows/aws-lambda-java-log4j2.yml b/.github/workflows/aws-lambda-java-log4j2.yml index 420b8c3d6..e9f6a56c1 100644 --- a/.github/workflows/aws-lambda-java-log4j2.yml +++ b/.github/workflows/aws-lambda-java-log4j2.yml @@ -5,13 +5,17 @@ name: Java CI aws-lambda-java-log4j2 on: push: - branches: [ master ] + branches: [ main ] paths: - - 'aws-lambda-java-log4j2/**' + - 'aws-lambda-java-log4j2/**' pull_request: branches: [ '*' ] paths: - - 'aws-lambda-java-log4j2/**' + - 'aws-lambda-java-log4j2/**' + - '.github/workflows/aws-lambda-java-log4j2.yml' + +permissions: + contents: read jobs: build: @@ -19,11 +23,12 @@ jobs: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v2 + - uses: actions/checkout@v5 - name: Set up JDK 1.8 - uses: actions/setup-java@v1 + uses: actions/setup-java@v4 with: - java-version: 1.8 + java-version: 8 + distribution: corretto # Install base module - name: Install core with Maven diff --git a/.github/workflows/aws-lambda-java-profiler.yml b/.github/workflows/aws-lambda-java-profiler.yml new file mode 100644 index 000000000..a3afe3729 --- /dev/null +++ b/.github/workflows/aws-lambda-java-profiler.yml @@ -0,0 +1,78 @@ +name: Run integration tests for aws-lambda-java-profiler + +on: + pull_request: + branches: [ '*' ] + paths: + - 'experimental/aws-lambda-java-profiler/**' + - '.github/workflows/aws-lambda-java-profiler.yml' + push: + branches: ['*'] + paths: + - 'experimental/aws-lambda-java-profiler/**' + - '.github/workflows/aws-lambda-java-profiler.yml' + +jobs: + + build: + runs-on: ubuntu-latest + + permissions: + id-token: write + contents: read + + steps: + - uses: actions/checkout@v5 + + - name: Set up JDK + uses: actions/setup-java@v4 + with: + java-version: 21 + distribution: corretto + + - name: Issue AWS credentials + uses: aws-actions/configure-aws-credentials@v4 + with: + aws-region: ${{ secrets.AWS_REGION_PROFILER_EXTENSION_INTEGRATION_TEST }} + role-to-assume: ${{ secrets.AWS_ROLE_PROFILER_EXTENSION_INTEGRATION_TEST }} + role-session-name: GitHubActionsRunIntegrationTests + role-duration-seconds: 900 + + - name: Build layer + working-directory: ./experimental/aws-lambda-java-profiler/extension + run: ./build_layer.sh + + - name: Publish layer + working-directory: ./experimental/aws-lambda-java-profiler + run: ./integration_tests/publish_layer.sh + + - name: Create the bucket layer + working-directory: ./experimental/aws-lambda-java-profiler + run: ./integration_tests/create_bucket.sh + + - name: Create Java function + working-directory: ./experimental/aws-lambda-java-profiler + run: ./integration_tests/create_function.sh + + - name: Invoke Java function + 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 + with: + name: profiles + path: /tmp/s3-artifacts + + - name: cleanup + if: always() + working-directory: ./experimental/aws-lambda-java-profiler + run: ./integration_tests/cleanup.sh \ No newline at end of file diff --git a/.github/workflows/aws-lambda-java-runtime-interface-client.yml b/.github/workflows/aws-lambda-java-runtime-interface-client.yml deleted file mode 100644 index 15d5921de..000000000 --- a/.github/workflows/aws-lambda-java-runtime-interface-client.yml +++ /dev/null @@ -1,32 +0,0 @@ -# This workflow will be triggered if there will be changes to -# aws-lambda-java-runtime-interface-client package and it builds the package. - -name: Java CI aws-lambda-java-runtime-interface-client - -on: - push: - branches: [ master ] - paths: - - 'aws-lambda-java-runtime-interface-client/**' - pull_request: - branches: [ '*' ] - paths: - - 'aws-lambda-java-runtime-interface-client/**' - -jobs: - build: - - runs-on: ubuntu-latest - - steps: - - uses: actions/checkout@v2 - - name: Set up JDK 1.8 - uses: actions/setup-java@v1 - with: - java-version: 1.8 - - # Test Runtime Interface Client - - name: Run 'pr' target - working-directory: ./aws-lambda-java-runtime-interface-client - run: make pr - diff --git a/.github/workflows/aws-lambda-java-serialization.yml b/.github/workflows/aws-lambda-java-serialization.yml index 72592de8a..13b7e08b0 100644 --- a/.github/workflows/aws-lambda-java-serialization.yml +++ b/.github/workflows/aws-lambda-java-serialization.yml @@ -5,13 +5,17 @@ name: Java CI aws-lambda-java-serialization on: push: - branches: [ master ] + branches: [ main ] paths: - - 'aws-lambda-java-serialization/**' + - 'aws-lambda-java-serialization/**' pull_request: branches: [ '*' ] paths: - - 'aws-lambda-java-serialization/**' + - 'aws-lambda-java-serialization/**' + - '.github/workflows/aws-lambda-java-serialization.yml' + +permissions: + contents: read jobs: build: @@ -19,15 +23,21 @@ jobs: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v2 + - uses: actions/checkout@v5 - name: Set up JDK 1.8 - uses: actions/setup-java@v1 + uses: actions/setup-java@v4 with: - java-version: 1.8 + java-version: 8 + distribution: corretto # Install base module - name: Install events with Maven run: mvn -B install --file aws-lambda-java-events/pom.xml - # Package target module + + # Package and install target module - name: Package serialization with Maven - run: mvn -B package --file aws-lambda-java-serialization/pom.xml + run: mvn -B package install --file aws-lambda-java-serialization/pom.xml + + # Run tests + - name: Run tests from aws-lambda-java-tests + run: mvn test --file aws-lambda-java-tests/pom.xml diff --git a/.github/workflows/aws-lambda-java-tests.yml b/.github/workflows/aws-lambda-java-tests.yml index b00b3781e..720c52c11 100644 --- a/.github/workflows/aws-lambda-java-tests.yml +++ b/.github/workflows/aws-lambda-java-tests.yml @@ -5,13 +5,17 @@ name: Java CI aws-lambda-java-tests on: push: - branches: [ master ] + branches: [ main ] paths: - - 'aws-lambda-java-tests/**' + - 'aws-lambda-java-tests/**' pull_request: branches: [ '*' ] paths: - - 'aws-lambda-java-tests/**' + - 'aws-lambda-java-tests/**' + - '.github/workflows/aws-lambda-java-tests.yml' + +permissions: + contents: read jobs: build: @@ -19,11 +23,12 @@ jobs: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v2 + - uses: actions/checkout@v5 - name: Set up JDK 1.8 - uses: actions/setup-java@v1 + uses: actions/setup-java@v4 with: - java-version: 1.8 + java-version: 8 + distribution: corretto # Install base module - name: Install events with Maven diff --git a/.github/workflows/repo-sync.yml b/.github/workflows/repo-sync.yml new file mode 100644 index 000000000..2d97bc868 --- /dev/null +++ b/.github/workflows/repo-sync.yml @@ -0,0 +1,39 @@ +name: Repo Sync + +on: + schedule: + - cron: "0 8 * * 1-5" # At 08:00 on every day-of-week from Monday through Friday + pull_request: + branches: [ '*' ] + paths: + - '.github/workflows/repo-sync.yml' + workflow_dispatch: + +permissions: + contents: write + pull-requests: write + +jobs: + repo-sync: + name: Repo Sync + runs-on: ubuntu-latest + env: + IS_CONFIGURED: ${{ secrets.SOURCE_REPO != '' }} + steps: + - uses: actions/checkout@v5 + if: ${{ env.IS_CONFIGURED == 'true' }} + - uses: repo-sync/github-sync@v2 + name: Sync repo to branch + if: ${{ env.IS_CONFIGURED == 'true' }} + with: + source_repo: ${{ secrets.SOURCE_REPO }} + source_branch: main + destination_branch: ${{ secrets.INTERMEDIATE_BRANCH }} + github_token: ${{ secrets.GITHUB_TOKEN }} + - uses: repo-sync/pull-request@v2 + name: Create pull request + if: ${{ env.IS_CONFIGURED == 'true' }} + with: + source_branch: ${{ secrets.INTERMEDIATE_BRANCH }} + destination_branch: main + github_token: ${{ secrets.GITHUB_TOKEN }} diff --git a/.github/workflows/runtime-interface-client_merge_to_main.yml b/.github/workflows/runtime-interface-client_merge_to_main.yml new file mode 100644 index 000000000..3560207f3 --- /dev/null +++ b/.github/workflows/runtime-interface-client_merge_to_main.yml @@ -0,0 +1,95 @@ +# This workflow will be triggered on merge to the main branch if +# aws-lambda-java-runtime-interface-client package was changed +# +# It will publish artifacts to CodeArtifact repository, specified by properties defined in GitHub repo secrets: +# CODE_ARTIFACT_REPO_ACCOUNT, AWS_REGION, CODE_ARTIFACT_REPO_NAME, CODE_ARTIFACT_DOMAIN +# and will assume role specified by AWS_ROLE +# +# Prerequisite setup: +# https://docs.github.com/en/actions/deployment/security-hardening-your-deployments/configuring-openid-connect-in-amazon-web-services + +name: Publish artifact for aws-lambda-java-runtime-interface-client + +on: + push: + branches: [ main ] + paths: + - 'aws-lambda-java-runtime-interface-client/**' + - '.github/workflows/runtime-interface-client_*.yml' + workflow_dispatch: + +jobs: + + publish: + runs-on: ubuntu-latest + + permissions: + id-token: write + contents: read + + steps: + - uses: actions/checkout@v5 + + - name: Set up JDK 1.8 + uses: actions/setup-java@v4 + with: + java-version: 8 + distribution: corretto + + - name: Set up QEMU + uses: docker/setup-qemu-action@v3 + + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@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 + env: + IS_JAVA_8: true + + - name: Issue AWS credentials + if: env.ENABLE_SNAPSHOT != null + env: + ENABLE_SNAPSHOT: ${{ secrets.ENABLE_SNAPSHOT }} + uses: aws-actions/configure-aws-credentials@v4 + with: + aws-region: ${{ secrets.AWS_REGION }} + role-to-assume: ${{ secrets.AWS_ROLE }} + role-session-name: GitHubActionsPublishPackage + role-duration-seconds: 900 + + - name: Prepare codeartifact properties + if: env.ENABLE_SNAPSHOT != null + env: + ENABLE_SNAPSHOT: ${{ secrets.ENABLE_SNAPSHOT }} + working-directory: ./aws-lambda-java-runtime-interface-client/ric-dev-environment + run: | + cat < codeartifact-properties.mk + CODE_ARTIFACT_REPO_ACCOUNT=${{ secrets.AWS_ACCOUNT }} + CODE_ARTIFACT_REPO_REGION=${{ env.AWS_REGION }} + CODE_ARTIFACT_REPO_NAME=${{ secrets.CODE_ARTIFACT_REPO_NAME }} + CODE_ARTIFACT_DOMAIN=${{ secrets.AWS_CODEARTIFACT_DOMAIN }} + EOF + + - name: Publish + if: env.ENABLE_SNAPSHOT != null + working-directory: ./aws-lambda-java-runtime-interface-client + env: + ENABLE_SNAPSHOT: ${{ secrets.ENABLE_SNAPSHOT }} + run: make publish + + - name: Upload coverage to Codecov + if: env.CODECOV_TOKEN != null + uses: codecov/codecov-action@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 new file mode 100644 index 000000000..dcad4fa0a --- /dev/null +++ b/.github/workflows/runtime-interface-client_pr.yml @@ -0,0 +1,89 @@ +# This workflow will be triggered if there will be changes to +# aws-lambda-java-runtime-interface-client package and it builds the package. + +name: PR to runtime-interface-client + +on: + pull_request: + branches: [ '*' ] + paths: + - 'aws-lambda-java-runtime-interface-client/**' + - '.github/workflows/runtime-interface-client_*.yml' + +permissions: + contents: read + +jobs: + + smoke-test: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v5 + + - name: Set up JDK 1.8 + uses: actions/setup-java@v4 + with: + java-version: 8 + distribution: corretto + + - 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@v5 + + - name: Set up JDK 1.8 + uses: actions/setup-java@v4 + with: + java-version: 8 + distribution: corretto + + - name: Set up QEMU + uses: docker/setup-qemu-action@v3 + + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@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 + env: + IS_JAVA_8: true + + - name: Save the built jar + uses: actions/upload-artifact@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 + env: + CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }} diff --git a/.github/workflows/samples.yml b/.github/workflows/samples.yml index 5c2e8869e..aebb708a7 100644 --- a/.github/workflows/samples.yml +++ b/.github/workflows/samples.yml @@ -5,29 +5,35 @@ name: Java CI samples on: push: - branches: [ master ] + branches: [ main ] paths: - - 'samples/kinesis-firehose-event-handler/**' + - 'samples/**' pull_request: branches: [ '*' ] paths: - - 'samples/kinesis-firehose-event-handler/**' + - 'samples/**' + - '.github/workflows/samples.yml' + +permissions: + contents: read jobs: build: - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v2 + - uses: actions/checkout@v5 - name: Set up JDK 1.8 - uses: actions/setup-java@v1 + uses: actions/setup-java@v4 with: - java-version: 1.8 + java-version: 8 + distribution: corretto # Install events module - 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 @@ -35,3 +41,39 @@ jobs: # Install samples - name: Install Kinesis Firehose Sample with Maven run: mvn -B install --file samples/kinesis-firehose-event-handler/pom.xml + + custom-serialization: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v5 + # Set up both Java 8 and 21 + - name: Set up Java 8 and 21 + uses: actions/setup-java@v4 + with: + java-version: | + 8 + 21 + distribution: corretto + + # Install events module using Java 8 + - name: Install events with Maven + run: | + export JAVA_HOME=$JAVA_HOME_8_X64 + mvn -B clean install \ + -Dmaven.compiler.source=1.8 \ + -Dmaven.compiler.target=1.8 \ + --file aws-lambda-java-events/pom.xml + + # Build custom-serialization samples + - name: install sam + uses: aws-actions/setup-sam@v2 + - name: test fastJson + run: cd samples/custom-serialization/fastJson && sam build && sam local invoke -e events/event.json | grep 200 + - name: test gson + run: cd samples/custom-serialization/gson && sam build && sam local invoke -e events/event.json | grep 200 + - name: test jackson-jr + run: cd samples/custom-serialization/jackson-jr && sam build && sam local invoke -e events/event.json | grep 200 + - name: test moshi + run: cd samples/custom-serialization/moshi && sam build && sam local invoke -e events/event.json | grep 200 + - name: test request-stream-handler + run: cd samples/custom-serialization/request-stream-handler && sam build && sam local invoke -e events/event.json | grep 200 diff --git a/.gitignore b/.gitignore index fbdc4bbe2..1adf36493 100644 --- a/.gitignore +++ b/.gitignore @@ -21,4 +21,18 @@ dependency-reduced-pom.xml .gradle .settings .classpath -.project \ No newline at end of file +.project + +# OSX +.DS_Store + +# snapshot process +aws-lambda-java-runtime-interface-client/pom.xml.versionsBackup + +# profiler +experimental/aws-lambda-java-profiler/integration_tests/helloworld/build +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 diff --git a/README.md b/README.md index 0a5de6cf6..b6c67b9e8 100644 --- a/README.md +++ b/README.md @@ -1,59 +1,101 @@ # AWS Lambda Java Support Libraries -Interface definitions for Java code running on the AWS Lambda platform. +Key libraries for running Java on the AWS Lambda platform. For issues and questions, you can start with our [FAQ](https://aws.amazon.com/lambda/faqs/) - and the [AWS forums](https://forums.aws.amazon.com/forum.jspa?forumID=186) +and the AWS questions and answer site [re:Post](https://repost.aws/tags/TA5uNafDy2TpGNjidWLMSxDw/aws-lambda) -To get started writing AWS Lambda functions in Java, check out the [official documentation](http://docs.aws.amazon.com/lambda/latest/dg/java-gs.html). +To get started writing Lambda functions in Java, check out the official [developer guide](https://docs.aws.amazon.com/lambda/latest/dg/lambda-java.html). -# Disclaimer of use +For information on how to optimize your functions watch the re:Invent talk [Optimize your Java application on AWS Lambda](https://www.youtube.com/watch?v=sVJOJUD0fhQ). -Each of the supplied packages should be used without modification. Removing -dependencies, adding conflicting dependencies, or selectively including classes -from the packages can result in unexpected behavior. +## Core Java Lambda interfaces - aws-lambda-java-core + +[![Maven](https://img.shields.io/maven-central/v/com.amazonaws/aws-lambda-java-core.svg?label=Maven)](https://central.sonatype.com/artifact/com.amazonaws/aws-lambda-java-core) + +This package defines the Lambda [Context](http://docs.aws.amazon.com/lambda/latest/dg/java-context-object.html) object +as well as [interfaces](http://docs.aws.amazon.com/lambda/latest/dg/java-handler-using-predefined-interfaces.html) that Lambda accepts. + +- [Release Notes](aws-lambda-java-core/RELEASE.CHANGELOG.md) + +Example request handler -# Release Notes +```java +public class Handler implements RequestHandler, String>{ + @Override + public String handleRequest(Map event, Context context) { + + } +} +``` -Check out the per-module release notes: -- [aws-lambda-java-core](aws-lambda-java-core/RELEASE.CHANGELOG.md) -- [aws-lambda-java-events](aws-lambda-java-events/RELEASE.CHANGELOG.md) -- [aws-lambda-java-events-sdk-transformer](aws-lambda-java-events-sdk-transformer/RELEASE.CHANGELOG.md) -- [aws-lambda-java-log4j2](aws-lambda-java-log4j2/RELEASE.CHANGELOG.md) -- [aws-lambda-java-runtime-interface-client](aws-lambda-java-runtime-interface-client/RELEASE.CHANGELOG.md) -- [aws-lambda-java-serialization](aws-lambda-java-serialization/RELEASE.CHANGELOG.md) -- [aws-lambda-java-test](aws-lambda-java-tests/RELEASE.CHANGELOG.md) +Example request stream handler -# Where to get packages -___ +```java +public class HandlerStream implements RequestStreamHandler { + @Override + public void handleRequest(InputStream inputStream, OutputStream outputStream, Context context) throws IOException { -[Maven](https://maven.apache.org) + } +} +``` ```xml - com.amazonaws - aws-lambda-java-core - 1.2.1 - - - com.amazonaws - aws-lambda-java-events - 3.11.0 - - - com.amazonaws - aws-lambda-java-events-sdk-transformer - 3.0.7 - - - com.amazonaws - aws-lambda-java-log4j2 - 1.5.0 + com.amazonaws + aws-lambda-java-core + 1.3.0 +``` + +## Java objects of Lambda event sources - aws-lambda-java-events + +[![Maven](https://img.shields.io/maven-central/v/com.amazonaws/aws-lambda-java-events.svg?label=Maven)](https://central.sonatype.com/artifact/com.amazonaws/aws-lambda-java-events) + +This package defines [event sources](http://docs.aws.amazon.com/lambda/latest/dg/intro-invocation-modes.html) that Lambda natively accepts. +See the [documentation](aws-lambda-java-events/README.md) for a list of currently supported event sources. +Using this library you can have Java objects which represent event sources. + +For example an SQS event: + +```java +import com.amazonaws.services.lambda.runtime.events.SQSEvent; + +public class SqsHandler implements RequestHandler { + + @Override + public String handleRequest(SQSEvent event, Context context) { + + } +} +``` + +- [Release Notes](aws-lambda-java-events/RELEASE.CHANGELOG.md) + +```xml - com.amazonaws - aws-lambda-java-runtime-interface-client - 2.0.0 + com.amazonaws + aws-lambda-java-events + 3.16.0 +``` + +## Java Lambda JUnit Support - aws-lambda-java-tests + +[![Maven](https://img.shields.io/maven-central/v/com.amazonaws/aws-lambda-java-tests.svg?label=Maven)](https://central.sonatype.com/artifact/com.amazonaws/aws-lambda-java-tests) + +This package provides utils to ease Lambda Java testing. It uses the same Lambda serialisation logic and `aws-lambda-java-events` to inject events in your JUnit tests. + +- [Release Notes](aws-lambda-java-tests/RELEASE.CHANGELOG.md) + +```java +@ParameterizedTest +@Event(value = "sqs/sqs_event.json", type = SQSEvent.class) +public void testInjectSQSEvent(SQSEvent event) { + ... +} +``` + +```xml com.amazonaws aws-lambda-java-tests @@ -62,69 +104,87 @@ ___ ``` -[Gradle](https://gradle.org) +## aws-lambda-java-events-sdk-transformer -```groovy -'com.amazonaws:aws-lambda-java-core:1.2.1' -'com.amazonaws:aws-lambda-java-events:3.11.0' -'com.amazonaws:aws-lambda-java-events-sdk-transformer:3.0.7' -'com.amazonaws:aws-lambda-java-log4j2:1.5.0' -'com.amazonaws:aws-lambda-java-runtime-interface-client:2.0.0' -'com.amazonaws:aws-lambda-java-tests:1.1.1' -``` - -[Leiningen](http://leiningen.org) and [Boot](http://boot-clj.com) +[![Maven](https://img.shields.io/maven-central/v/com.amazonaws/aws-lambda-java-events-sdk-transformer.svg?label=Maven)](https://central.sonatype.com/artifact/com.amazonaws/aws-lambda-java-events-sdk-transformer) -```clojure -[com.amazonaws/aws-lambda-java-core "1.2.1"] -[com.amazonaws/aws-lambda-java-events "3.11.0"] -[com.amazonaws/aws-lambda-java-events-sdk-transformer "3.0.7"] -[com.amazonaws/aws-lambda-java-log4j2 "1.5.0"] -[com.amazonaws/aws-lambda-java-runtime-interface-client "2.0.0"] -[com.amazonaws/aws-lambda-java-tests "1.1.1"] -``` +This package provides helper classes/methods to use alongside `aws-lambda-java-events` in order to transform +Lambda input event model objects into SDK-compatible output model objects. +See the [documentation](aws-lambda-java-events-sdk-transformer/README.md) for more information. -[sbt](http://www.scala-sbt.org) +- [Release Notes](aws-lambda-java-events-sdk-transformer/RELEASE.CHANGELOG.md) -```scala -"com.amazonaws" % "aws-lambda-java-core" % "1.2.1" -"com.amazonaws" % "aws-lambda-java-events" % "3.11.0" -"com.amazonaws" % "aws-lambda-java-events-sdk-transformer" % "3.0.7" -"com.amazonaws" % "aws-lambda-java-log4j2" % "1.5.0" -"com.amazonaws" % "aws-lambda-java-runtime-interface-client" % "2.0.0" -"com.amazonaws" % "aws-lambda-java-tests" % "1.1.1" +```xml + + com.amazonaws + aws-lambda-java-events-sdk-transformer + 3.1.0 + ``` -# Using aws-lambda-java-core +## Java Lambda Log4J2 support - aws-lambda-java-log4j2 -This package defines the Lambda [Context](http://docs.aws.amazon.com/lambda/latest/dg/java-context-object.html) object - as well as [interfaces](http://docs.aws.amazon.com/lambda/latest/dg/java-handler-using-predefined-interfaces.html) that Lambda accepts. +[![Maven](https://img.shields.io/maven-central/v/com.amazonaws/aws-lambda-java-log4j2.svg?label=Maven)](https://central.sonatype.com/artifact/com.amazonaws/aws-lambda-java-log4j2) -# Using aws-lambda-java-events +This package defines the Lambda adapter to use with Log4J version 2. +See the [README](aws-lambda-java-log4j2/README.md) or the [official documentation](http://docs.aws.amazon.com/lambda/latest/dg/java-logging.html#java-wt-logging-using-log4j) for information on how to use the adapter. -This package defines [event sources](http://docs.aws.amazon.com/lambda/latest/dg/intro-invocation-modes.html) that AWS Lambda natively accepts. -See the [documentation](aws-lambda-java-events/README.md) for more information. +- [Release Notes](aws-lambda-java-log4j2/RELEASE.CHANGELOG.md) -# Using aws-lambda-java-events-sdk-transformer +```xml + + com.amazonaws + aws-lambda-java-log4j2 + 1.6.0 + +``` -This package provides helper classes/methods to use alongside `aws-lambda-java-events` in order to transform - Lambda input event model objects into SDK-compatible output model objects. -See the [documentation](aws-lambda-java-events-sdk-transformer/README.md) for more information. +## Lambda Profiler Extension for Java - aws-lambda-java-profiler -# Using aws-lambda-java-log4j2 +

+ A flame graph of a Java Lambda function +

-This package defines the Lambda adapter to use with log4j version 2. -See the [README](aws-lambda-java-log4j2/README.md) or the [official documentation](http://docs.aws.amazon.com/lambda/latest/dg/java-logging.html#java-wt-logging-using-log4j) for information on how to use the adapter. +This project allows you to profile your Java functions invoke by invoke, with high fidelity, and no code changes. It +uses the [async-profiler](https://github.com/async-profiler/async-profiler) project to produce profiling data and +automatically uploads the data as flame graphs to S3. + +Follow our [Quick Start](experimental/aws-lambda-java-profiler#quick-start) to profile your functions. -# Using aws-lambda-java-runtime-interface-client +## Java implementation of the Runtime Interface Client API - aws-lambda-java-runtime-interface-client +[![Maven](https://img.shields.io/maven-central/v/com.amazonaws/aws-lambda-java-runtime-interface-client.svg?label=Maven)](https://central.sonatype.com/artifact/com.amazonaws/aws-lambda-java-runtime-interface-client) This package defines the Lambda Java Runtime Interface Client package, a Lambda Runtime component that starts the runtime and interacts with the Runtime API - i.e., it calls the API for invocation events, starts the function code, calls the API to return the response. The purpose of this package is to allow developers to deploy their applications in Lambda under the form of Container Images. See the [README](aws-lambda-java-runtime-interface-client/README.md) for information on how to use the library. -# Using aws-lambda-java-serialization +- [Release Notes](aws-lambda-java-runtime-interface-client/RELEASE.CHANGELOG.md) + +```xml + + com.amazonaws + aws-lambda-java-runtime-interface-client + 2.8.6 + +``` + +## Java Lambda provided serialization support - aws-lambda-java-serialization + +[![Maven](https://img.shields.io/maven-central/v/com.amazonaws/aws-lambda-java-serialization.svg?label=Maven)](https://central.sonatype.com/artifact/com.amazonaws/aws-lambda-java-serialization) -This package defines the Lambda serialization logic using in the aws-lambda-java-runtime-client library. It has no current standalone usage. +This package defines the Lambda serialization logic using in the `aws-lambda-java-runtime-client` library. It has no current standalone usage. -# Using aws-lambda-java-tests +- [Release Notes](aws-lambda-java-serialization/RELEASE.CHANGELOG.md) -This package provides utils to ease Lambda Java testing. Used with `aws-lambda-java-serialization` and `aws-lambda-java-events` to inject events in your JUnit tests. +```xml + + com.amazonaws + aws-lambda-java-serialization + 1.1.5 + +``` + +## Disclaimer of use + +Each of the supplied packages should be used without modification. Removing +dependencies, adding conflicting dependencies, or selectively including classes +from the packages can result in unexpected behavior. diff --git a/aws-lambda-java-core/RELEASE.CHANGELOG.md b/aws-lambda-java-core/RELEASE.CHANGELOG.md index 406390d7d..aebc8ecd9 100644 --- a/aws-lambda-java-core/RELEASE.CHANGELOG.md +++ b/aws-lambda-java-core/RELEASE.CHANGELOG.md @@ -1,3 +1,20 @@ +### 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)) + +### August 17, 2023 +`1.2.3`: +- Extended logger interface with level-aware logging backend functions + +### November 09, 2022 +`1.2.2`: +- Added new `CustomPojoSerializer` interface +- Removed unnecessary usage of public on interface methods (aws#172) + ### April 28, 2020 `1.2.1`: - Added missing XML namespace declarations to `pom.xml` file ([#97](https://github.com/aws/aws-lambda-java-libs/issues/97)) diff --git a/aws-lambda-java-core/pom.xml b/aws-lambda-java-core/pom.xml index 52d2976f7..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.2.1 + 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/Client.java b/aws-lambda-java-core/src/main/java/com/amazonaws/services/lambda/runtime/Client.java index 6acdd38cc..be8856871 100644 --- a/aws-lambda-java-core/src/main/java/com/amazonaws/services/lambda/runtime/Client.java +++ b/aws-lambda-java-core/src/main/java/com/amazonaws/services/lambda/runtime/Client.java @@ -11,28 +11,28 @@ public interface Client { /** * Gets the application's installation id */ - public String getInstallationId(); + String getInstallationId(); /** * Gets the application's title * */ - public String getAppTitle(); + String getAppTitle(); /** * Gets the application's version * */ - public String getAppVersionName(); + String getAppVersionName(); /** * Gets the application's version code * */ - public String getAppVersionCode(); + String getAppVersionCode(); /** * Gets the application's package name */ - public String getAppPackageName(); + String getAppPackageName(); } diff --git a/aws-lambda-java-core/src/main/java/com/amazonaws/services/lambda/runtime/ClientContext.java b/aws-lambda-java-core/src/main/java/com/amazonaws/services/lambda/runtime/ClientContext.java index 2815396d0..71f442e49 100644 --- a/aws-lambda-java-core/src/main/java/com/amazonaws/services/lambda/runtime/ClientContext.java +++ b/aws-lambda-java-core/src/main/java/com/amazonaws/services/lambda/runtime/ClientContext.java @@ -14,7 +14,7 @@ public interface ClientContext { * Gets the client information provided by the AWS Mobile SDK * */ - public Client getClient(); + Client getClient(); /** * Gets custom values set by the client application @@ -22,12 +22,12 @@ public interface ClientContext { * This map is mutable (and not thread-safe if mutated) *

*/ - public Map getCustom(); + Map getCustom(); /** * Gets environment information provided by mobile SDK, immutable. * */ - public Map getEnvironment(); + Map getEnvironment(); } diff --git a/aws-lambda-java-core/src/main/java/com/amazonaws/services/lambda/runtime/CognitoIdentity.java b/aws-lambda-java-core/src/main/java/com/amazonaws/services/lambda/runtime/CognitoIdentity.java index 25f3b3dd3..a65887632 100644 --- a/aws-lambda-java-core/src/main/java/com/amazonaws/services/lambda/runtime/CognitoIdentity.java +++ b/aws-lambda-java-core/src/main/java/com/amazonaws/services/lambda/runtime/CognitoIdentity.java @@ -11,11 +11,11 @@ public interface CognitoIdentity { * Gets the Amazon Cognito identity ID * */ - public String getIdentityId(); + String getIdentityId(); /** * Gets the Amazon Cognito identity pool ID * */ - public String getIdentityPoolId(); + String getIdentityPoolId(); } 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 a0850e78c..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 @@ -100,4 +100,23 @@ public interface Context { */ LambdaLogger getLogger(); + /** + * + * Returns the tenant ID associated with the request. + * + * @return null by default + */ + 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-core/src/main/java/com/amazonaws/services/lambda/runtime/CustomPojoSerializer.java b/aws-lambda-java-core/src/main/java/com/amazonaws/services/lambda/runtime/CustomPojoSerializer.java new file mode 100644 index 000000000..0d7cc27d4 --- /dev/null +++ b/aws-lambda-java-core/src/main/java/com/amazonaws/services/lambda/runtime/CustomPojoSerializer.java @@ -0,0 +1,38 @@ +/* Copyright 2022 Amazon.com, Inc. or its affiliates. All Rights Reserved. */ + +package com.amazonaws.services.lambda.runtime; + +import java.io.InputStream; +import java.io.OutputStream; + +import java.lang.reflect.Type; + +/** + * Interface required to implement a custom plain old java objects serializer + */ +public interface CustomPojoSerializer { + + /** + * Deserializes from input stream to plain old java object + * @param input input stream + * @param type plain old java object type + * @return deserialized plain old java object of type T + */ + T fromJson(InputStream input, Type type); + + /** + * Deserializes from String to plain old java object + * @param input input string + * @param type plain old java object type + * @return deserialized plain old java object of type T + */ + T fromJson(String input, Type type); + + /** + * Serializes plain old java object to output stream + * @param value instance of type T to be serialized + * @param output OutputStream to serialize plain old java object to + * @param type plain old java object type + */ + void toJson(T value, OutputStream output, Type type); +} diff --git a/aws-lambda-java-core/src/main/java/com/amazonaws/services/lambda/runtime/LambdaLogger.java b/aws-lambda-java-core/src/main/java/com/amazonaws/services/lambda/runtime/LambdaLogger.java index 8ff064589..e068abe8a 100644 --- a/aws-lambda-java-core/src/main/java/com/amazonaws/services/lambda/runtime/LambdaLogger.java +++ b/aws-lambda-java-core/src/main/java/com/amazonaws/services/lambda/runtime/LambdaLogger.java @@ -2,6 +2,8 @@ package com.amazonaws.services.lambda.runtime; +import com.amazonaws.services.lambda.runtime.logging.LogLevel; + /** * A low level Lambda runtime logger * @@ -10,7 +12,7 @@ public interface LambdaLogger { /** * Logs a string to AWS CloudWatch Logs - * + * *

* Logging will not be done: *

    @@ -22,15 +24,37 @@ public interface LambdaLogger { * *
*

- * + * * @param message A string containing the event to log. */ - public void log(String message); + void log(String message); /** * Logs a byte array to AWS CloudWatch Logs * @param message byte array containing logs */ - public void log(byte[] message); + void log(byte[] message); + + /** + * LogLevel aware logging backend function. + * + * @param message in String format + * @param logLevel + */ + default void log(String message, LogLevel logLevel) { + log(message); + } + + /** + * LogLevel aware logging backend function. + * + * @param message in byte[] format + * @param logLevel + */ + default void log(byte[] message, LogLevel logLevel) { + log(message); + } + + } diff --git a/aws-lambda-java-core/src/main/java/com/amazonaws/services/lambda/runtime/RequestHandler.java b/aws-lambda-java-core/src/main/java/com/amazonaws/services/lambda/runtime/RequestHandler.java index 406f2be3b..834683f26 100644 --- a/aws-lambda-java-core/src/main/java/com/amazonaws/services/lambda/runtime/RequestHandler.java +++ b/aws-lambda-java-core/src/main/java/com/amazonaws/services/lambda/runtime/RequestHandler.java @@ -17,5 +17,5 @@ public interface RequestHandler { * @param context The Lambda execution environment context object. * @return The Lambda Function output */ - public O handleRequest(I input, Context context); + O handleRequest(I input, Context context); } diff --git a/aws-lambda-java-core/src/main/java/com/amazonaws/services/lambda/runtime/RequestStreamHandler.java b/aws-lambda-java-core/src/main/java/com/amazonaws/services/lambda/runtime/RequestStreamHandler.java index d8ccf5a6b..3a34adc9b 100644 --- a/aws-lambda-java-core/src/main/java/com/amazonaws/services/lambda/runtime/RequestStreamHandler.java +++ b/aws-lambda-java-core/src/main/java/com/amazonaws/services/lambda/runtime/RequestStreamHandler.java @@ -18,5 +18,5 @@ public interface RequestStreamHandler { * @param context The Lambda execution environment context object. * @throws IOException */ - public void handleRequest(InputStream input, OutputStream output, Context context) throws IOException; + void handleRequest(InputStream input, OutputStream output, Context context) throws IOException; } diff --git a/aws-lambda-java-core/src/main/java/com/amazonaws/services/lambda/runtime/logging/LogFormat.java b/aws-lambda-java-core/src/main/java/com/amazonaws/services/lambda/runtime/logging/LogFormat.java new file mode 100644 index 000000000..0d65860d7 --- /dev/null +++ b/aws-lambda-java-core/src/main/java/com/amazonaws/services/lambda/runtime/logging/LogFormat.java @@ -0,0 +1,17 @@ +/* Copyright 2023 Amazon.com, Inc. or its affiliates. All Rights Reserved. */ + +package com.amazonaws.services.lambda.runtime.logging; + + +public enum LogFormat { + JSON, + TEXT; + + public static LogFormat fromString(String logFormat) { + try { + return LogFormat.valueOf(logFormat.toUpperCase()); + } catch (Exception e) { + throw new IllegalArgumentException("Invalid log format: '" + logFormat + "' expected one of [JSON, TEXT]"); + } + } +} diff --git a/aws-lambda-java-core/src/main/java/com/amazonaws/services/lambda/runtime/logging/LogLevel.java b/aws-lambda-java-core/src/main/java/com/amazonaws/services/lambda/runtime/logging/LogLevel.java new file mode 100644 index 000000000..4f48a3e41 --- /dev/null +++ b/aws-lambda-java-core/src/main/java/com/amazonaws/services/lambda/runtime/logging/LogLevel.java @@ -0,0 +1,25 @@ +/* Copyright 2023 Amazon.com, Inc. or its affiliates. All Rights Reserved. */ + +package com.amazonaws.services.lambda.runtime.logging; + + +public enum LogLevel { + // UNDEFINED log level is used when the legacy LambdaLogger::log(String) function is called + // where the loglevel is not defined. In this case we're not filtering the message in the runtime + UNDEFINED, + TRACE, + DEBUG, + INFO, + WARN, + ERROR, + FATAL; + + public static LogLevel fromString(String logLevel) { + try { + return LogLevel.valueOf(logLevel.toUpperCase()); + } catch (Exception e) { + throw new IllegalArgumentException( + "Invalid log level: '" + logLevel + "' expected one of [TRACE, DEBUG, INFO, WARN, ERROR, FATAL]"); + } + } +} diff --git a/aws-lambda-java-events-sdk-transformer/README.md b/aws-lambda-java-events-sdk-transformer/README.md index cfc951fcd..02f2dc11f 100644 --- a/aws-lambda-java-events-sdk-transformer/README.md +++ b/aws-lambda-java-events-sdk-transformer/README.md @@ -16,12 +16,12 @@ Add the following Apache Maven dependencies to your `pom.xml` file: com.amazonaws aws-lambda-java-events-sdk-transformer - 3.0.7 + 3.1.0 com.amazonaws aws-lambda-java-events - 3.11.0 + 3.11.2 ``` diff --git a/aws-lambda-java-events-sdk-transformer/RELEASE.CHANGELOG.md b/aws-lambda-java-events-sdk-transformer/RELEASE.CHANGELOG.md index 5f86ebd77..791348208 100644 --- a/aws-lambda-java-events-sdk-transformer/RELEASE.CHANGELOG.md +++ b/aws-lambda-java-events-sdk-transformer/RELEASE.CHANGELOG.md @@ -1,3 +1,7 @@ +### February 03, 2022 +`3.1.0`: +- Make DynamodbAttributeValueTransformer v1 and v2 return empty list instead of null for empty list attribute ([#309](https://github.com/aws/aws-lambda-java-libs/pull/309)) + ### November 24, 2021 `3.0.7`: - Bumped `aws-lambda-java-events` to version `3.11.0` diff --git a/aws-lambda-java-events-sdk-transformer/pom.xml b/aws-lambda-java-events-sdk-transformer/pom.xml index 5575bab12..d719ec8ac 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.0.7 + 3.1.1 jar AWS Lambda Java Events SDK Transformer Library @@ -63,7 +63,7 @@ com.amazonaws aws-lambda-java-events - 3.11.0 + 3.16.1 provided @@ -160,18 +160,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-sdk-transformer/src/main/java/com/amazonaws/services/lambda/runtime/events/transformers/v1/dynamodb/DynamodbAttributeValueTransformer.java b/aws-lambda-java-events-sdk-transformer/src/main/java/com/amazonaws/services/lambda/runtime/events/transformers/v1/dynamodb/DynamodbAttributeValueTransformer.java index 9c2969d19..61f311f5b 100644 --- a/aws-lambda-java-events-sdk-transformer/src/main/java/com/amazonaws/services/lambda/runtime/events/transformers/v1/dynamodb/DynamodbAttributeValueTransformer.java +++ b/aws-lambda-java-events-sdk-transformer/src/main/java/com/amazonaws/services/lambda/runtime/events/transformers/v1/dynamodb/DynamodbAttributeValueTransformer.java @@ -2,6 +2,7 @@ import com.amazonaws.services.dynamodbv2.model.AttributeValue; +import java.util.Collections; import java.util.Map; import java.util.Objects; import java.util.stream.Collectors; @@ -40,7 +41,7 @@ public static AttributeValue toAttributeValueV1(final com.amazonaws.services.lam } else if (Objects.nonNull(value.getL())) { return new AttributeValue() .withL(value.getL().isEmpty() - ? null + ? Collections.emptyList() : value.getL().stream() .map(DynamodbAttributeValueTransformer::toAttributeValueV1) .collect(Collectors.toList())); diff --git a/aws-lambda-java-events-sdk-transformer/src/main/java/com/amazonaws/services/lambda/runtime/events/transformers/v2/dynamodb/DynamodbAttributeValueTransformer.java b/aws-lambda-java-events-sdk-transformer/src/main/java/com/amazonaws/services/lambda/runtime/events/transformers/v2/dynamodb/DynamodbAttributeValueTransformer.java index 25fc55886..ee810c501 100644 --- a/aws-lambda-java-events-sdk-transformer/src/main/java/com/amazonaws/services/lambda/runtime/events/transformers/v2/dynamodb/DynamodbAttributeValueTransformer.java +++ b/aws-lambda-java-events-sdk-transformer/src/main/java/com/amazonaws/services/lambda/runtime/events/transformers/v2/dynamodb/DynamodbAttributeValueTransformer.java @@ -3,6 +3,7 @@ import software.amazon.awssdk.core.SdkBytes; import software.amazon.awssdk.services.dynamodb.model.AttributeValue; +import java.util.Collections; import java.util.Map; import java.util.Objects; import java.util.stream.Collectors; @@ -52,7 +53,7 @@ public static AttributeValue toAttributeValueV2(final com.amazonaws.services.lam } else if (Objects.nonNull(value.getL())) { return AttributeValue.builder() .l(value.getL().isEmpty() - ? null + ? Collections.emptyList() : value.getL().stream() .map(DynamodbAttributeValueTransformer::toAttributeValueV2) .collect(Collectors.toList())) diff --git a/aws-lambda-java-events-sdk-transformer/src/test/java/com/amazonaws/services/lambda/runtime/events/transformers/v1/dynamodb/DynamodbAttributeValueTransformerTest.java b/aws-lambda-java-events-sdk-transformer/src/test/java/com/amazonaws/services/lambda/runtime/events/transformers/v1/dynamodb/DynamodbAttributeValueTransformerTest.java index de45474b2..14534fae5 100644 --- a/aws-lambda-java-events-sdk-transformer/src/test/java/com/amazonaws/services/lambda/runtime/events/transformers/v1/dynamodb/DynamodbAttributeValueTransformerTest.java +++ b/aws-lambda-java-events-sdk-transformer/src/test/java/com/amazonaws/services/lambda/runtime/events/transformers/v1/dynamodb/DynamodbAttributeValueTransformerTest.java @@ -179,6 +179,7 @@ public void testToAttributeValueV1_L() { com.amazonaws.services.dynamodbv2.model.AttributeValue convertedAttributeValueL = DynamodbAttributeValueTransformer.toAttributeValueV1(attributeValueL_event); Assertions.assertEquals(attributeValueL_v1, convertedAttributeValueL); + Assertions.assertEquals("ArrayList", convertedAttributeValueL.getL().getClass().getSimpleName(), "List is mutable"); } @Test @@ -262,12 +263,14 @@ public void testToAttributeValueV1_DoesNotThrowWhenEmpty_BS() { @Test public void testToAttributeValueV1_DoesNotThrowWhenEmpty_L() { - Assertions.assertDoesNotThrow(() -> - DynamodbAttributeValueTransformer.toAttributeValueV1(new AttributeValue().withL()) - ); - Assertions.assertDoesNotThrow(() -> - DynamodbAttributeValueTransformer.toAttributeValueV1(new AttributeValue().withL(Collections.emptyList())) - ); + Assertions.assertDoesNotThrow(() -> { + com.amazonaws.services.dynamodbv2.model.AttributeValue attributeValue = DynamodbAttributeValueTransformer.toAttributeValueV1(new AttributeValue().withL()); + Assertions.assertEquals("ArrayList", attributeValue.getL().getClass().getSimpleName(), "List is mutable"); + }); + Assertions.assertDoesNotThrow(() -> { + com.amazonaws.services.dynamodbv2.model.AttributeValue attributeValue = DynamodbAttributeValueTransformer.toAttributeValueV1(new AttributeValue().withL(Collections.emptyList())); + Assertions.assertEquals("ArrayList", attributeValue.getL().getClass().getSimpleName(), "List is mutable"); + }); } @Test @@ -303,7 +306,7 @@ public void testToAttributeValueV1_EmptyV1ObjectWhenEmpty_BS() { @Test public void testToAttributeValueV1_EmptyV1ObjectWhenEmpty_L() { com.amazonaws.services.dynamodbv2.model.AttributeValue expectedAttributeValue_v1 = - new com.amazonaws.services.dynamodbv2.model.AttributeValue(); + new com.amazonaws.services.dynamodbv2.model.AttributeValue().withL(Collections.emptyList()); Assertions.assertEquals(expectedAttributeValue_v1, DynamodbAttributeValueTransformer.toAttributeValueV1(new AttributeValue().withL())); Assertions.assertEquals(expectedAttributeValue_v1, diff --git a/aws-lambda-java-events-sdk-transformer/src/test/java/com/amazonaws/services/lambda/runtime/events/transformers/v2/dynamodb/DynamodbAttributeValueTransformerTest.java b/aws-lambda-java-events-sdk-transformer/src/test/java/com/amazonaws/services/lambda/runtime/events/transformers/v2/dynamodb/DynamodbAttributeValueTransformerTest.java index 8ae521f3d..1c7f05f7d 100644 --- a/aws-lambda-java-events-sdk-transformer/src/test/java/com/amazonaws/services/lambda/runtime/events/transformers/v2/dynamodb/DynamodbAttributeValueTransformerTest.java +++ b/aws-lambda-java-events-sdk-transformer/src/test/java/com/amazonaws/services/lambda/runtime/events/transformers/v2/dynamodb/DynamodbAttributeValueTransformerTest.java @@ -183,6 +183,7 @@ public void testToAttributeValueV2_L() { software.amazon.awssdk.services.dynamodb.model.AttributeValue convertedAttributeValueL = DynamodbAttributeValueTransformer.toAttributeValueV2(attributeValueL_event); Assertions.assertEquals(attributeValueL_v2, convertedAttributeValueL); + Assertions.assertEquals("UnmodifiableRandomAccessList", convertedAttributeValueL.l().getClass().getSimpleName(), "List is immutable"); } @Test @@ -266,12 +267,14 @@ public void testToAttributeValueV2_DoesNotThrowWhenEmpty_BS() { @Test public void testToAttributeValueV2_DoesNotThrowWhenEmpty_L() { - Assertions.assertDoesNotThrow(() -> - DynamodbAttributeValueTransformer.toAttributeValueV2(new AttributeValue().withL()) - ); - Assertions.assertDoesNotThrow(() -> - DynamodbAttributeValueTransformer.toAttributeValueV2(new AttributeValue().withL(Collections.emptyList())) - ); + Assertions.assertDoesNotThrow(() -> { + software.amazon.awssdk.services.dynamodb.model.AttributeValue attributeValue = DynamodbAttributeValueTransformer.toAttributeValueV2(new AttributeValue().withL()); + Assertions.assertEquals("UnmodifiableRandomAccessList", attributeValue.l().getClass().getSimpleName(), "List is immutable"); + }); + Assertions.assertDoesNotThrow(() -> { + software.amazon.awssdk.services.dynamodb.model.AttributeValue attributeValue = DynamodbAttributeValueTransformer.toAttributeValueV2(new AttributeValue().withL(Collections.emptyList())); + Assertions.assertEquals("UnmodifiableRandomAccessList", attributeValue.l().getClass().getSimpleName(), "List is immutable"); + }); } @Test @@ -307,7 +310,7 @@ public void testToAttributeValueV2_EmptyV2ObjectWhenEmpty_BS() { @Test public void testToAttributeValueV2_EmptyV2ObjectWhenEmpty_L() { software.amazon.awssdk.services.dynamodb.model.AttributeValue expectedAttributeValue_v2 = - software.amazon.awssdk.services.dynamodb.model.AttributeValue.builder().build(); + software.amazon.awssdk.services.dynamodb.model.AttributeValue.builder().l(Collections.emptyList()).build(); Assertions.assertEquals(expectedAttributeValue_v2, DynamodbAttributeValueTransformer.toAttributeValueV2(new AttributeValue().withL())); Assertions.assertEquals(expectedAttributeValue_v2, diff --git a/aws-lambda-java-events/README.md b/aws-lambda-java-events/README.md index 1d741469a..43c25d76a 100644 --- a/aws-lambda-java-events/README.md +++ b/aws-lambda-java-events/README.md @@ -16,7 +16,9 @@ * `AppSyncLambdaAuthorizerResponse` * `CloudFormationCustomResourceEvent` * `CloudFrontEvent` +* `CloudWatchCompositeAlarmEvent` * `CloudWatchLogsEvent` +* `CloudWatchMetricAlarmEvent` * `CodeCommitEvent` * `CognitoEvent` * `CognitoUserPoolCreateAuthChallengeEvent` @@ -29,6 +31,7 @@ * `CognitoUserPoolPreAuthenticationEvent` * `CognitoUserPoolPreSignUpEvent` * `CognitoUserPoolPreTokenGenerationEvent` +* `CognitoUserPoolPreTokenGenerationEventV2` * `CognitoUserPoolVerifyAuthChallengeResponseEvent` * `ConfigEvent` * `ConnectEvent` @@ -44,6 +47,8 @@ * `KinesisFirehoseEvent` * `LambdaDestinationEvent` * `LexEvent` +* `MSKFirehoseEvent` +* `MSKFirehoseResponse` * `RabbitMQEvent` * `S3BatchEvent` * `S3BatchResponse` @@ -55,12 +60,8 @@ * `SQSBatchResponse` * `SQSEvent` -*As of version `3.0.0`, users are no longer required to pull in SDK dependencies in order to use this library.* - -### Getting Started - -[Maven](https://maven.apache.org) +### Usage ```xml @@ -68,34 +69,13 @@ com.amazonaws aws-lambda-java-core - 1.2.1 + 1.2.3 com.amazonaws aws-lambda-java-events - 3.11.0 + 3.16.0 ... ``` - -[Gradle](https://gradle.org) - -```groovy -'com.amazonaws:aws-lambda-java-core:1.2.1' -'com.amazonaws:aws-lambda-java-events:3.11.0' -``` - -[Leiningen](http://leiningen.org) and [Boot](http://boot-clj.com) - -```clojure -[com.amazonaws/aws-lambda-java-core "1.2.1"] -[com.amazonaws/aws-lambda-java-events "3.11.0"] -``` - -[sbt](http://www.scala-sbt.org) - -```scala -"com.amazonaws" % "aws-lambda-java-core" % "1.2.1" -"com.amazonaws" % "aws-lambda-java-events" % "3.11.0" -``` diff --git a/aws-lambda-java-events/RELEASE.CHANGELOG.md b/aws-lambda-java-events/RELEASE.CHANGELOG.md index 6451e5b96..a4bcd10a0 100644 --- a/aws-lambda-java-events/RELEASE.CHANGELOG.md +++ b/aws-lambda-java-events/RELEASE.CHANGELOG.md @@ -1,3 +1,56 @@ +### June 17, 2025 +`3.16.0`: +- Add Schema metadata related attributes in KafkaEvent ([#548](https://github.com/aws/aws-lambda-java-libs/pull/548)) + +### January 31, 2025 +`3.15.0`: +- Fix `CognitoUserPoolPreTokenGenerationEventV2` model ([#519](https://github.com/aws/aws-lambda-java-libs/pull/519)) +- Add RotationToken to SecretsManagerRotationEvent ([#520](https://github.com/aws/aws-lambda-java-libs/pull/520)) + + +### September 13, 2024 +`3.14.0`: +- Fix name of s3Bucket field of Task class in S3BatchEventV2 ([#506](https://github.com/aws/aws-lambda-java-libs/pull/506)) + +### July 29, 2024 +`3.13.0`: +- Add S3BatchEventV2 ([#496](https://github.com/aws/aws-lambda-java-libs/pull/496)) + +### July 11, 2024 +`3.12.0`: +- Added the object representations of the CloudWatch alarms([#493](https://github.com/aws/aws-lambda-java-libs/pull/493)) +- Added event class MskFirehoseEvent.java for Firehose Lambda transformation when MSK is the source([#490](https://github.com/aws/aws-lambda-java-libs/pull/490)) + +### June 11, 2024 +`3.11.6`: +- Add the V2 version of the pre token generation event([#465](https://github.com/aws/aws-lambda-java-libs/pull/465)) + +### April 12, 2024 +`3.11.5`: +- Add requestHeaders field for Appsync lambda authorizer event([#473](https://github.com/aws/aws-lambda-java-libs/pull/473)) + +### December 1, 2023 +`3.11.4`: +- Improve `toString` in Cognito events by calling `super` +- Added missing `version` field to ScheduledEvent from CloudWatch + +### September 1, 2023 +`3.11.3`: +- Update challengeAnswer field format in CognitoUserPoolEvent + +### May 18, 2023 +`3.11.2`: +- Add missing fields to API Gateway request context + +### March 10, 2023 +`3.11.1`: +- Extended ActiveMQEvent with custom properties ([#408](https://github.com/aws/aws-lambda-java-libs/pull/408)) +- Updated dependencies([#410](https://github.com/aws/aws-lambda-java-libs/pull/410)): + - `joda-time` from 2.6 to 2.10.8 + - `jackson-databind` from 2.13.4.1 to 2.14.2 + - `junit-jupiter-engine` from 5.7.0 to 5.9.2 + - `json-unit-assertj` from 2.22.0 to 2.36.1 + ### November 24, 2021 `3.11.0`: - Added support for SQSaaES Partial Batch Feature ([#279](https://github.com/aws/aws-lambda-java-libs/pull/279)) diff --git a/aws-lambda-java-events/pom.xml b/aws-lambda-java-events/pom.xml index 4ab0bc0a6..714c825d9 100644 --- a/aws-lambda-java-events/pom.xml +++ b/aws-lambda-java-events/pom.xml @@ -1,166 +1,192 @@ - 4.0.0 + 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.11.0 - jar + com.amazonaws + aws-lambda-java-events + 3.16.1 + jar - AWS Lambda Java Events Library - - Event interface definitions AWS services supported by AWS Lambda. - - https://aws.amazon.com/lambda/ - - - Apache License, Version 2.0 - https://aws.amazon.com/apache2.0 - repo - - - - https://github.com/aws/aws-lambda-java-libs.git - - - - AWS Lambda team - Amazon Web Services - https://aws.amazon.com/ - - + AWS Lambda Java Events Library + + Event interface definitions AWS services supported by AWS Lambda. + + https://aws.amazon.com/lambda/ + + + Apache License, Version 2.0 + https://aws.amazon.com/apache2.0 + repo + + + + https://github.com/aws/aws-lambda-java-libs.git + + + + AWS Lambda team + Amazon Web Services + https://aws.amazon.com/ + + - - 1.8 - 1.8 - + + 1.8 + 1.8 + 1.18.22 + UTF-8 + UTF-8 + 2.20.1 + 2.40.1 + - - - sonatype-nexus-staging - https://oss.sonatype.org/service/local/staging/deploy/maven2/ - - + + + sonatype-nexus-staging + https://oss.sonatype.org/service/local/staging/deploy/maven2/ + + - - - joda-time - joda-time - 2.6 - + + + joda-time + joda-time + 2.10.8 + - - org.junit.jupiter - junit-jupiter-engine - 5.7.0 - test - - - com.fasterxml.jackson.core - jackson-databind - 2.10.5.1 - test - - - net.javacrumbs.json-unit - json-unit-assertj - 2.22.0 - test - + + org.junit.jupiter + junit-jupiter-engine + 5.9.2 + test + + + com.fasterxml.jackson.core + jackson-databind + ${jackson.version} + test + + + net.javacrumbs.json-unit + json-unit-assertj + ${json.unit} + test + - - org.projectlombok - lombok - 1.18.16 - provided - - + + org.projectlombok + lombok + ${lombok.version} + provided + + - - - dev - - - - org.apache.maven.plugins - maven-javadoc-plugin - 2.9.1 - - -Xdoclint:none - - - - attach-javadocs - - jar - - - - - - - - - release - - - - org.apache.maven.plugins - maven-source-plugin - 2.2.1 - - - attach-sources - - jar-no-fork - - - - - - org.apache.maven.plugins - maven-javadoc-plugin - 2.9.1 - - -Xdoclint:none - - - - attach-javadocs - - jar - - - - - - org.apache.maven.plugins - maven-gpg-plugin - 1.5 - - - sign-artifacts - verify - - sign - - - - - - org.sonatype.plugins - nexus-staging-maven-plugin - 1.6.3 - true - - sonatype-nexus-staging - https://aws.oss.sonatype.org/ - false - - - - - - - + + + dev + + + + org.apache.maven.plugins + maven-javadoc-plugin + 2.9.1 + + -Xdoclint:none + + + + attach-javadocs + + jar + + + + + + + + + release + + + + org.apache.maven.plugins + maven-source-plugin + 3.2.1 + + + attach-sources + + jar-no-fork + + + + + + org.apache.maven.plugins + maven-javadoc-plugin + 2.9.1 + + -Xdoclint:none + + + + attach-javadocs + + jar + + + + + + org.apache.maven.plugins + maven-gpg-plugin + 1.5 + + + sign-artifacts + verify + + sign + + + + + + org.sonatype.central + central-publishing-maven-plugin + 0.8.0 + true + + central + + + + org.apache.maven.plugins + maven-resources-plugin + 3.3.1 + + UTF-8 + + + + org.apache.maven.plugins + maven-compiler-plugin + 3.11.0 + + + + org.projectlombok + lombok + ${lombok.version} + + + UTF-8 + + + + + + + \ No newline at end of file diff --git a/aws-lambda-java-events/src/main/java/com/amazonaws/services/lambda/runtime/events/APIGatewayProxyRequestEvent.java b/aws-lambda-java-events/src/main/java/com/amazonaws/services/lambda/runtime/events/APIGatewayProxyRequestEvent.java index 042a481a9..8ff8ccb8b 100644 --- a/aws-lambda-java-events/src/main/java/com/amazonaws/services/lambda/runtime/events/APIGatewayProxyRequestEvent.java +++ b/aws-lambda-java-events/src/main/java/com/amazonaws/services/lambda/runtime/events/APIGatewayProxyRequestEvent.java @@ -66,6 +66,18 @@ public static class ProxyRequestContext implements Serializable, Cloneable { private Map authorizer; + private String extendedRequestId; + + private String requestTime; + + private Long requestTimeEpoch; + + private String domainName; + + private String domainPrefix; + + private String protocol; + /** * default constructor */ @@ -305,6 +317,145 @@ public ProxyRequestContext withOperationName(String operationName) { return this; } + /** + * @return The API Gateway Extended Request Id + */ + public String getExtendedRequestId() { + return extendedRequestId; + } + + /** + * @param extendedRequestId The API Gateway Extended Request Id + */ + public void setExtendedRequestId(String extendedRequestId) { + this.extendedRequestId = extendedRequestId; + } + + /** + * @param extendedRequestId The API Gateway Extended Request Id + * @return ProxyRequestContext object + */ + public ProxyRequestContext withExtendedRequestId(String extendedRequestId) { + this.setExtendedRequestId(extendedRequestId); + return this; + } + + /** + * @return The CLF-formatted request time (dd/MMM/yyyy:HH:mm:ss +-hhmm). + */ + public String getRequestTime() { + return requestTime; + } + + /** + * @param requestTime The CLF-formatted request time (dd/MMM/yyyy:HH:mm:ss +-hhmm). + */ + public void setRequestTime(String requestTime) { + this.requestTime = requestTime; + } + + /** + * @param requestTime The CLF-formatted request time (dd/MMM/yyyy:HH:mm:ss +-hhmm). + * @return ProxyRequestContext object + */ + public ProxyRequestContext withRequestTime(String requestTime) { + this.setRequestTime(requestTime); + return this; + } + + /** + * @return The Epoch-formatted request time (in millis) + */ + public Long getRequestTimeEpoch() { + return requestTimeEpoch; + } + + /** + * @param requestTimeEpoch The Epoch-formatted request time (in millis) + */ + public void setRequestTimeEpoch(Long requestTimeEpoch) { + this.requestTimeEpoch = requestTimeEpoch; + } + + /** + * @param requestTimeEpoch The Epoch-formatted request time (in millis) + * @return ProxyRequestContext object + */ + public ProxyRequestContext withRequestTimeEpoch(Long requestTimeEpoch) { + this.setRequestTimeEpoch(requestTimeEpoch); + return this; + } + + /** + * @return The full domain name used to invoke the API. This should be the same as the incoming Host header. + */ + public String getDomainName() { + return domainName; + } + + /** + * @param domainName The full domain name used to invoke the API. + * This should be the same as the incoming Host header. + */ + public void setDomainName(String domainName) { + this.domainName = domainName; + } + + /** + * @param domainName The full domain name used to invoke the API. + * This should be the same as the incoming Host header. + * @return ProxyRequestContext object + */ + public ProxyRequestContext withDomainName(String domainName) { + this.setDomainName(domainName); + return this; + } + + /** + * @return The first label of the domainName. This is often used as a caller/customer identifier. + */ + public String getDomainPrefix() { + return domainPrefix; + } + + /** + * @param domainPrefix The first label of the domainName. This is often used as a caller/customer identifier. + */ + public void setDomainPrefix(String domainPrefix) { + this.domainPrefix = domainPrefix; + } + + /** + * @param domainPrefix The first label of the domainName. This is often used as a caller/customer identifier. + * @return + */ + public ProxyRequestContext withDomainPrefix(String domainPrefix) { + this.setDomainPrefix(domainPrefix); + return this; + } + /** + * @return The request protocol, for example, HTTP/1.1. + */ + public String getProtocol() { + return protocol; + } + + /** + * @param protocol The request protocol, for example, HTTP/1.1. + */ + public void setProtocol(String protocol) { + this.protocol = protocol; + } + + /** + * @param protocol The request protocol, for example, HTTP/1.1. + * @return ProxyRequestContext object + */ + public ProxyRequestContext withProtocol(String protocol) { + this.setProtocol(protocol); + return this; + } + /** * Returns a string representation of this object; useful for testing and debugging. * @@ -338,6 +489,18 @@ public String toString() { sb.append("authorizer: ").append(getAuthorizer().toString()); if (getOperationName() != null) sb.append("operationName: ").append(getOperationName().toString()); + if (getExtendedRequestId() != null) + sb.append("extendedRequestId: ").append(getExtendedRequestId()).append(","); + if (getRequestTime() != null) + sb.append("requestTime: ").append(getRequestTime()).append(","); + if (getProtocol() != null) + sb.append("protocol: ").append(getProtocol()).append(","); + if (getRequestTimeEpoch() != null) + sb.append("requestTimeEpoch: ").append(getRequestTimeEpoch()).append(","); + if (getDomainPrefix() != null) + sb.append("domainPrefix: ").append(getDomainPrefix()).append(","); + if (getDomainName() != null) + sb.append("domainName: ").append(getDomainName()); sb.append("}"); return sb.toString(); } @@ -396,6 +559,30 @@ public boolean equals(Object obj) { return false; if (other.getOperationName() != null && !other.getOperationName().equals(this.getOperationName())) return false; + if (other.getExtendedRequestId() == null ^ this.getExtendedRequestId() == null) + return false; + if (other.getExtendedRequestId() != null && other.getExtendedRequestId().equals(this.getExtendedRequestId()) == false) + return false; + if (other.getRequestTime() == null ^ this.getRequestTime() == null) + return false; + if (other.getRequestTime() != null && other.getRequestTime().equals(this.getRequestTime()) == false) + return false; + if (other.getRequestTimeEpoch() == null ^ this.getRequestTimeEpoch() == null) + return false; + if (other.getRequestTimeEpoch() != null && other.getRequestTimeEpoch().equals(this.getRequestTimeEpoch()) == false) + return false; + if (other.getDomainName() == null ^ this.getDomainName() == null) + return false; + if (other.getDomainName() != null && other.getDomainName().equals(this.getDomainName()) == false) + return false; + if (other.getDomainPrefix() == null ^ this.getDomainPrefix() == null) + return false; + if (other.getDomainPrefix() != null && other.getDomainPrefix().equals(this.getDomainPrefix()) == false) + return false; + if (other.getProtocol() == null ^ this.getProtocol() == null) + return false; + if (other.getProtocol() != null && other.getProtocol().equals(this.getProtocol()) == false) + return false; return true; } @@ -415,6 +602,12 @@ public int hashCode() { hashCode = prime * hashCode + ((getPath() == null) ? 0 : getPath().hashCode()); hashCode = prime * hashCode + ((getAuthorizer() == null) ? 0 : getAuthorizer().hashCode()); hashCode = prime * hashCode + ((getOperationName() == null) ? 0: getOperationName().hashCode()); + hashCode = prime * hashCode + ((getExtendedRequestId() == null) ? 0 : getExtendedRequestId().hashCode()); + hashCode = prime * hashCode + ((getRequestTime() == null) ? 0 : getRequestTime().hashCode()); + hashCode = prime * hashCode + ((getRequestTimeEpoch() == null) ? 0 : getRequestTimeEpoch().hashCode()); + hashCode = prime * hashCode + ((getDomainName() == null) ? 0 : getDomainName().hashCode()); + hashCode = prime * hashCode + ((getDomainPrefix() == null) ? 0 : getDomainPrefix().hashCode()); + hashCode = prime * hashCode + ((getProtocol() == null) ? 0 : getProtocol().hashCode()); return hashCode; } diff --git a/aws-lambda-java-events/src/main/java/com/amazonaws/services/lambda/runtime/events/ActiveMQEvent.java b/aws-lambda-java-events/src/main/java/com/amazonaws/services/lambda/runtime/events/ActiveMQEvent.java index 46791980e..e896a223e 100644 --- a/aws-lambda-java-events/src/main/java/com/amazonaws/services/lambda/runtime/events/ActiveMQEvent.java +++ b/aws-lambda-java-events/src/main/java/com/amazonaws/services/lambda/runtime/events/ActiveMQEvent.java @@ -18,6 +18,7 @@ import lombok.EqualsAndHashCode; import lombok.NoArgsConstructor; import java.util.List; +import java.util.Map; /** * Represents an Active MQ event sent to Lambda @@ -52,6 +53,7 @@ public static class ActiveMQMessage { private String data; private long brokerInTime; private long brokerOutTime; + private Map properties; } @Data diff --git a/aws-lambda-java-events/src/main/java/com/amazonaws/services/lambda/runtime/events/AppSyncLambdaAuthorizerEvent.java b/aws-lambda-java-events/src/main/java/com/amazonaws/services/lambda/runtime/events/AppSyncLambdaAuthorizerEvent.java index 3fae4d756..0bb6c8b06 100644 --- a/aws-lambda-java-events/src/main/java/com/amazonaws/services/lambda/runtime/events/AppSyncLambdaAuthorizerEvent.java +++ b/aws-lambda-java-events/src/main/java/com/amazonaws/services/lambda/runtime/events/AppSyncLambdaAuthorizerEvent.java @@ -30,6 +30,7 @@ public class AppSyncLambdaAuthorizerEvent { private RequestContext requestContext; private String authorizationToken; + private Map requestHeaders; @Data @Builder(setterPrefix = "with") diff --git a/aws-lambda-java-events/src/main/java/com/amazonaws/services/lambda/runtime/events/CloudWatchCompositeAlarmEvent.java b/aws-lambda-java-events/src/main/java/com/amazonaws/services/lambda/runtime/events/CloudWatchCompositeAlarmEvent.java new file mode 100644 index 000000000..d4090b55b --- /dev/null +++ b/aws-lambda-java-events/src/main/java/com/amazonaws/services/lambda/runtime/events/CloudWatchCompositeAlarmEvent.java @@ -0,0 +1,70 @@ +package com.amazonaws.services.lambda.runtime.events; + +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +/** + * Represents an CloudWatch Composite Alarm event. This event occurs when a composite alarm is triggered. + * + * @see Using Amazon CloudWatch alarms + */ +@Data +@Builder(setterPrefix = "with") +@NoArgsConstructor +@AllArgsConstructor +public class CloudWatchCompositeAlarmEvent { + private String source; + private String alarmArn; + private String accountId; + private String time; + private String region; + private AlarmData alarmData; + + @Data + @Builder(setterPrefix = "with") + @NoArgsConstructor + @AllArgsConstructor + public static class AlarmData { + private String alarmName; + private State state; + private PreviousState previousState; + private Configuration configuration; + } + + @Data + @Builder(setterPrefix = "with") + @NoArgsConstructor + @AllArgsConstructor + public static class State { + private String value; + private String reason; + private String reasonData; + private String timestamp; + } + + @Data + @Builder(setterPrefix = "with") + @NoArgsConstructor + @AllArgsConstructor + public static class PreviousState { + private String value; + private String reason; + private String reasonData; + private String timestamp; + private String actionsSuppressedBy; + private String actionsSuppressedReason; + } + + @Data + @Builder(setterPrefix = "with") + @NoArgsConstructor + @AllArgsConstructor + public static class Configuration { + private String alarmRule; + private String actionsSuppressor; + private Integer actionsSuppressorWaitPeriod; + private Integer actionsSuppressorExtensionPeriod; + } +} diff --git a/aws-lambda-java-events/src/main/java/com/amazonaws/services/lambda/runtime/events/CloudWatchMetricAlarmEvent.java b/aws-lambda-java-events/src/main/java/com/amazonaws/services/lambda/runtime/events/CloudWatchMetricAlarmEvent.java new file mode 100644 index 000000000..2b5f503c3 --- /dev/null +++ b/aws-lambda-java-events/src/main/java/com/amazonaws/services/lambda/runtime/events/CloudWatchMetricAlarmEvent.java @@ -0,0 +1,99 @@ +package com.amazonaws.services.lambda.runtime.events; + +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +import java.util.List; +import java.util.Map; + +/** + * Represents an CloudWatch Metric Alarm event. This event occurs when a metric alarm is triggered. + * + * @see Using Amazon CloudWatch alarms + */ +@Data +@Builder(setterPrefix = "with") +@NoArgsConstructor +@AllArgsConstructor +public class CloudWatchMetricAlarmEvent { + private String source; + private String alarmArn; + private String accountId; + private String time; + private String region; + private AlarmData alarmData; + + @Data + @Builder(setterPrefix = "with") + @NoArgsConstructor + @AllArgsConstructor + public static class AlarmData { + private String alarmName; + private State state; + private PreviousState previousState; + private Configuration configuration; + } + + @Data + @Builder(setterPrefix = "with") + @NoArgsConstructor + @AllArgsConstructor + public static class State { + private String value; + private String reason; + private String timestamp; + } + + @Data + @Builder(setterPrefix = "with") + @NoArgsConstructor + @AllArgsConstructor + public static class PreviousState { + private String value; + private String reason; + private String reasonData; + private String timestamp; + } + + @Data + @Builder(setterPrefix = "with") + @NoArgsConstructor + @AllArgsConstructor + public static class Configuration { + private String description; + private List metrics; + } + + @Data + @Builder(setterPrefix = "with") + @NoArgsConstructor + @AllArgsConstructor + public static class Metric { + private String id; + private MetricStat metricStat; + private Boolean returnData; + } + + @Data + @Builder(setterPrefix = "with") + @NoArgsConstructor + @AllArgsConstructor + public static class MetricStat { + private MetricDetail metric; + private Integer period; + private String stat; + private String unit; + } + + @Data + @Builder(setterPrefix = "with") + @NoArgsConstructor + @AllArgsConstructor + public static class MetricDetail { + private String namespace; + private String name; + private Map dimensions; + } +} diff --git a/aws-lambda-java-events/src/main/java/com/amazonaws/services/lambda/runtime/events/CognitoUserPoolCreateAuthChallengeEvent.java b/aws-lambda-java-events/src/main/java/com/amazonaws/services/lambda/runtime/events/CognitoUserPoolCreateAuthChallengeEvent.java index 6739d27f4..6074ca9b5 100644 --- a/aws-lambda-java-events/src/main/java/com/amazonaws/services/lambda/runtime/events/CognitoUserPoolCreateAuthChallengeEvent.java +++ b/aws-lambda-java-events/src/main/java/com/amazonaws/services/lambda/runtime/events/CognitoUserPoolCreateAuthChallengeEvent.java @@ -26,6 +26,7 @@ @Data @EqualsAndHashCode(callSuper = true) @NoArgsConstructor +@ToString(callSuper = true) public class CognitoUserPoolCreateAuthChallengeEvent extends CognitoUserPoolEvent { /** @@ -56,6 +57,7 @@ public CognitoUserPoolCreateAuthChallengeEvent( @Data @EqualsAndHashCode(callSuper = true) @NoArgsConstructor + @ToString(callSuper = true) public static class Request extends CognitoUserPoolEvent.Request { /** * One or more key-value pairs that you can provide as custom input to the Lambda function that you specify for the create auth challenge trigger. diff --git a/aws-lambda-java-events/src/main/java/com/amazonaws/services/lambda/runtime/events/CognitoUserPoolCustomMessageEvent.java b/aws-lambda-java-events/src/main/java/com/amazonaws/services/lambda/runtime/events/CognitoUserPoolCustomMessageEvent.java index f8642c75c..403f85393 100644 --- a/aws-lambda-java-events/src/main/java/com/amazonaws/services/lambda/runtime/events/CognitoUserPoolCustomMessageEvent.java +++ b/aws-lambda-java-events/src/main/java/com/amazonaws/services/lambda/runtime/events/CognitoUserPoolCustomMessageEvent.java @@ -26,6 +26,7 @@ @Data @EqualsAndHashCode(callSuper = true) @NoArgsConstructor +@ToString(callSuper = true) public class CognitoUserPoolCustomMessageEvent extends CognitoUserPoolEvent { /** * The request from the Amazon Cognito service. @@ -55,6 +56,7 @@ public CognitoUserPoolCustomMessageEvent( @Data @EqualsAndHashCode(callSuper = true) @NoArgsConstructor + @ToString(callSuper = true) public static class Request extends CognitoUserPoolEvent.Request { /** * One or more key-value pairs that you can provide as custom input to the Lambda function that you specify for the custom message trigger. diff --git a/aws-lambda-java-events/src/main/java/com/amazonaws/services/lambda/runtime/events/CognitoUserPoolDefineAuthChallengeEvent.java b/aws-lambda-java-events/src/main/java/com/amazonaws/services/lambda/runtime/events/CognitoUserPoolDefineAuthChallengeEvent.java index 33fcf53ad..8577c9f7a 100644 --- a/aws-lambda-java-events/src/main/java/com/amazonaws/services/lambda/runtime/events/CognitoUserPoolDefineAuthChallengeEvent.java +++ b/aws-lambda-java-events/src/main/java/com/amazonaws/services/lambda/runtime/events/CognitoUserPoolDefineAuthChallengeEvent.java @@ -26,6 +26,7 @@ @Data @EqualsAndHashCode(callSuper = true) @NoArgsConstructor +@ToString(callSuper = true) public class CognitoUserPoolDefineAuthChallengeEvent extends CognitoUserPoolEvent { /** @@ -56,6 +57,7 @@ public CognitoUserPoolDefineAuthChallengeEvent( @Data @EqualsAndHashCode(callSuper = true) @NoArgsConstructor + @ToString(callSuper = true) public static class Request extends CognitoUserPoolEvent.Request { /** * One or more key-value pairs that you can provide as custom input to the Lambda function that you specify for the define auth challenge trigger. diff --git a/aws-lambda-java-events/src/main/java/com/amazonaws/services/lambda/runtime/events/CognitoUserPoolMigrateUserEvent.java b/aws-lambda-java-events/src/main/java/com/amazonaws/services/lambda/runtime/events/CognitoUserPoolMigrateUserEvent.java index ee47c360e..381010a76 100644 --- a/aws-lambda-java-events/src/main/java/com/amazonaws/services/lambda/runtime/events/CognitoUserPoolMigrateUserEvent.java +++ b/aws-lambda-java-events/src/main/java/com/amazonaws/services/lambda/runtime/events/CognitoUserPoolMigrateUserEvent.java @@ -26,6 +26,7 @@ @Data @EqualsAndHashCode(callSuper = true) @NoArgsConstructor +@ToString(callSuper = true) public class CognitoUserPoolMigrateUserEvent extends CognitoUserPoolEvent { /** * The request from the Amazon Cognito service. @@ -55,6 +56,7 @@ public CognitoUserPoolMigrateUserEvent( @Data @EqualsAndHashCode(callSuper = true) @NoArgsConstructor + @ToString(callSuper = true) public static class Request extends CognitoUserPoolEvent.Request { /** * The username entered by the user. diff --git a/aws-lambda-java-events/src/main/java/com/amazonaws/services/lambda/runtime/events/CognitoUserPoolPostAuthenticationEvent.java b/aws-lambda-java-events/src/main/java/com/amazonaws/services/lambda/runtime/events/CognitoUserPoolPostAuthenticationEvent.java index 5d2f5089f..de1af6565 100644 --- a/aws-lambda-java-events/src/main/java/com/amazonaws/services/lambda/runtime/events/CognitoUserPoolPostAuthenticationEvent.java +++ b/aws-lambda-java-events/src/main/java/com/amazonaws/services/lambda/runtime/events/CognitoUserPoolPostAuthenticationEvent.java @@ -16,6 +16,7 @@ import lombok.Data; import lombok.EqualsAndHashCode; import lombok.NoArgsConstructor; +import lombok.ToString; import java.util.Map; @@ -29,6 +30,7 @@ @Data @EqualsAndHashCode(callSuper = true) @NoArgsConstructor +@ToString(callSuper = true) public class CognitoUserPoolPostAuthenticationEvent extends CognitoUserPoolEvent { /** @@ -52,6 +54,7 @@ public CognitoUserPoolPostAuthenticationEvent( @Data @EqualsAndHashCode(callSuper = true) @NoArgsConstructor + @ToString(callSuper = true) public static class Request extends CognitoUserPoolEvent.Request { /** * One or more key-value pairs that you can provide as custom input to the Lambda function that you specify for the post authentication trigger. diff --git a/aws-lambda-java-events/src/main/java/com/amazonaws/services/lambda/runtime/events/CognitoUserPoolPostConfirmationEvent.java b/aws-lambda-java-events/src/main/java/com/amazonaws/services/lambda/runtime/events/CognitoUserPoolPostConfirmationEvent.java index 340b2a885..4a835489d 100644 --- a/aws-lambda-java-events/src/main/java/com/amazonaws/services/lambda/runtime/events/CognitoUserPoolPostConfirmationEvent.java +++ b/aws-lambda-java-events/src/main/java/com/amazonaws/services/lambda/runtime/events/CognitoUserPoolPostConfirmationEvent.java @@ -16,6 +16,7 @@ import lombok.Data; import lombok.EqualsAndHashCode; import lombok.NoArgsConstructor; +import lombok.ToString; import java.util.Map; @@ -29,6 +30,7 @@ @EqualsAndHashCode(callSuper = true) @Data @NoArgsConstructor +@ToString(callSuper = true) public class CognitoUserPoolPostConfirmationEvent extends CognitoUserPoolEvent { /** @@ -52,6 +54,7 @@ public CognitoUserPoolPostConfirmationEvent( @Data @EqualsAndHashCode(callSuper = true) @NoArgsConstructor + @ToString(callSuper = true) public static class Request extends CognitoUserPoolEvent.Request { /** * One or more key-value pairs that you can provide as custom input to the Lambda function that you specify for the post confirmation trigger. diff --git a/aws-lambda-java-events/src/main/java/com/amazonaws/services/lambda/runtime/events/CognitoUserPoolPreAuthenticationEvent.java b/aws-lambda-java-events/src/main/java/com/amazonaws/services/lambda/runtime/events/CognitoUserPoolPreAuthenticationEvent.java index 26f45bb99..110160415 100644 --- a/aws-lambda-java-events/src/main/java/com/amazonaws/services/lambda/runtime/events/CognitoUserPoolPreAuthenticationEvent.java +++ b/aws-lambda-java-events/src/main/java/com/amazonaws/services/lambda/runtime/events/CognitoUserPoolPreAuthenticationEvent.java @@ -16,6 +16,7 @@ import lombok.Data; import lombok.EqualsAndHashCode; import lombok.NoArgsConstructor; +import lombok.ToString; import java.util.Map; @@ -29,6 +30,7 @@ @Data @EqualsAndHashCode(callSuper = true) @NoArgsConstructor +@ToString(callSuper = true) public class CognitoUserPoolPreAuthenticationEvent extends CognitoUserPoolEvent { /** @@ -52,6 +54,7 @@ public CognitoUserPoolPreAuthenticationEvent( @Data @EqualsAndHashCode(callSuper = true) @NoArgsConstructor + @ToString(callSuper = true) public static class Request extends CognitoUserPoolEvent.Request { /** * One or more name-value pairs containing the validation data in the request to register a user. diff --git a/aws-lambda-java-events/src/main/java/com/amazonaws/services/lambda/runtime/events/CognitoUserPoolPreSignUpEvent.java b/aws-lambda-java-events/src/main/java/com/amazonaws/services/lambda/runtime/events/CognitoUserPoolPreSignUpEvent.java index 2a7f0e3fc..da7a848e5 100644 --- a/aws-lambda-java-events/src/main/java/com/amazonaws/services/lambda/runtime/events/CognitoUserPoolPreSignUpEvent.java +++ b/aws-lambda-java-events/src/main/java/com/amazonaws/services/lambda/runtime/events/CognitoUserPoolPreSignUpEvent.java @@ -26,6 +26,7 @@ @Data @EqualsAndHashCode(callSuper = true) @NoArgsConstructor +@ToString(callSuper = true) public class CognitoUserPoolPreSignUpEvent extends CognitoUserPoolEvent { /** @@ -56,6 +57,7 @@ public CognitoUserPoolPreSignUpEvent( @Data @EqualsAndHashCode(callSuper = true) @NoArgsConstructor + @ToString(callSuper = true) public static class Request extends CognitoUserPoolEvent.Request { /** * One or more name-value pairs containing the validation data in the request to register a user. diff --git a/aws-lambda-java-events/src/main/java/com/amazonaws/services/lambda/runtime/events/CognitoUserPoolPreTokenGenerationEvent.java b/aws-lambda-java-events/src/main/java/com/amazonaws/services/lambda/runtime/events/CognitoUserPoolPreTokenGenerationEvent.java index db73bbd99..e49ce3c40 100644 --- a/aws-lambda-java-events/src/main/java/com/amazonaws/services/lambda/runtime/events/CognitoUserPoolPreTokenGenerationEvent.java +++ b/aws-lambda-java-events/src/main/java/com/amazonaws/services/lambda/runtime/events/CognitoUserPoolPreTokenGenerationEvent.java @@ -26,6 +26,7 @@ @Data @EqualsAndHashCode(callSuper = true) @NoArgsConstructor +@ToString(callSuper = true) public class CognitoUserPoolPreTokenGenerationEvent extends CognitoUserPoolEvent { /** * The request from the Amazon Cognito service. @@ -55,6 +56,7 @@ public CognitoUserPoolPreTokenGenerationEvent( @Data @EqualsAndHashCode(callSuper = true) @NoArgsConstructor + @ToString(callSuper = true) public static class Request extends CognitoUserPoolEvent.Request { /** * One or more key-value pairs that you can provide as custom input to the Lambda function that you specify for the pre token generation trigger. diff --git a/aws-lambda-java-events/src/main/java/com/amazonaws/services/lambda/runtime/events/CognitoUserPoolPreTokenGenerationEventV2.java b/aws-lambda-java-events/src/main/java/com/amazonaws/services/lambda/runtime/events/CognitoUserPoolPreTokenGenerationEventV2.java new file mode 100644 index 000000000..9faeb9704 --- /dev/null +++ b/aws-lambda-java-events/src/main/java/com/amazonaws/services/lambda/runtime/events/CognitoUserPoolPreTokenGenerationEventV2.java @@ -0,0 +1,134 @@ +/* Copyright 2023 Amazon.com, Inc. or its affiliates. All Rights Reserved. */ + +package com.amazonaws.services.lambda.runtime.events; + +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.EqualsAndHashCode; +import lombok.NoArgsConstructor; +import lombok.ToString; + +import java.util.Map; + +/** + * Represent the class for the Cognito User Pool Pre Token Generation Lambda Trigger V2 + *

+ * See Pre Token Generation Lambda Trigger + */ +@Data +@EqualsAndHashCode(callSuper = true) +@NoArgsConstructor +@ToString(callSuper = true) +public class CognitoUserPoolPreTokenGenerationEventV2 extends CognitoUserPoolEvent { + /** + * The request from the Amazon Cognito service. + */ + private Request request; + + /** + * The response from your Lambda trigger. + */ + private Response response; + + @Builder(setterPrefix = "with") + public CognitoUserPoolPreTokenGenerationEventV2( + String version, + String triggerSource, + String region, + String userPoolId, + String userName, + CallerContext callerContext, + Request request, + Response response) { + super(version, triggerSource, region, userPoolId, userName, callerContext); + this.request = request; + this.response = response; + } + + @Data + @EqualsAndHashCode(callSuper = true) + @NoArgsConstructor + @ToString(callSuper = true) + public static class Request extends CognitoUserPoolEvent.Request { + + private String[] scopes; + private GroupConfiguration groupConfiguration; + private Map clientMetadata; + + @Builder(setterPrefix = "with") + public Request(Map userAttributes, String[] scopes, GroupConfiguration groupConfiguration, Map clientMetadata) { + super(userAttributes); + this.scopes = scopes; + this.groupConfiguration = groupConfiguration; + this.clientMetadata = clientMetadata; + } + } + + @Data + @AllArgsConstructor + @Builder(setterPrefix = "with") + @NoArgsConstructor + public static class GroupConfiguration { + /** + * A list of the group names that are associated with the user that the identity token is issued for. + */ + private String[] groupsToOverride; + /** + * A list of the current IAM roles associated with these groups. + */ + private String[] iamRolesToOverride; + /** + * Indicates the preferred IAM role. + */ + private String preferredRole; + } + + @Data + @AllArgsConstructor + @Builder(setterPrefix = "with") + @NoArgsConstructor + public static class Response { + private ClaimsAndScopeOverrideDetails claimsAndScopeOverrideDetails; + } + + @Data + @AllArgsConstructor + @Builder(setterPrefix = "with") + @NoArgsConstructor + public static class ClaimsAndScopeOverrideDetails { + private IdTokenGeneration idTokenGeneration; + private AccessTokenGeneration accessTokenGeneration; + private GroupOverrideDetails groupOverrideDetails; + } + + @Data + @AllArgsConstructor + @Builder(setterPrefix = "with") + @NoArgsConstructor + public static class IdTokenGeneration { + private Map claimsToAddOrOverride; + private String[] claimsToSuppress; + } + + @Data + @AllArgsConstructor + @Builder(setterPrefix = "with") + @NoArgsConstructor + public static class AccessTokenGeneration { + private Map claimsToAddOrOverride; + private String[] claimsToSuppress; + private String[] scopesToAdd; + private String[] scopesToSuppress; + } + + @Data + @AllArgsConstructor + @Builder(setterPrefix = "with") + @NoArgsConstructor + public static class GroupOverrideDetails { + private String[] groupsToOverride; + private String[] iamRolesToOverride; + private String preferredRole; + } +} \ No newline at end of file diff --git a/aws-lambda-java-events/src/main/java/com/amazonaws/services/lambda/runtime/events/CognitoUserPoolVerifyAuthChallengeResponseEvent.java b/aws-lambda-java-events/src/main/java/com/amazonaws/services/lambda/runtime/events/CognitoUserPoolVerifyAuthChallengeResponseEvent.java index 13aafb554..982ff72fd 100644 --- a/aws-lambda-java-events/src/main/java/com/amazonaws/services/lambda/runtime/events/CognitoUserPoolVerifyAuthChallengeResponseEvent.java +++ b/aws-lambda-java-events/src/main/java/com/amazonaws/services/lambda/runtime/events/CognitoUserPoolVerifyAuthChallengeResponseEvent.java @@ -26,6 +26,7 @@ @Data @EqualsAndHashCode(callSuper = true) @NoArgsConstructor +@ToString(callSuper = true) public class CognitoUserPoolVerifyAuthChallengeResponseEvent extends CognitoUserPoolEvent { /** * The request from the Amazon Cognito service. @@ -55,6 +56,7 @@ public CognitoUserPoolVerifyAuthChallengeResponseEvent( @Data @EqualsAndHashCode(callSuper = true) @NoArgsConstructor + @ToString(callSuper = true) public static class Request extends CognitoUserPoolEvent.Request { /** * One or more key-value pairs that you can provide as custom input to the Lambda function that you specify for the verify auth challenge trigger. @@ -67,7 +69,7 @@ public static class Request extends CognitoUserPoolEvent.Request { /** * The answer from the user's response to the challenge. */ - private Map challengeAnswer; + private String challengeAnswer; /** * This boolean is populated when PreventUserExistenceErrors is set to ENABLED for your User Pool client */ @@ -76,7 +78,7 @@ public static class Request extends CognitoUserPoolEvent.Request { @Builder(setterPrefix = "with") public Request(Map userAttributes, Map clientMetadata, - Map challengeAnswer, + String challengeAnswer, Map privateChallengeParameters, boolean userNotFound) { super(userAttributes); 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-events/src/main/java/com/amazonaws/services/lambda/runtime/events/KafkaEvent.java b/aws-lambda-java-events/src/main/java/com/amazonaws/services/lambda/runtime/events/KafkaEvent.java index dd051d48f..aa6c00de3 100644 --- a/aws-lambda-java-events/src/main/java/com/amazonaws/services/lambda/runtime/events/KafkaEvent.java +++ b/aws-lambda-java-events/src/main/java/com/amazonaws/services/lambda/runtime/events/KafkaEvent.java @@ -43,6 +43,8 @@ public static class KafkaEventRecord { private String key; private String value; private List> headers; + private SchemaMetadata keySchemaMetadata; + private SchemaMetadata valueSchemaMetadata; } @Data @@ -59,4 +61,13 @@ public String toString() { return topic + "-" + partition; } } + + @Data + @AllArgsConstructor + @NoArgsConstructor + @Builder(setterPrefix = "with") + public static class SchemaMetadata { + private String schemaId; + private String dataFormat; + } } diff --git a/aws-lambda-java-events/src/main/java/com/amazonaws/services/lambda/runtime/events/MSKFirehoseEvent.java b/aws-lambda-java-events/src/main/java/com/amazonaws/services/lambda/runtime/events/MSKFirehoseEvent.java new file mode 100644 index 000000000..1af40ce43 --- /dev/null +++ b/aws-lambda-java-events/src/main/java/com/amazonaws/services/lambda/runtime/events/MSKFirehoseEvent.java @@ -0,0 +1,51 @@ +/* +Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +SPDX-License-Identifier: Apache-2.0 +*/ + +package com.amazonaws.services.lambda.runtime.events; + +import java.nio.ByteBuffer; +import java.util.List; +import java.util.Map; + +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +@Data +@Builder(setterPrefix = "with") +@NoArgsConstructor +@AllArgsConstructor + +public class MSKFirehoseEvent { + + private String invocationId; + + private String deliveryStreamArn; + + private String sourceMSKArn; + + private String region; + + private List records; + + @Data + @Builder(setterPrefix = "with") + @NoArgsConstructor + @AllArgsConstructor + public static class Record { + + private ByteBuffer kafkaRecordValue; + + private String recordId; + + private Long approximateArrivalEpoch; + + private Long approximateArrivalTimestamp; + + private Map mskRecordMetadata; + + } +} diff --git a/aws-lambda-java-events/src/main/java/com/amazonaws/services/lambda/runtime/events/MSKFirehoseResponse.java b/aws-lambda-java-events/src/main/java/com/amazonaws/services/lambda/runtime/events/MSKFirehoseResponse.java new file mode 100644 index 000000000..18b5aa13f --- /dev/null +++ b/aws-lambda-java-events/src/main/java/com/amazonaws/services/lambda/runtime/events/MSKFirehoseResponse.java @@ -0,0 +1,61 @@ +/* +Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +SPDX-License-Identifier: Apache-2.0 +*/ + +package com.amazonaws.services.lambda.runtime.events; + +import java.nio.ByteBuffer; +import java.util.List; + +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +/** + * Response model for Amazon Data Firehose Lambda transformation with MSK as a source. + * [+] Amazon Data Firehose Data Transformation - Data Transformation and Status Model - ... + * OK : Indicates that processing of this item succeeded. + * ProcessingFailed : Indicate that the processing of this item failed. + * Dropped : Indicates that this item should be silently dropped + */ + +@Data +@Builder(setterPrefix = "with") +@NoArgsConstructor +@AllArgsConstructor + +public class MSKFirehoseResponse { + + public enum Result { + + /** + * Indicates that processing of this item succeeded. + */ + Ok, + + /** + * Indicate that the processing of this item failed + */ + ProcessingFailed, + + /** + * Indicates that this item should be silently dropped + */ + Dropped + } + public List records; + + @Data + @NoArgsConstructor + @Builder(setterPrefix = "with") + @AllArgsConstructor + + public static class Record { + public String recordId; + public Result result; + public ByteBuffer kafkaRecordValue; + + } +} diff --git a/aws-lambda-java-events/src/main/java/com/amazonaws/services/lambda/runtime/events/S3BatchEventV2.java b/aws-lambda-java-events/src/main/java/com/amazonaws/services/lambda/runtime/events/S3BatchEventV2.java new file mode 100644 index 000000000..e9beb1f41 --- /dev/null +++ b/aws-lambda-java-events/src/main/java/com/amazonaws/services/lambda/runtime/events/S3BatchEventV2.java @@ -0,0 +1,50 @@ +package com.amazonaws.services.lambda.runtime.events; + +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +import java.util.List; +import java.util.Map; + +/** + * Event to represent the payload which is sent to Lambda by S3 Batch to perform a custom + * action when using invocation schema version 2.0. + * + * https://docs.aws.amazon.com/AmazonS3/latest/dev/batch-ops-invoke-lambda.html + */ + +@Data +@Builder(setterPrefix = "with") +@NoArgsConstructor +@AllArgsConstructor +public class S3BatchEventV2 { + + private String invocationSchemaVersion; + private String invocationId; + private Job job; + private List tasks; + + @Data + @Builder(setterPrefix = "with") + @NoArgsConstructor + @AllArgsConstructor + public static class Job { + + private String id; + private Map userArguments; + } + + @Data + @Builder(setterPrefix = "with") + @NoArgsConstructor + @AllArgsConstructor + public static class Task { + + private String taskId; + private String s3Key; + private String s3VersionId; + private String s3Bucket; + } +} diff --git a/aws-lambda-java-events/src/main/java/com/amazonaws/services/lambda/runtime/events/S3BatchResponse.java b/aws-lambda-java-events/src/main/java/com/amazonaws/services/lambda/runtime/events/S3BatchResponse.java index 4fdd12732..d584a31dd 100644 --- a/aws-lambda-java-events/src/main/java/com/amazonaws/services/lambda/runtime/events/S3BatchResponse.java +++ b/aws-lambda-java-events/src/main/java/com/amazonaws/services/lambda/runtime/events/S3BatchResponse.java @@ -62,4 +62,10 @@ public static S3BatchResponseBuilder fromS3BatchEvent(S3BatchEvent s3BatchEvent) .withInvocationId(s3BatchEvent.getInvocationId()) .withInvocationSchemaVersion(s3BatchEvent.getInvocationSchemaVersion()); } -} \ No newline at end of file + + public static S3BatchResponseBuilder fromS3BatchEvent(S3BatchEventV2 s3BatchEvent) { + return S3BatchResponse.builder() + .withInvocationId(s3BatchEvent.getInvocationId()) + .withInvocationSchemaVersion(s3BatchEvent.getInvocationSchemaVersion()); + } +} diff --git a/aws-lambda-java-events/src/main/java/com/amazonaws/services/lambda/runtime/events/ScheduledEvent.java b/aws-lambda-java-events/src/main/java/com/amazonaws/services/lambda/runtime/events/ScheduledEvent.java index 5908c39c3..405ede583 100644 --- a/aws-lambda-java-events/src/main/java/com/amazonaws/services/lambda/runtime/events/ScheduledEvent.java +++ b/aws-lambda-java-events/src/main/java/com/amazonaws/services/lambda/runtime/events/ScheduledEvent.java @@ -26,6 +26,8 @@ public class ScheduledEvent implements Serializable, Cloneable { private static final long serialVersionUID = -5810383198587331146L; + private String version; + private String account; private String region; @@ -47,6 +49,29 @@ public class ScheduledEvent implements Serializable, Cloneable { */ public ScheduledEvent() {} + /** + * @return the version number + */ + public String getVersion() { + return version; + } + + /** + * @param version the version number + */ + public void setVersion(String version) { + this.version = version; + } + + /** + * @param version version number + * @return ScheduledEvent + */ + public ScheduledEvent withVersion(String version) { + setVersion(version); + return this; + } + /** * @return the account id */ @@ -69,7 +94,7 @@ public ScheduledEvent withAccount(String account) { setAccount(account); return this; } - + /** * @return the aws region */ @@ -92,7 +117,7 @@ public ScheduledEvent withRegion(String region) { setRegion(region); return this; } - + /** * @return The details of the events (usually left blank) */ @@ -115,7 +140,7 @@ public ScheduledEvent withDetail(Map detail) { setDetail(detail); return this; } - + /** * @return The details type - see cloud watch events for more info */ @@ -138,19 +163,19 @@ public ScheduledEvent withDetailType(String detailType) { setDetailType(detailType); return this; } - + /** - * @return the soruce of the event + * @return the source of the event */ public String getSource() { return source; } /** - * @param soruce the soruce of the event + * @param source the source of the event */ - public void setSource(String soruce) { - this.source = soruce; + public void setSource(String source) { + this.source = source; } /** @@ -161,7 +186,7 @@ public ScheduledEvent withSource(String source) { setSource(source); return this; } - + /** * @return the timestamp for when the event is scheduled */ @@ -184,7 +209,7 @@ public ScheduledEvent withTime(DateTime time) { setTime(time); return this; } - + /** * @return the id of the event */ @@ -207,7 +232,7 @@ public ScheduledEvent withId(String id) { setId(id); return this; } - + /** * @return the resources used by event */ @@ -242,6 +267,8 @@ public ScheduledEvent withResources(List resources) { public String toString() { StringBuilder sb = new StringBuilder(); sb.append("{"); + if (getVersion() != null) + sb.append("version: ").append(getVersion()).append(","); if (getAccount() != null) sb.append("account: ").append(getAccount()).append(","); if (getRegion() != null) @@ -272,6 +299,10 @@ public boolean equals(Object obj) { if (obj instanceof ScheduledEvent == false) return false; ScheduledEvent other = (ScheduledEvent) obj; + if (other.getVersion() == null ^ this.getVersion() == null) + return false; + if (other.getVersion() != null && other.getVersion().equals(this.getVersion()) == false) + return false; if (other.getAccount() == null ^ this.getAccount() == null) return false; if (other.getAccount() != null && other.getAccount().equals(this.getAccount()) == false) @@ -312,6 +343,7 @@ public int hashCode() { final int prime = 31; int hashCode = 1; + hashCode = prime * hashCode + ((getVersion() == null) ? 0 : getVersion().hashCode()); hashCode = prime * hashCode + ((getAccount() == null) ? 0 : getAccount().hashCode()); hashCode = prime * hashCode + ((getRegion() == null) ? 0 : getRegion().hashCode()); hashCode = prime * hashCode + ((getDetail() == null) ? 0 : getDetail().hashCode()); @@ -331,5 +363,5 @@ public ScheduledEvent clone() { throw new IllegalStateException("Got a CloneNotSupportedException from Object.clone()", e); } } - + } diff --git a/aws-lambda-java-events/src/main/java/com/amazonaws/services/lambda/runtime/events/SecretsManagerRotationEvent.java b/aws-lambda-java-events/src/main/java/com/amazonaws/services/lambda/runtime/events/SecretsManagerRotationEvent.java index 4634c5152..3e8df5bce 100644 --- a/aws-lambda-java-events/src/main/java/com/amazonaws/services/lambda/runtime/events/SecretsManagerRotationEvent.java +++ b/aws-lambda-java-events/src/main/java/com/amazonaws/services/lambda/runtime/events/SecretsManagerRotationEvent.java @@ -35,5 +35,6 @@ public class SecretsManagerRotationEvent { private String step; private String secretId; private String clientRequestToken; + private String rotationToken; } diff --git a/aws-lambda-java-log4j2/README.md b/aws-lambda-java-log4j2/README.md index febc5c352..f13121750 100644 --- a/aws-lambda-java-log4j2/README.md +++ b/aws-lambda-java-log4j2/README.md @@ -10,17 +10,22 @@ Example for Maven pom.xml com.amazonaws aws-lambda-java-log4j2 - 1.5.0 + 1.6.0 org.apache.logging.log4j log4j-core - 2.17.0 + 2.17.1 org.apache.logging.log4j log4j-api - 2.17.0 + 2.17.1 + + + org.apache.logging.log4j + log4j-layout-template-json + 2.17.1 .... @@ -34,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 @@ -65,10 +70,10 @@ If using maven shade plugin, set the plugin configuration as follows If you are using the [John Rengelman](https://github.com/johnrengelman/shadow) Gradle shadow plugin, then the plugin configuration is as follows: ```groovy - + dependencies{ ... - implementation group: 'com.amazonaws', name: 'aws-lambda-java-log4j2', version: '1.5.0' + implementation group: 'com.amazonaws', name: 'aws-lambda-java-log4j2', version: '1.6.0' implementation group: 'org.apache.logging.log4j', name: 'log4j-core', version: log4jVersion implementation group: 'org.apache.logging.log4j', name: 'log4j-api', version: log4jVersion } @@ -83,8 +88,8 @@ shadowJar { build.dependsOn(shadowJar) ``` - -If you are using the `sam build` and `sam deploy` commands to deploy your lambda function, then you don't + +If you are using the `sam build` and `sam deploy` commands to deploy your lambda function, then you don't need to use the shadow jar plugin. The `sam` cli-tool merges itself the `Log4j2Plugins.dat` files. @@ -94,22 +99,29 @@ Add the following file `/src/main/resources/log4j2.xml` ```xml - + - - + + + %d{yyyy-MM-dd HH:mm:ss} %X{AWSRequestId} %-5p %c{1}:%L - %m%n - + + + + + - + ``` +If the `AWS_LAMBDA_LOG_FORMAT` is set to `JSON`, the `LambdaJSONFormat` formatter will be applied, otherwise the `LambdaTextFormat`. + ### 3. Example code ```java @@ -117,6 +129,8 @@ package example; import com.amazonaws.services.lambda.runtime.Context; +import static org.apache.logging.log4j.CloseableThreadContext.put; +import org.apache.logging.log4j.CloseableThreadContext.Instance; import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; @@ -136,6 +150,12 @@ public class Hello { logger.error("log data from log4j err. \n this is a continuation of log4j.err"); + // When logging in JSON, you can also add custom fields + // In java11+ you can use the `try (var ctx = put("name", name)) {}` structure + Instance ctx = put("name", name); + logger.info("log line with input name"); + ctx.close(); + // Return will include the log stream name so you can look // up the log later. return String.format("Hello %s. log stream = %s", name, context.getLogStreamName()); diff --git a/aws-lambda-java-log4j2/RELEASE.CHANGELOG.md b/aws-lambda-java-log4j2/RELEASE.CHANGELOG.md index ab0c7958e..49535d388 100644 --- a/aws-lambda-java-log4j2/RELEASE.CHANGELOG.md +++ b/aws-lambda-java-log4j2/RELEASE.CHANGELOG.md @@ -1,3 +1,11 @@ +### October 24, 2023 +`1.6.0`: +- Log level and log format support + +### January 04, 2022 +`1.5.1`: +- Updated `log4j-core` and `log4j-api` dependencies to `2.17.1` + ### December 18, 2021 `1.5.0`: - Updated `log4j-core` and `log4j-api` dependencies to `2.17.0` @@ -26,4 +34,4 @@ ### June 29, 2017 `1.0.0`: -- Initial release of AWS Lambda Log4j2 support \ No newline at end of file +- Initial release of AWS Lambda Log4j2 support diff --git a/aws-lambda-java-log4j2/pom.xml b/aws-lambda-java-log4j2/pom.xml index b9e8dad50..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.5.0 + 1.6.1 jar AWS Lambda Java Log4j 2.x Libraries @@ -34,7 +34,7 @@ 1.8 1.8 - 2.17.0 + 2.25.3 @@ -48,7 +48,7 @@ com.amazonaws aws-lambda-java-core - 1.2.1 + 1.2.3 org.apache.logging.log4j @@ -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-log4j2/src/main/java/com/amazonaws/services/lambda/runtime/log4j2/LambdaAppender.java b/aws-lambda-java-log4j2/src/main/java/com/amazonaws/services/lambda/runtime/log4j2/LambdaAppender.java index 5c1dd3158..a511c8dea 100755 --- a/aws-lambda-java-log4j2/src/main/java/com/amazonaws/services/lambda/runtime/log4j2/LambdaAppender.java +++ b/aws-lambda-java-log4j2/src/main/java/com/amazonaws/services/lambda/runtime/log4j2/LambdaAppender.java @@ -2,16 +2,24 @@ import com.amazonaws.services.lambda.runtime.LambdaRuntime; import com.amazonaws.services.lambda.runtime.LambdaRuntimeInternal; + import com.amazonaws.services.lambda.runtime.LambdaLogger; +import com.amazonaws.services.lambda.runtime.logging.LogFormat; +import com.amazonaws.services.lambda.runtime.logging.LogLevel; +import org.apache.logging.log4j.Level; import org.apache.logging.log4j.core.Filter; import org.apache.logging.log4j.core.Layout; import org.apache.logging.log4j.core.LogEvent; import org.apache.logging.log4j.core.appender.AbstractAppender; import org.apache.logging.log4j.core.config.plugins.Plugin; +import org.apache.logging.log4j.core.config.plugins.PluginAttribute; import org.apache.logging.log4j.core.config.plugins.PluginBuilderFactory; +import org.apache.logging.log4j.core.config.plugins.PluginElement; import java.io.Serializable; +import java.util.HashMap; +import java.util.Map; /** * Class to append log4j2 logs from AWS Lambda function to CloudWatch @@ -20,6 +28,9 @@ @Plugin(name = LambdaAppender.PLUGIN_NAME, category = LambdaAppender.PLUGIN_CATEGORY, elementType = LambdaAppender.PLUGIN_TYPE, printObject = true) public class LambdaAppender extends AbstractAppender { + static { + LambdaRuntimeInternal.setUseLog4jAppender(true); + } public static final String PLUGIN_NAME = "Lambda"; public static final String PLUGIN_CATEGORY = "Core"; @@ -27,6 +38,17 @@ public class LambdaAppender extends AbstractAppender { private LambdaLogger logger = LambdaRuntime.getLogger(); + private static LogFormat logFormat = LogFormat.TEXT; + + private static final Map logLevelMapper = new HashMap() {{ + put(Level.TRACE, LogLevel.TRACE); + put(Level.DEBUG, LogLevel.DEBUG); + put(Level.INFO, LogLevel.INFO); + put(Level.WARN, LogLevel.WARN); + put(Level.ERROR, LogLevel.ERROR); + put(Level.FATAL, LogLevel.FATAL); + }}; + /** * Builder class that follows log4j2 plugin convention * @param Generic Builder class @@ -34,13 +56,25 @@ public class LambdaAppender extends AbstractAppender { public static class Builder> extends AbstractAppender.Builder implements org.apache.logging.log4j.core.util.Builder { + @PluginAttribute(value = "format", defaultString = "TEXT") + LogFormat logFormat; + @PluginElement("LambdaTextFormat") + private LambdaTextFormat lambdaTextFormat; + @PluginElement("LambdaJsonFormat") + private LambdaJsonFormat lambdaJsonFormat; + /** * creates a new LambdaAppender * @return a new LambdaAppender */ public LambdaAppender build() { - return new LambdaAppender(super.getName(), super.getFilter(), super.getOrCreateLayout(), - super.isIgnoreExceptions()); + Layout layout; + if (logFormat == LogFormat.TEXT) { + layout = lambdaTextFormat != null ? lambdaTextFormat.getLayout() : super.getOrCreateLayout(); + } else { + layout = lambdaJsonFormat != null ? lambdaJsonFormat.getLayout() : super.getOrCreateLayout(); + } + return new LambdaAppender(super.getName(), super.getFilter(), layout, super.isIgnoreExceptions()); } } @@ -63,7 +97,15 @@ public static > B newBuilder() { */ private LambdaAppender(String name, Filter filter, Layout layout, boolean ignoreExceptions) { super(name, filter, layout, ignoreExceptions); - LambdaRuntimeInternal.setUseLog4jAppender(true); + } + + /** + * Converts log4j Level into Lambda LogLevel + * @param level the log4j log level + * @return Lambda log leve + */ + private LogLevel toLambdaLogLevel(Level level) { + return logLevelMapper.getOrDefault(level, LogLevel.UNDEFINED); } /** @@ -71,6 +113,6 @@ private LambdaAppender(String name, Filter filter, Layout layout) { + return new LambdaJsonFormat(layout); + } + + private LambdaJsonFormat(Layout layout) { + this.layout = layout; + } + + public Layout getLayout() { + return layout; + } +} diff --git a/aws-lambda-java-log4j2/src/main/java/com/amazonaws/services/lambda/runtime/log4j2/LambdaTextFormat.java b/aws-lambda-java-log4j2/src/main/java/com/amazonaws/services/lambda/runtime/log4j2/LambdaTextFormat.java new file mode 100644 index 000000000..0bd0304a0 --- /dev/null +++ b/aws-lambda-java-log4j2/src/main/java/com/amazonaws/services/lambda/runtime/log4j2/LambdaTextFormat.java @@ -0,0 +1,29 @@ +/* Copyright 2023 Amazon.com, Inc. or its affiliates. All Rights Reserved. */ + +package com.amazonaws.services.lambda.runtime.log4j2; + +import org.apache.logging.log4j.core.Layout; +import org.apache.logging.log4j.core.config.plugins.Plugin; +import org.apache.logging.log4j.core.config.plugins.PluginElement; +import org.apache.logging.log4j.core.config.plugins.PluginFactory; + +import java.io.Serializable; + +@Plugin(name = "LambdaTextFormat", category = "core", printObject = true) +public class LambdaTextFormat { + + private Layout layout; + + @PluginFactory + public static LambdaTextFormat createNode(@PluginElement("Layout") Layout layout) { + return new LambdaTextFormat(layout); + } + + private LambdaTextFormat(Layout layout) { + this.layout = layout; + } + + public Layout getLayout() { + return layout; + } +} diff --git a/aws-lambda-java-log4j2/src/main/resources/LambdaLayout.json b/aws-lambda-java-log4j2/src/main/resources/LambdaLayout.json new file mode 100644 index 000000000..975f4b529 --- /dev/null +++ b/aws-lambda-java-log4j2/src/main/resources/LambdaLayout.json @@ -0,0 +1,39 @@ +{ + "timestamp": { + "$resolver": "timestamp", + "pattern": { + "format": "yyyy-MM-dd'T'HH:mm:ss.SSS'Z'", + "timeZone": "UTC" + } + }, + "level": { + "$resolver": "level", + "field": "name" + }, + "message": { + "$resolver": "message" + }, + "logger": { + "$resolver": "logger", + "field": "name" + }, + + "errorType": { + "$resolver": "exception", + "field": "className" + }, + "errorMessage": { + "$resolver": "exception", + "field": "message" + }, + "stackTrace": { + "$resolver": "exception", + "field": "stackTrace" + }, + + "labels": { + "$resolver": "mdc", + "flatten": true, + "stringified": true + } +} \ No newline at end of file diff --git a/aws-lambda-java-runtime-interface-client/.gitignore b/aws-lambda-java-runtime-interface-client/.gitignore index 73a7f3283..b1e77deb4 100644 --- a/aws-lambda-java-runtime-interface-client/.gitignore +++ b/aws-lambda-java-runtime-interface-client/.gitignore @@ -1 +1,2 @@ compile-flags.txt +ric-dev-environment/codeartifact-properties.mk 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 6d23db6d7..6c3a268fb 100644 --- a/aws-lambda-java-runtime-interface-client/Makefile +++ b/aws-lambda-java-runtime-interface-client/Makefile @@ -1,3 +1,20 @@ +x86_64_ALIAS := amd64 +aarch64_ALIAS := arm64 +ARCHITECTURE := $(shell arch) +ARCHITECTURE_ALIAS := $($(shell echo "$(ARCHITECTURE)_ALIAS")) +ARCHITECTURE_ALIAS := $(or $(ARCHITECTURE_ALIAS),amd64) # on any other archs defaulting to amd64 + +# Java 8 does not support passing some args (such add --add-opens) so we need to clear them +ifeq ($(IS_JAVA_8),true) + EXTRA_LOAD_ARG := -DargLineForReflectionTestOnly="" +else + EXTRA_LOAD_ARG := +endif + +# This optional module exports MAVEN_REPO_URL, MAVEN_REPO_USERNAME and MAVEN_REPO_PASSWORD environment variables +# making it possible to publish resulting artifacts to a codeartifact maven repository +-include ric-dev-environment/codeartifact-repo.mk + .PHONY: target target: $(info ${HELP_MESSAGE}) @@ -5,16 +22,20 @@ target: .PHONY: test test: - mvn test + mvn test $(EXTRA_LOAD_ARG) .PHONY: setup-codebuild-agent setup-codebuild-agent: - docker build -t codebuild-agent - < test/integration/codebuild-local/Dockerfile.agent + docker build -t codebuild-agent \ + --build-arg ARCHITECTURE=$(ARCHITECTURE_ALIAS) \ + - < test/integration/codebuild-local/Dockerfile.agent .PHONY: test-smoke test-smoke: setup-codebuild-agent - CODEBUILD_IMAGE_TAG=codebuild-agent test/integration/codebuild-local/test_one.sh test/integration/codebuild/buildspec.os.alpine.yml alpine 3.12 corretto11 - CODEBUILD_IMAGE_TAG=codebuild-agent test/integration/codebuild-local/test_one.sh test/integration/codebuild/buildspec.os.amazoncorretto.yml amazoncorretto amazoncorretto 11 + CODEBUILD_IMAGE_TAG=codebuild-agent test/integration/codebuild-local/test_one.sh test/integration/codebuild/buildspec.os.alpine.yml alpine 3.15 corretto11 linux/amd64 + CODEBUILD_IMAGE_TAG=codebuild-agent test/integration/codebuild-local/test_one.sh test/integration/codebuild/buildspec.os.alpine.yml alpine 3.15 corretto11 linux/arm64/v8 + CODEBUILD_IMAGE_TAG=codebuild-agent test/integration/codebuild-local/test_one.sh test/integration/codebuild/buildspec.os.amazoncorretto.yml amazoncorretto amazoncorretto 11 linux/amd64 + CODEBUILD_IMAGE_TAG=codebuild-agent test/integration/codebuild-local/test_one.sh test/integration/codebuild/buildspec.os.amazoncorretto.yml amazoncorretto amazoncorretto 11 linux/arm64/v8 .PHONY: test-integ test-integ: setup-codebuild-agent @@ -30,7 +51,23 @@ pr: test test-smoke .PHONY: build build: - mvn clean install + mvn clean install $(EXTRA_LOAD_ARG) + mvn install -P linux-x86_64 $(EXTRA_LOAD_ARG) + mvn install -P linux_musl-x86_64 $(EXTRA_LOAD_ARG) + mvn install -P linux-aarch64 $(EXTRA_LOAD_ARG) + mvn install -P linux_musl-aarch64 $(EXTRA_LOAD_ARG) + +.PHONY: publish +publish: + ./ric-dev-environment/publish_snapshot.sh + +.PHONY: 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 @@ -41,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 8332a11f2..4e03f041f 100644 --- a/aws-lambda-java-runtime-interface-client/README.md +++ b/aws-lambda-java-runtime-interface-client/README.md @@ -13,7 +13,7 @@ You can include this package in your preferred base image to make that base imag Choose a preferred base image. The Runtime Interface Client is tested on Amazon Linux, Alpine, Ubuntu, Debian, and CentOS. The requirements are that the image is: -* built for x86_64 +* built for x86_64 and ARM64 * contains Java >= 8 * contains glibc >= 2.17 or musl @@ -24,7 +24,7 @@ The Runtime Interface Client library can be installed into the image separate fr Dockerfile ```dockerfile # we'll use Amazon Linux 2 + Corretto 11 as our base -FROM amazoncorretto:11 as base +FROM public.ecr.aws/amazoncorretto/amazoncorretto:11 as base # configure the build environment FROM base as build @@ -37,7 +37,7 @@ RUN mvn dependency:go-offline dependency:copy-dependencies # compile the function ADD . . -RUN mvn package +RUN mvn package # copy the function artifact and dependencies onto a clean base FROM base @@ -70,7 +70,7 @@ pom.xml com.amazonaws aws-lambda-java-runtime-interface-client - 2.0.0 + 2.8.7 @@ -106,18 +106,18 @@ public class App { ### Local Testing -To make it easy to locally test Lambda functions packaged as container images we open-sourced a lightweight web-server, Lambda Runtime Interface Emulator (RIE), which allows your function packaged as a container image to accept HTTP requests. You can install the [AWS Lambda Runtime Interface Emulator](https://github.com/aws/aws-lambda-runtime-interface-emulator) on your local machine to test your function. Then when you run the image function, you set the entrypoint to be the emulator. +To make it easy to locally test Lambda functions packaged as container images we open-sourced a lightweight web-server, Lambda Runtime Interface Emulator (RIE), which allows your function packaged as a container image to accept HTTP requests. You can install the [AWS Lambda Runtime Interface Emulator](https://github.com/aws/aws-lambda-runtime-interface-emulator) on your local machine to test your function. Then when you run the image function, you set the entrypoint to be the emulator. *To install the emulator and test your Lambda function* -1) Run the following command to download the RIE from GitHub and install it on your local machine. +1) Run the following command to download the RIE from GitHub and install it on your local machine. ```shell script mkdir -p ~/.aws-lambda-rie && \ curl -Lo ~/.aws-lambda-rie/aws-lambda-rie https://github.com/aws/aws-lambda-runtime-interface-emulator/releases/latest/download/aws-lambda-rie && \ chmod +x ~/.aws-lambda-rie/aws-lambda-rie ``` -2) Run your Lambda image function using the docker run command. +2) Run your Lambda image function using the docker run command. ```shell script docker run -d -v ~/.aws-lambda-rie:/aws-lambda -p 9000:8080 \ @@ -126,9 +126,9 @@ docker run -d -v ~/.aws-lambda-rie:/aws-lambda -p 9000:8080 \ /usr/bin/java -cp './*' com.amazonaws.services.lambda.runtime.api.client.AWSLambda example.App::sayHello ``` -This runs the image as a container and starts up an endpoint locally at `http://localhost:9000/2015-03-31/functions/function/invocations`. +This runs the image as a container and starts up an endpoint locally at `http://localhost:9000/2015-03-31/functions/function/invocations`. -3) Post an event to the following endpoint using a curl command: +3) Post an event to the following endpoint using a curl command: ```shell script curl -XPOST "http://localhost:9000/2015-03-31/functions/function/invocations" -d '{}' @@ -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: @@ -151,6 +194,33 @@ DOCKERHUB_PASSWORD= ``` Recommended way is to set the Docker Hub credentials in CodeBuild job by retrieving them from AWS Secrets Manager. +## Configuration +The `aws-lambda-java-runtime-interface-client` JAR is a large uber jar, which contains compiled C libraries +for x86_64 and aarch_64 for glibc and musl LIBC implementations. If the size is an issue, you can pick a smaller +platform-specific JAR by setting the ``. +``` + + + com.amazonaws + aws-lambda-java-runtime-interface-client + 2.8.7 + linux-x86_64 + +``` + +Available platform classifiers: `linux-x86_64`, `linux-aarch_64`, `linux_musl-aarch_64`, `linux_musl-x86_64` + +The Lambda runtime interface client tries to load compatible library during execution, by unpacking it to a temporary +location `/tmp/.libaws-lambda-jni.so`. +If this behaviour is not desirable, it is possible to extract the `.so` files during build time and specify the location via +`com.amazonaws.services.lambda.runtime.api.client.runtimeapi.NativeClient.JNI` system property, like +``` +ENTRYPOINT [ "/usr/bin/java", +"-Dcom.amazonaws.services.lambda.runtime.api.client.runtimeapi.NativeClient.JNI=/function/libaws-lambda-jni.linux_x86_64.so" +"-cp", "./*", +"com.amazonaws.services.lambda.runtime.api.client.AWSLambda" ] +``` + ## Security If you discover a potential security issue in this project we ask that you notify AWS/Amazon Security via our [vulnerability reporting page](http://aws.amazon.com/security/vulnerability-reporting/). Please do **not** create a public github issue. diff --git a/aws-lambda-java-runtime-interface-client/RELEASE.CHANGELOG.md b/aws-lambda-java-runtime-interface-client/RELEASE.CHANGELOG.md index 8b74e9f94..93d8cf23a 100644 --- a/aws-lambda-java-runtime-interface-client/RELEASE.CHANGELOG.md +++ b/aws-lambda-java-runtime-interface-client/RELEASE.CHANGELOG.md @@ -1,3 +1,102 @@ +### 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)) + +### August 7, 2024 +`2.6.0` +- Runtime API client improvements: use Lambda-Runtime-Function-Error-Type for reporting errors in format "Runtime." + +### June 28, 2024 +`2.5.1` +- Runtime API client improvements: fix a DNS cache issue +- Runtime API client improvements: fix circular exception references causing stackOverflow + +### March 20, 2024 +`2.5.0` +- Runtime API client improvements ([#471](https://github.com/aws/aws-lambda-java-libs/pull/471)) + +### February 27, 2024 +`2.4.2` +- Exceptions caught by the runtime are logged as ERROR in JSON mode + +### September 4, 2023 +`2.4.1` +- Null pointer bugfix ([#439](https://github.com/aws/aws-lambda-java-libs/pull/439)) + +### August 29, 2023 +`2.4.0` +- Logging improvements ([#436](https://github.com/aws/aws-lambda-java-libs/pull/436)) + +### July 17, 2023 +`2.3.3` +- Build platform specific JAR files +- NativeClient optimisations + +### April 14, 2023 +`2.3.2` +- Add curl patch + +### March 16, 2023 +`2.3.1` +- ignore module-info for CDS preparation purposes +- clear thread interrupted flag instead of exiting Lambda Runtime + +### March 14, 2023 +`2.3.0` +- added CRaC context implementation +- added runtime hooks execution logic +- updated serialisation dependency +- reduced Reflection API usage + +### February 3, 2023 +`2.2.0` +- Added timestamps to TLV +- Removed legacy `init` method support +- libcurl updated to version 7.86 +- Support sockets as transport for framed telemetry +- Updated aws-lambda-java-core to 1.2.2 + +### April 11, 2022 +`2.1.1` +- fix: Re-build of the x86_64/aarch64 artifacts + +### January 20, 2022 +`2.1.0` +- fix: Added support for ARM64 architecture + ### Sept 29, 2021 `2.0.0` - Added support for ARM64 architecture @@ -9,4 +108,4 @@ ### December 01, 2020 `1.0.0`: -- Initial release of AWS Lambda Java Runtime Interface Client \ No newline at end of file +- Initial release of AWS Lambda Java Runtime Interface Client diff --git a/aws-lambda-java-runtime-interface-client/build-tools/checkstyle.xml b/aws-lambda-java-runtime-interface-client/build-tools/checkstyle.xml new file mode 100644 index 000000000..263834dc4 --- /dev/null +++ b/aws-lambda-java-runtime-interface-client/build-tools/checkstyle.xml @@ -0,0 +1,115 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/aws-lambda-java-runtime-interface-client/pom.xml b/aws-lambda-java-runtime-interface-client/pom.xml index 09716367d..ab7166c84 100644 --- a/aws-lambda-java-runtime-interface-client/pom.xml +++ b/aws-lambda-java-runtime-interface-client/pom.xml @@ -1,254 +1,463 @@ - 4.0.0 + 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.9.0 + jar - com.amazonaws - aws-lambda-java-runtime-interface-client - 2.0.0 - jar + AWS Lambda Java Runtime Interface Client + + The AWS Lambda Java Runtime Interface Client implements the Lambda programming model for Java + + https://aws.amazon.com/lambda/ + + + Apache License, Version 2.0 + https://aws.amazon.com/apache2.0 + repo + + + + https://github.com/aws/aws-lambda-java-libs.git + + + + AWS Lambda team + Amazon Web Services + https://aws.amazon.com/ + + - AWS Lambda Java Runtime Interface Client - - The AWS Lambda Java Runtime Interface Client implements the Lambda programming model for Java - - https://aws.amazon.com/lambda/ - - - Apache License, Version 2.0 - https://aws.amazon.com/apache2.0 - repo - - - - https://github.com/aws/aws-lambda-java-libs.git - - - - AWS Lambda team - Amazon Web Services - https://aws.amazon.com/ - - - - 1.8 - 1.8 - + + UTF-8 + UTF-8 + 0.8.12 + 2.4 + 3.1.1 + 5.9.2 + 3.4.0 + + true + + + + + --add-opens java.base/java.net=ALL-UNNAMED + - - - com.amazonaws - aws-lambda-java-core - 1.2.0 - - - com.amazonaws - aws-lambda-java-serialization - 1.0.0 - + + + com.amazonaws + aws-lambda-java-core + 1.4.0 + + + com.amazonaws + aws-lambda-java-serialization + 1.2.0 + + + software.amazon.awssdk + utils-lite + 2.34.0 + + + org.junit.jupiter + junit-jupiter-engine + ${junit-jupiter.version} + test + + + org.junit.jupiter + junit-jupiter + ${junit-jupiter.version} + test + + + org.mockito + mockito-core + 4.11.0 + test + + + org.mockito + mockito-junit-jupiter + 4.11.0 + test + + + com.squareup.okhttp3 + mockwebserver + 4.12.0 + test + + - - org.junit.jupiter - junit-jupiter-engine - 5.7.0 - test - - - org.junit.jupiter - junit-jupiter-api - 5.7.0 - test - - - - - - - com.allogy.maven.wagon - maven-s3-wagon - 1.2.0 - - - - - maven-surefire-plugin - 2.22.2 - - - maven-failsafe-plugin - 2.22.2 - - - org.apache.maven.plugins - maven-antrun-plugin - 1.7 - - - build-jni-lib - prepare-package - - run - - - - - - - - - - - - - org.apache.maven.plugins - maven-compiler-plugin - 2.3.2 - - UTF-8 - - - - org.apache.maven.plugins - maven-jar-plugin - - - - com.amazonaws.services.lambda.runtime.api.client.AWSLambda - true - true - - - - - - org.jacoco - jacoco-maven-plugin - 0.8.6 - - - default-prepare-agent - - prepare-agent - - - - default-report - test - - report - - - - default-check - test - - check - - - - - PACKAGE - - - LINE - COVEREDRATIO - 0 - - - - - - - - - - - - - - - dev - + + + + com.allogy.maven.wagon + maven-s3-wagon + 1.2.0 + + - - org.apache.maven.plugins - maven-javadoc-plugin - 2.9.1 - - -Xdoclint:none - - - - attach-javadocs - - jar - - - - + + maven-install-plugin + org.apache.maven.plugins + ${maven-install-plugin.version} + + + maven-deploy-plugin + org.apache.maven.plugins + ${maven-deploy-plugin.version} + + + maven-surefire-plugin + 3.0.0-M9 + + ${argLineForReflectionTestOnly} ${argLine} + + + + org.junit.jupiter + junit-jupiter-engine + ${junit-jupiter.version} + + + + + maven-failsafe-plugin + 2.22.2 + + + org.apache.maven.plugins + maven-antrun-plugin + 1.7 + + + build-jni-lib-for-tests + generate-test-sources + + run + + + + + + + + + + + + + + build-jni-lib + prepare-package + + run + + + + + + + + + + + + + + + + org.apache.maven.plugins + maven-compiler-plugin + 3.11.0 + + 1.8 + 1.8 + UTF-8 + + + + org.apache.maven.plugins + maven-jar-plugin + 3.3.0 + + + + com.amazonaws.services.lambda.runtime.api.client.AWSLambda + true + true + + + ${ric.classifier} + + com/ + jni/*${ric.classifier}.so + + + + + org.jacoco + jacoco-maven-plugin + ${jacoco.maven.plugin.version} + + + + **/*Exception.class + + **/Resource.class + + **/dto/*.class + + **/ReservedRuntimeEnvironmentVariables.class + **/RapidErrorType.class + + **/FrameType.class + **/StructuredLogMessage.class + + **/AWSLambda.class + + + + + default-prepare-agent + + prepare-agent + + + + default-report + test + + report + + + + default-check + test + + check + + + + + BUNDLE + + + LINE + COVEREDRATIO + 0.5 + + + + + + + + + + org.apache.maven.plugins + maven-checkstyle-plugin + ${maven-checkstyle-plugin.version} + + build-tools/checkstyle.xml + true + true + true + + + + validate + validate + + check + + + + - - - - release - - - - org.apache.maven.plugins - maven-source-plugin - 2.2.1 - - - attach-sources - - jar-no-fork - - - - - - org.apache.maven.plugins - maven-javadoc-plugin - 2.9.1 - - -Xdoclint:none - - - - attach-javadocs - - jar - - - - - - org.apache.maven.plugins - maven-gpg-plugin - 1.5 - - - sign-artifacts - verify - - sign - - - - - - org.sonatype.plugins - nexus-staging-maven-plugin - 1.6.3 - true - - sonatype-nexus-staging - https://aws.oss.sonatype.org/ - false - - - - - - + + + + + + dev + + + + org.apache.maven.plugins + maven-javadoc-plugin + 2.9.1 + + -Xdoclint:none + + + + attach-javadocs + + jar + + + + + + + + + release + + + + org.apache.maven.plugins + maven-source-plugin + 2.2.1 + + + attach-sources + + jar-no-fork + + + + + + org.apache.maven.plugins + maven-javadoc-plugin + 2.9.1 + + -Xdoclint:none + + + + attach-javadocs + + jar + + + + + + org.apache.maven.plugins + maven-gpg-plugin + 1.5 + + + sign-artifacts + verify + + sign + + + + + + org.sonatype.central + central-publishing-maven-plugin + 0.8.0 + true + + 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 + + + + + + + + + + + ci-repo + + + ci-repo + ${env.MAVEN_REPO_URL} + + + + + linux-x86_64 + + linux + x86_64 + linux-x86_64 + + + + linux_musl-x86_64 + + linux_musl + x86_64 + linux_musl-x86_64 + + + + linux-aarch64 + + linux + aarch_64 + linux-aarch_64 + + + + linux_musl-aarch64 + + linux_musl + aarch_64 + linux_musl-aarch_64 + + + diff --git a/aws-lambda-java-runtime-interface-client/ric-dev-environment/codeartifact-repo.mk b/aws-lambda-java-runtime-interface-client/ric-dev-environment/codeartifact-repo.mk new file mode 100644 index 000000000..022c49e79 --- /dev/null +++ b/aws-lambda-java-runtime-interface-client/ric-dev-environment/codeartifact-repo.mk @@ -0,0 +1,27 @@ + +ifneq ("$(wildcard ric-dev-environment/codeartifact-properties.mk)","") + + include ric-dev-environment/codeartifact-properties.mk + $(info Found codeartifact-properties.mk module) + + export MAVEN_REPO_URL:=$(shell aws codeartifact get-repository-endpoint \ + --domain ${CODE_ARTIFACT_DOMAIN} \ + --repository ${CODE_ARTIFACT_REPO_NAME} \ + --format maven \ + --output text \ + --region ${CODE_ARTIFACT_REPO_REGION}) + + export MAVEN_REPO_PASSWORD:=$(shell aws codeartifact get-authorization-token \ + --domain ${CODE_ARTIFACT_DOMAIN} \ + --domain-owner ${CODE_ARTIFACT_REPO_ACCOUNT} \ + --query authorizationToken \ + --output text \ + --region ${CODE_ARTIFACT_REPO_REGION}) + + export MAVEN_REPO_USERNAME:=aws + + $(info MAVEN_REPO_URL: $(MAVEN_REPO_URL)) + # $(info MAVEN_REPO_PASSWORD: $(MAVEN_REPO_PASSWORD)) + $(info MAVEN_REPO_USERNAME: $(MAVEN_REPO_USERNAME)) + $(info CODE_ARTIFACT_REPO_NAME: $(CODE_ARTIFACT_REPO_NAME)) +endif diff --git a/aws-lambda-java-runtime-interface-client/ric-dev-environment/publish_snapshot.sh b/aws-lambda-java-runtime-interface-client/ric-dev-environment/publish_snapshot.sh new file mode 100755 index 000000000..9d2f9837f --- /dev/null +++ b/aws-lambda-java-runtime-interface-client/ric-dev-environment/publish_snapshot.sh @@ -0,0 +1,54 @@ +#!/bin/bash -x + +set -e + +projectVersion=$(mvn help:evaluate -Dexpression=project.version -q -DforceStdout) +if [[ -z ${ENABLE_SNAPSHOT} ]]; then + echo "Skipping SNAPSHOT deployment, as ENABLE_SNAPSHOT environment variable is not defined" + exit +fi + +echo "Deploying SNAPSHOT artifact" +if [[ ${projectVersion} != *"SNAPSHOT"* ]]; then + snapshotProjectVersion="${projectVersion}-SNAPSHOT" + echo "projectVersion: ${projectVersion}" + echo "snapshotProjectVersion: ${snapshotProjectVersion}" + mvn versions:set "-DnewVersion=${snapshotProjectVersion}" +else + echo "Already -SNAPSHOT version" +fi + +# get the updated project version +snapshotProjectVersion=$(mvn help:evaluate -Dexpression=project.version -q -DforceStdout) +echo "Updated project version is ${snapshotProjectVersion}" + +CLASSIFIERS_ARRAY=("linux-x86_64" "linux_musl-x86_64" "linux-aarch_64" "linux_musl-aarch_64") + +for str in "${CLASSIFIERS_ARRAY[@]}"; do + FILES="${FILES}target/aws-lambda-java-runtime-interface-client-$projectVersion-$str.jar," + CLASSIFIERS="${CLASSIFIERS}${str}," + TYPES="${TYPES}jar," +done + +# remove the last "," +FILES=${FILES%?} +CLASSIFIERS=${CLASSIFIERS%?} +TYPES=${TYPES%?} + +mvn -B -X -P ci-repo \ + deploy:deploy-file \ + -DgroupId=com.amazonaws \ + -DartifactId=aws-lambda-java-runtime-interface-client \ + -Dpackaging=jar \ + -Dversion=$snapshotProjectVersion \ + -Dfile=./target/aws-lambda-java-runtime-interface-client-$projectVersion.jar \ + -Dfiles=$FILES \ + -Dclassifiers=$CLASSIFIERS \ + -Dtypes=$TYPES \ + -DpomFile=pom.xml \ + -DrepositoryId=ci-repo -Durl=$MAVEN_REPO_URL \ + --settings ric-dev-environment/settings.xml + +if [ -f pom.xml.versionsBackup ]; then + mv pom.xml.versionsBackup pom.xml +fi diff --git a/aws-lambda-java-runtime-interface-client/ric-dev-environment/settings.xml b/aws-lambda-java-runtime-interface-client/ric-dev-environment/settings.xml new file mode 100644 index 000000000..d6f38929b --- /dev/null +++ b/aws-lambda-java-runtime-interface-client/ric-dev-environment/settings.xml @@ -0,0 +1,20 @@ + + + + dev-ci + + + ci-repo + ${env.MAVEN_REPO_URL} + + + + + + + ci-repo + ${env.MAVEN_REPO_USERNAME} + ${env.MAVEN_REPO_PASSWORD} + + + diff --git a/aws-lambda-java-runtime-interface-client/ric-dev-environment/test-platform-specific-jar-snapshot.sh b/aws-lambda-java-runtime-interface-client/ric-dev-environment/test-platform-specific-jar-snapshot.sh new file mode 100755 index 000000000..c9eced5cb --- /dev/null +++ b/aws-lambda-java-runtime-interface-client/ric-dev-environment/test-platform-specific-jar-snapshot.sh @@ -0,0 +1,27 @@ +#!/bin/bash + +set -e + +projectVersion=$(mvn help:evaluate -Dexpression=project.version -q -DforceStdout) + + +# test uber jar +mvn -B -X -P ci-repo \ + dependency:get \ + -DremoteRepositories=ci-repo::::$MAVEN_REPO_URL \ + -Dartifact=com.amazonaws:aws-lambda-java-runtime-interface-client:${projectVersion}-SNAPSHOT \ + -Dtransitive=false \ + --settings ric-dev-environment/settings.xml + + +PLATFORM_ARRAY=("linux-x86_64" "linux_musl-x86_64" "linux-aarch_64" "linux_musl-aarch_64") + +for classifier in "${PLATFORM_ARRAY[@]}"; do + # Test platform specific jar + mvn -B -P ci-repo \ + dependency:get \ + -DremoteRepositories=ci-repo::::$MAVEN_REPO_URL \ + -Dartifact=com.amazonaws:aws-lambda-java-runtime-interface-client:${projectVersion}-SNAPSHOT:jar:${classifier} \ + -Dtransitive=false \ + --settings ric-dev-environment/settings.xml +done \ No newline at end of file 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/.gitkeep b/aws-lambda-java-runtime-interface-client/src/.gitkeep deleted file mode 100644 index 74a6ce441..000000000 --- a/aws-lambda-java-runtime-interface-client/src/.gitkeep +++ /dev/null @@ -1 +0,0 @@ -Feel free to delete this file as soon as actual Java code is added to this directory. diff --git a/aws-lambda-java-runtime-interface-client/src/main/java/com/amazonaws/services/lambda/crac/CheckpointException.java b/aws-lambda-java-runtime-interface-client/src/main/java/com/amazonaws/services/lambda/crac/CheckpointException.java new file mode 100644 index 000000000..f802ad5f7 --- /dev/null +++ b/aws-lambda-java-runtime-interface-client/src/main/java/com/amazonaws/services/lambda/crac/CheckpointException.java @@ -0,0 +1,12 @@ +/* +Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +SPDX-License-Identifier: Apache-2.0 +*/ + +package com.amazonaws.services.lambda.crac; + +public class CheckpointException extends Exception { + private static final long serialVersionUID = -4956873658083157585L; + public CheckpointException() { + } +} diff --git a/aws-lambda-java-runtime-interface-client/src/main/java/com/amazonaws/services/lambda/crac/Context.java b/aws-lambda-java-runtime-interface-client/src/main/java/com/amazonaws/services/lambda/crac/Context.java new file mode 100644 index 000000000..d62ef0143 --- /dev/null +++ b/aws-lambda-java-runtime-interface-client/src/main/java/com/amazonaws/services/lambda/crac/Context.java @@ -0,0 +1,22 @@ +/* +Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +SPDX-License-Identifier: Apache-2.0 +*/ + +package com.amazonaws.services.lambda.crac; + +public abstract class Context implements Resource { + + protected Context() { + } + + @Override + public abstract void beforeCheckpoint(Context context) + throws CheckpointException; + + @Override + public abstract void afterRestore(Context context) + throws RestoreException; + + public abstract void register(R resource); +} diff --git a/aws-lambda-java-runtime-interface-client/src/main/java/com/amazonaws/services/lambda/crac/ContextImpl.java b/aws-lambda-java-runtime-interface-client/src/main/java/com/amazonaws/services/lambda/crac/ContextImpl.java new file mode 100644 index 000000000..04b1436a8 --- /dev/null +++ b/aws-lambda-java-runtime-interface-client/src/main/java/com/amazonaws/services/lambda/crac/ContextImpl.java @@ -0,0 +1,96 @@ +/* +Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +SPDX-License-Identifier: Apache-2.0 +*/ + +package com.amazonaws.services.lambda.crac; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import java.util.Map; +import java.util.WeakHashMap; +import java.util.stream.Collectors; + + +/** + * Spec reference: https://crac.github.io/openjdk-builds/javadoc/api/java.base/jdk/crac/package-summary.html + */ + +public class ContextImpl extends Context { + + private volatile long order = -1L; + private final WeakHashMap checkpointQueue = new WeakHashMap<>(); + + @Override + public synchronized void beforeCheckpoint(Context context) throws CheckpointException { + executeBeforeCheckpointHooks(); + DNSManager.clearCache(); + System.gc(); + } + + @Override + public synchronized void afterRestore(Context context) throws RestoreException { + + List exceptionsThrown = new ArrayList<>(); + for (Resource resource : getCheckpointQueueForwardOrderOfRegistration()) { + try { + resource.afterRestore(this); + } catch (RestoreException e) { + Collections.addAll(exceptionsThrown, e.getSuppressed()); + } catch (Exception e) { + exceptionsThrown.add(e); + } + } + + if (!exceptionsThrown.isEmpty()) { + RestoreException restoreException = new RestoreException(); + for (Throwable t : exceptionsThrown) { + restoreException.addSuppressed(t); + } + throw restoreException; + } + } + + @Override + public synchronized void register(Resource resource) { + checkpointQueue.put(resource, ++order); + } + + private List getCheckpointQueueReverseOrderOfRegistration() { + return checkpointQueue.entrySet(). + stream(). + sorted((r1, r2) -> (int) (r2.getValue() - r1.getValue())). + map(Map.Entry::getKey). + collect(Collectors.toList()); + } + + private List getCheckpointQueueForwardOrderOfRegistration() { + return checkpointQueue.entrySet(). + stream(). + sorted((r1, r2) -> (int) (r1.getValue() - r2.getValue())). + map(Map.Entry::getKey). + collect(Collectors.toList()); + } + + private void executeBeforeCheckpointHooks() throws CheckpointException { + List exceptionsThrown = new ArrayList<>(); + for (Resource resource : getCheckpointQueueReverseOrderOfRegistration()) { + try { + resource.beforeCheckpoint(this); + } catch (CheckpointException e) { + Collections.addAll(exceptionsThrown, e.getSuppressed()); + } catch (Exception e) { + exceptionsThrown.add(e); + } + } + + if (!exceptionsThrown.isEmpty()) { + CheckpointException checkpointException = new CheckpointException(); + for (Throwable t : exceptionsThrown) { + checkpointException.addSuppressed(t); + } + throw checkpointException; + } + } +} diff --git a/aws-lambda-java-runtime-interface-client/src/main/java/com/amazonaws/services/lambda/crac/Core.java b/aws-lambda-java-runtime-interface-client/src/main/java/com/amazonaws/services/lambda/crac/Core.java new file mode 100644 index 000000000..7e0b24a2d --- /dev/null +++ b/aws-lambda-java-runtime-interface-client/src/main/java/com/amazonaws/services/lambda/crac/Core.java @@ -0,0 +1,29 @@ +/* +Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +SPDX-License-Identifier: Apache-2.0 +*/ + +package com.amazonaws.services.lambda.crac; + +/** + * Provides the global context for registering resources. + */ +public final class Core { + + private static Context globalContext = new ContextImpl(); + + private Core() { + } + + public static Context getGlobalContext() { + return globalContext; + } + + public static void checkpointRestore() { + throw new UnsupportedOperationException(); + } + + static void resetGlobalContext() { + globalContext = new ContextImpl(); + } +} diff --git a/aws-lambda-java-runtime-interface-client/src/main/java/com/amazonaws/services/lambda/crac/DNSManager.java b/aws-lambda-java-runtime-interface-client/src/main/java/com/amazonaws/services/lambda/crac/DNSManager.java new file mode 100644 index 000000000..6c485ec80 --- /dev/null +++ b/aws-lambda-java-runtime-interface-client/src/main/java/com/amazonaws/services/lambda/crac/DNSManager.java @@ -0,0 +1,10 @@ +/* +Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +SPDX-License-Identifier: Apache-2.0 +*/ + +package com.amazonaws.services.lambda.crac; + +class DNSManager { + static native void clearCache(); +} diff --git a/aws-lambda-java-runtime-interface-client/src/main/java/com/amazonaws/services/lambda/crac/Resource.java b/aws-lambda-java-runtime-interface-client/src/main/java/com/amazonaws/services/lambda/crac/Resource.java new file mode 100644 index 000000000..7ef933202 --- /dev/null +++ b/aws-lambda-java-runtime-interface-client/src/main/java/com/amazonaws/services/lambda/crac/Resource.java @@ -0,0 +1,12 @@ +/* +Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +SPDX-License-Identifier: Apache-2.0 +*/ + +package com.amazonaws.services.lambda.crac; + +public interface Resource { + void afterRestore(Context context) throws Exception; + + void beforeCheckpoint(Context context) throws Exception; +} diff --git a/aws-lambda-java-runtime-interface-client/src/main/java/com/amazonaws/services/lambda/crac/RestoreException.java b/aws-lambda-java-runtime-interface-client/src/main/java/com/amazonaws/services/lambda/crac/RestoreException.java new file mode 100644 index 000000000..cef38e00f --- /dev/null +++ b/aws-lambda-java-runtime-interface-client/src/main/java/com/amazonaws/services/lambda/crac/RestoreException.java @@ -0,0 +1,13 @@ +/* +Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +SPDX-License-Identifier: Apache-2.0 +*/ + +package com.amazonaws.services.lambda.crac; + +public class RestoreException extends Exception { + private static final long serialVersionUID = -823900409868237860L; + + public RestoreException() { + } +} 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 45301c5d7..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 @@ -1,40 +1,46 @@ -// -// AWSLambda.java -// -// Copyright (c) 2013 Amazon. All rights reserved. -// +/* +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 com.amazonaws.services.lambda.crac.Core; import com.amazonaws.services.lambda.runtime.LambdaLogger; +import com.amazonaws.services.lambda.runtime.api.client.LambdaRequestHandler.UserFaultHandler; +import com.amazonaws.services.lambda.runtime.api.client.logging.FramedTelemetryLogSink; import com.amazonaws.services.lambda.runtime.api.client.logging.LambdaContextLogger; +import com.amazonaws.services.lambda.runtime.api.client.logging.LogSink; import com.amazonaws.services.lambda.runtime.api.client.logging.StdOutLogSink; -import com.amazonaws.services.lambda.runtime.api.client.util.EnvReader; -import com.amazonaws.services.lambda.runtime.api.client.util.EnvWriter; +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.serialization.PojoSerializer; -import com.amazonaws.services.lambda.runtime.serialization.factories.GsonFactory; -import com.amazonaws.services.lambda.runtime.serialization.factories.JacksonFactory; +import com.amazonaws.services.lambda.runtime.logging.LogFormat; +import com.amazonaws.services.lambda.runtime.logging.LogLevel; import com.amazonaws.services.lambda.runtime.serialization.util.ReflectUtil; -import com.amazonaws.services.lambda.runtime.api.client.LambdaRequestHandler.UserFaultHandler; -import com.amazonaws.services.lambda.runtime.api.client.logging.FramedTelemetryLogSink; -import com.amazonaws.services.lambda.runtime.api.client.logging.LogSink; -import com.amazonaws.services.lambda.runtime.api.client.runtimeapi.InvocationRequest; -import com.amazonaws.services.lambda.runtime.api.client.runtimeapi.LambdaRuntimeClient; - import java.io.ByteArrayOutputStream; import java.io.File; +import java.io.FileDescriptor; import java.io.FileInputStream; +import java.io.IOError; import java.io.IOException; -import java.io.OutputStream; import java.io.PrintStream; -import java.lang.reflect.Method; +import java.lang.reflect.Constructor; import java.net.URLClassLoader; -import java.nio.file.Paths; import java.security.Security; import java.util.Properties; - -import static com.amazonaws.services.lambda.runtime.api.client.UserFault.makeUserFault; +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,9 +55,8 @@ */ public class AWSLambda { - private static final Runnable doNothing = () -> { - }; - + 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"; @@ -62,6 +67,16 @@ public class AWSLambda { private static final String DEFAULT_NEGATIVE_CACHE_TTL = "1"; + // System property for Lambda tracing, see aws-xray-sdk-java/LambdaSegmentContext + // https://github.com/aws/aws-xray-sdk-java/blob/2f467e50db61abb2ed2bd630efc21bddeabd64d9/aws-xray-recorder-sdk-core/src/main/java/com/amazonaws/xray/contexts/LambdaSegmentContext.java#L39-L40 + private static final String LAMBDA_TRACE_HEADER_PROP = "com.amazonaws.xray.traceHeader"; + + private static final String INIT_TYPE_SNAP_START = "snap-start"; + + private static final String AWS_LAMBDA_INITIALIZATION_TYPE = System.getenv(ReservedRuntimeEnvironmentVariables.AWS_LAMBDA_INITIALIZATION_TYPE); + + 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. @@ -73,9 +88,9 @@ public class AWSLambda { // The ca-certificates package provides /etc/pki/java/cacerts which becomes the symlink destination // of $java_home/lib/security/cacerts when java is installed in the chroot. Given that java is provided // in /var/lang as opposed to installed in the chroot, this brings it closer. - if(System.getProperty(TRUST_STORE_PROPERTY) == null) { + if (System.getProperty(TRUST_STORE_PROPERTY) == null) { final File systemCacerts = new File("/etc/pki/java/cacerts"); - if(systemCacerts.exists() && systemCacerts.isFile()) { + if (systemCacerts.exists() && systemCacerts.isFile()) { System.setProperty(TRUST_STORE_PROPERTY, systemCacerts.getPath()); } } @@ -101,20 +116,17 @@ private static boolean isNegativeCacheOverridable() { } } - private static UserMethods findUserMethods(final String handlerString, ClassLoader customerClassLoader) { + private static LambdaRequestHandler findRequestHandler(final String handlerString, ClassLoader customerClassLoader) { final HandlerInfo handlerInfo; try { handlerInfo = HandlerInfo.fromString(handlerString, customerClassLoader); } catch (HandlerInfo.InvalidHandlerException e) { UserFault userFault = UserFault.makeUserFault("Invalid handler: `" + handlerString + "'"); - return new UserMethods( - doNothing, - new UserFaultHandler(userFault) - ); + return new UserFaultHandler(userFault); } catch (ClassNotFoundException e) { - return new UserMethods(doNothing, LambdaRequestHandler.classNotFound(e, HandlerInfo.className(handlerString))); + return LambdaRequestHandler.classNotFound(e, HandlerInfo.className(handlerString)); } catch (NoClassDefFoundError e) { - return new UserMethods(doNothing, LambdaRequestHandler.initErrorHandler(e, HandlerInfo.className(handlerString))); + return LambdaRequestHandler.initErrorHandler(e, HandlerInfo.className(handlerString)); } catch (Throwable t) { throw UserFault.makeInitErrorUserFault(t, HandlerInfo.className(handlerString)); } @@ -122,34 +134,51 @@ private static UserMethods findUserMethods(final String handlerString, ClassLoad final LambdaRequestHandler requestHandler = EventHandlerLoader.loadEventHandler(handlerInfo); // if loading the handler failed and the failure is fatal (for e.g. the constructor threw an exception) // we want to report this as an init error rather than deferring to the first invoke. - if(requestHandler instanceof UserFaultHandler) { - UserFault userFault =((UserFaultHandler) requestHandler).fault; - if(userFault.fatal) { + if (requestHandler instanceof UserFaultHandler) { + UserFault userFault = ((UserFaultHandler) requestHandler).fault; + if (userFault.fatal) { throw userFault; } } + return requestHandler; + } + + private static LambdaRequestHandler getLambdaRequestHandlerObject(String handler, LambdaContextLogger lambdaLogger, LambdaRuntimeApiClient runtimeClient) throws ClassNotFoundException, IOException { + UnsafeUtil.disableIllegalAccessWarning(); - Runnable initHandler = doNothing; + System.setOut(new PrintStream(new LambdaOutputStream(System.out), false, "UTF-8")); + System.setErr(new PrintStream(new LambdaOutputStream(System.err), false, "UTF-8")); + setupRuntimeLogger(lambdaLogger); + + 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 + // are loaded from the system classloader. + customerClassLoader = new CustomerClassLoader(taskRoot, libRoot, ClassLoader.getSystemClassLoader()); + Thread.currentThread().setContextClassLoader(customerClassLoader); + + // Load the user's handler + LambdaRequestHandler requestHandler = null; try { - initHandler = wrapInitCall(handlerInfo.clazz.getMethod("init")); - } catch (NoSuchMethodException | NoClassDefFoundError e) { + requestHandler = findRequestHandler(handler, customerClassLoader); + } catch (UserFault userFault) { + lambdaLogger.log(userFault.reportableError(), lambdaLogger.getLogFormat() == LogFormat.JSON ? LogLevel.ERROR : LogLevel.UNDEFINED); + LambdaError error = new LambdaError( + LambdaErrorConverter.fromUserFault(userFault), + RapidErrorType.BadFunctionCode); + runtimeClient.reportInitError(error); + System.exit(1); } - return new UserMethods(initHandler, requestHandler); - } + if (INIT_TYPE_SNAP_START.equals(AWS_LAMBDA_INITIALIZATION_TYPE)) { + onInitComplete(lambdaLogger, runtimeClient); + } - private static Runnable wrapInitCall(final Method method) { - return () -> { - try { - method.invoke(null); - } catch (Throwable t) { - throw UserFault.makeUserFault(t); - } - }; + return requestHandler; } - public static void setupRuntimeLogger(LambdaLogger lambdaLogger) - throws ClassNotFoundException, IllegalAccessException, NoSuchFieldException { + private static void setupRuntimeLogger(LambdaLogger lambdaLogger) + throws ClassNotFoundException { ReflectUtil.setStaticField( Class.forName("com.amazonaws.services.lambda.runtime.LambdaRuntime"), "logger", @@ -158,152 +187,190 @@ public static void setupRuntimeLogger(LambdaLogger lambdaLogger) ); } - public static String getEnvOrExit(String envVariableName) { - String value = System.getenv(envVariableName); - if(value == null) { - System.err.println("Could not get environment variable " + envVariableName); - System.exit(-1); + /** + * convert an integer into a FileDescriptor object using reflection to access private members. + */ + private static FileDescriptor intToFd(int fd) throws RuntimeException { + try { + Class clazz = FileDescriptor.class; + Constructor c = clazz.getDeclaredConstructor(Integer.TYPE); + c.setAccessible(true); + return c.newInstance(fd); + } catch (Exception e) { + throw new RuntimeException(e); } - return value; } - protected static URLClassLoader customerClassLoader; - private static LogSink createLogSink() { - final String fd = System.getenv("_LAMBDA_TELEMETRY_LOG_FD"); - if(fd == null) { + final String fdStr = System.getenv("_LAMBDA_TELEMETRY_LOG_FD"); + if (fdStr == null) { return new StdOutLogSink(); } try { - File pipeFdFile = Paths.get("/proc", "self", "fd", fd).toFile(); - return new FramedTelemetryLogSink(pipeFdFile); - } catch (IOException e) { + int fdInt = Integer.parseInt(fdStr); + FileDescriptor fd = intToFd(fdInt); + return new FramedTelemetryLogSink(fd); + } catch (Exception e) { return new StdOutLogSink(); } } - public static void main(String[] args) { - // TODO validate arguments, show usage - startRuntime(args[0]); - } - - private static void startRuntime(String handler) { - try (LogSink logSink = createLogSink()) { - startRuntime(handler, new LambdaContextLogger(logSink)); - } catch (Throwable t) { + public static void main(String[] args) throws Throwable { + 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); } } - private static void startRuntime(String handler, LambdaLogger lambdaLogger) throws Throwable { - UnsafeUtil.disableIllegalAccessWarning(); + private static LambdaContextLogger initLogger() { + LogSink logSink = createLogSink(); + LambdaContextLogger logger = new LambdaContextLogger( + logSink, + LogLevel.fromString(LambdaEnvironment.LAMBDA_LOG_LEVEL), + LogFormat.fromString(LambdaEnvironment.LAMBDA_LOG_FORMAT)); - System.setOut(new PrintStream(new LambdaOutputStream(System.out), false, "UTF-8")); - System.setErr(new PrintStream(new LambdaOutputStream(System.err), false, "UTF-8")); - setupRuntimeLogger(lambdaLogger); + return logger; + } - String runtimeApi = getEnvOrExit(ReservedRuntimeEnvironmentVariables.AWS_LAMBDA_RUNTIME_API); - LambdaRuntimeClient runtimeClient = new LambdaRuntimeClient(runtimeApi); + 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); + } + }); + } - EnvReader envReader = new EnvReader(); - try (EnvWriter envWriter = new EnvWriter(envReader)) { - envWriter.unsetLambdaInternalEnv(); - envWriter.setupEnvironmentCredentials(); - envWriter.setupAwsExecutionEnv(); + 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 { + for (int i = 0; i < concurrencyConfig.getNumberOfPlatformThreads(); i++) { + startRuntimeLoopWithExecutor(lambdaRequestHandler, lambdaLogger, platformThreadExecutor, runtimeClient); + } + } finally { + platformThreadExecutor.shutdown(); + try { + platformThreadExecutor.awaitTermination(Long.MAX_VALUE, TimeUnit.DAYS); + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + } + } + } else { + startRuntimeLoop(lambdaRequestHandler, lambdaLogger, runtimeClient, true); } + } - 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 - // are loaded from the system classloader. - customerClassLoader = new CustomerClassLoader(taskRoot, libRoot, ClassLoader.getSystemClassLoader()); - Thread.currentThread().setContextClassLoader(customerClassLoader); - - // Load the user's handler - UserMethods methods; - try { - methods = findUserMethods(handler, customerClassLoader); - } catch (UserFault userFault) { - lambdaLogger.log(userFault.reportableError()); - ByteArrayOutputStream payload = new ByteArrayOutputStream(1024); - Failure failure = new Failure(userFault); - GsonFactory.getInstance().getSerializer(Failure.class).toJson(failure, payload); - runtimeClient.postInitError(payload.toByteArray(), failure.getErrorType()); - System.exit(1); - return; + 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); } + } - // Call the user's init handler(a function called 'init'), if provided in the same module as the request handler. - // This is an undocumented feature, and still exists to keep backward compatibility. Continue if this call fails. - try { - methods.initHandler.run(); - } catch (UserFault f) { - lambdaLogger.log(f.reportableError()); + 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); } + } - try (EnvWriter envWriter = new EnvWriter(envReader)) { - boolean shouldExit = false; - while (!shouldExit) { + 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 = runtimeClient.waitForNextInvocation(); - if (request.getXrayTraceId() != null) { - envWriter.modifyEnv(m -> m.put("_X_AMZN_TRACE_ID", request.getXrayTraceId())); + InvocationRequest request = exitLoopOnErrors ? runtimeClient.nextInvocation() : runtimeClient.nextInvocationWithExponentialBackoff(lambdaLogger); + if (exitLoopOnErrors) { + setEnvVarForXrayTraceId(request); } else { - envWriter.modifyEnv(m -> m.remove("_X_AMZN_TRACE_ID")); + SdkInternalThreadLocal.put(CONCURRENT_TRACE_ID_KEY, request.getXrayTraceId()); } - ByteArrayOutputStream payload; try { - payload = methods.requestHandler.call(request); - // TODO calling payload.toByteArray() creates a new copy of the underlying buffer - runtimeClient.postInvocationResponse(request.getId(), payload.toByteArray()); - } catch (UserFault f) { - userFault = f; - UserFault.filterStackTrace(f); - payload = new ByteArrayOutputStream(1024); - Failure failure = new Failure(f); - GsonFactory.getInstance().getSerializer(Failure.class).toJson(failure, payload); - shouldExit = f.fatal; - runtimeClient.postInvocationError(request.getId(), payload.toByteArray(), failure.getErrorType()); + 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); - payload = new ByteArrayOutputStream(1024); - Failure failure = new Failure(t); - GsonFactory.getInstance().getSerializer(Failure.class).toJson(failure, payload); - // These two categories of errors are considered fatal. - shouldExit = Failure.isInvokeFailureFatal(t); - runtimeClient.postInvocationError(request.getId(), payload.toByteArray(), failure.getErrorType(), - serializeAsXRayJson(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.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 PojoSerializer xRayErrorCauseSerializer; + private static void onInitComplete(final LambdaContextLogger lambdaLogger, LambdaRuntimeApiClient runtimeClient) throws IOException { + try { + Core.getGlobalContext().beforeCheckpoint(null); + runtimeClient.restoreNext(); + } catch (Exception e1) { + logExceptionCloudWatch(lambdaLogger, e1); + runtimeClient.reportInitError(new LambdaError( + LambdaErrorConverter.fromThrowable(e1), + RapidErrorType.BeforeCheckpointError)); + System.exit(64); + } - /** - * - * @param throwable throwable to convert - * @return json as string expected by XRay's web console. On conversion failure, returns null. - */ - private static String serializeAsXRayJson(Throwable throwable) { try { - final OutputStream outputStream = new ByteArrayOutputStream(); - final XRayErrorCause cause = new XRayErrorCause(throwable); - if(xRayErrorCauseSerializer == null) { - xRayErrorCauseSerializer = JacksonFactory.getInstance().getSerializer(XRayErrorCause.class); - } - xRayErrorCauseSerializer.toJson(cause, outputStream); - return outputStream.toString(); - } catch (Exception e) { - return null; + Core.getGlobalContext().afterRestore(null); + } catch (Exception restoreExc) { + logExceptionCloudWatch(lambdaLogger, restoreExc); + runtimeClient.reportRestoreError(new LambdaError( + LambdaErrorConverter.fromThrowable(restoreExc), + RapidErrorType.AfterRestoreError)); + System.exit(64); } } + private static void logExceptionCloudWatch(LambdaContextLogger lambdaLogger, Exception exc) { + UserFault.filterStackTrace(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/ClasspathLoader.java b/aws-lambda-java-runtime-interface-client/src/main/java/com/amazonaws/services/lambda/runtime/api/client/ClasspathLoader.java index 69845cbee..4204f3010 100644 --- a/aws-lambda-java-runtime-interface-client/src/main/java/com/amazonaws/services/lambda/runtime/api/client/ClasspathLoader.java +++ b/aws-lambda-java-runtime-interface-client/src/main/java/com/amazonaws/services/lambda/runtime/api/client/ClasspathLoader.java @@ -1,4 +1,7 @@ -/* Copyright 2019 Amazon.com, Inc. or its affiliates. All Rights Reserved. */ +/* +Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +SPDX-License-Identifier: Apache-2.0 +*/ package com.amazonaws.services.lambda.runtime.api.client; @@ -13,19 +16,19 @@ /** * This class loads all of the classes that are in jars on the classpath. - * + *

* It is used to generate a class list and Application CDS archive that includes all the possible classes that could be * loaded by the runtime. This simplifies the process of generating the Application CDS archive. */ public class ClasspathLoader { - private static final Set BLACKLIST = new HashSet<>(); + private static final Set BLOCKLIST = new HashSet<>(); private static final ClassLoader SYSTEM_CLASS_LOADER = ClassLoader.getSystemClassLoader(); private static final int CLASS_SUFFIX_LEN = ".class".length(); static { - // NativeClient loads a native library and crashes if loaded here so just exclude it - BLACKLIST.add("lambdainternal.runtimeapi.NativeClient"); + // Ignore module info class for serialization lib + BLOCKLIST.add("META-INF.versions.9.module-info"); } private static String pathToClassName(final String path) { @@ -35,8 +38,9 @@ private static String pathToClassName(final String path) { private static void loadClass(String name) { try { Class.forName(name, true, SYSTEM_CLASS_LOADER); + System.out.println("Loaded " + name); } catch (ClassNotFoundException e) { - System.err.println("[WARN] Failed to load " + name + ": " + e.getMessage()); + System.err.println("[WARN] Failed to load " + name + ": " + e.getMessage()); } } @@ -46,13 +50,13 @@ private static void loadClassesInJar(File file) throws IOException { while (en.hasMoreElements()) { JarEntry entry = en.nextElement(); - if(!entry.getName().endsWith(".class")) { + if (!entry.getName().endsWith(".class")) { continue; } String name = pathToClassName(entry.getName()); - if(BLACKLIST.contains(name)) { + if (BLOCKLIST.contains(name)) { continue; } @@ -63,11 +67,11 @@ private static void loadClassesInJar(File file) throws IOException { private static void loadClassesInClasspathEntry(String entry) throws IOException { File file = new File(entry); - if(!file.exists()) { + if (!file.exists()) { throw new FileNotFoundException("Classpath entry does not exist: " + file.getPath()); } - if(file.isDirectory() || !file.getPath().endsWith(".jar")) { + if (file.isDirectory() || !file.getPath().endsWith(".jar")) { System.err.println("[WARN] Only jar classpath entries are supported. Skipping " + file.getPath()); return; } @@ -77,10 +81,10 @@ private static void loadClassesInClasspathEntry(String entry) throws IOException private static void loadAllClasses() throws IOException { final String classPath = System.getProperty("java.class.path"); - if(classPath == null) { + if (classPath == null) { return; } - for(String classPathEntry : classPath.split(File.pathSeparator)) { + for (String classPathEntry : classPath.split(File.pathSeparator)) { loadClassesInClasspathEntry(classPathEntry); } } diff --git a/aws-lambda-java-runtime-interface-client/src/main/java/com/amazonaws/services/lambda/runtime/api/client/CustomerClassLoader.java b/aws-lambda-java-runtime-interface-client/src/main/java/com/amazonaws/services/lambda/runtime/api/client/CustomerClassLoader.java index 3932547de..b8aabbf37 100644 --- a/aws-lambda-java-runtime-interface-client/src/main/java/com/amazonaws/services/lambda/runtime/api/client/CustomerClassLoader.java +++ b/aws-lambda-java-runtime-interface-client/src/main/java/com/amazonaws/services/lambda/runtime/api/client/CustomerClassLoader.java @@ -1,8 +1,12 @@ -/* Copyright 2019 Amazon.com, Inc. or its affiliates. All Rights Reserved. */ +/* +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 java.io.File; +import java.io.FilenameFilter; import java.io.IOException; import java.net.MalformedURLException; import java.net.URL; @@ -18,6 +22,19 @@ class CustomerClassLoader extends URLClassLoader { * does not depend on the underlying filesystem. */ private final static Comparator LEXICAL_SORT_ORDER = Comparator.comparing(String::toString); + private final static FilenameFilter JAR_FILE_NAME_FILTER = new FilenameFilter() { + + @Override + public boolean accept(File dir, String name) { + int offset = name.length() - 4; + // must be at least A.jar + if (offset <= 0) { + return false; + } else { + return name.startsWith(".jar", offset); + } + } + }; CustomerClassLoader(String taskRoot, String optRoot, ClassLoader parent) throws IOException { super(getUrls(taskRoot, optRoot), parent); @@ -36,15 +53,14 @@ private static void appendJars(File dir, List result) throws MalformedURLEx if (!dir.isDirectory()) { return; } - String[] names = dir.list(); + String[] names = dir.list(CustomerClassLoader.JAR_FILE_NAME_FILTER); if (names == null) { return; } Arrays.sort(names, CustomerClassLoader.LEXICAL_SORT_ORDER); - for(String path : names) { - if(path.endsWith(".jar")) { - result.add(newURL(dir, path)); - } + + for (String path : names) { + result.add(newURL(dir, path)); } } 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 fc16d43f3..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 @@ -1,16 +1,22 @@ -/* Copyright 2019 Amazon.com, Inc. or its affiliates. All Rights Reserved. */ +/* +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 com.amazonaws.services.lambda.runtime.ClientContext; import com.amazonaws.services.lambda.runtime.Context; +import com.amazonaws.services.lambda.runtime.LambdaLogger; +import com.amazonaws.services.lambda.runtime.LambdaRuntimeInternal; import com.amazonaws.services.lambda.runtime.RequestHandler; import com.amazonaws.services.lambda.runtime.RequestStreamHandler; -import com.amazonaws.services.lambda.runtime.LambdaRuntimeInternal; - +import com.amazonaws.services.lambda.runtime.api.client.LambdaRequestHandler.UserFaultHandler; import com.amazonaws.services.lambda.runtime.api.client.api.LambdaClientContext; import com.amazonaws.services.lambda.runtime.api.client.api.LambdaCognitoIdentity; import com.amazonaws.services.lambda.runtime.api.client.api.LambdaContext; +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.api.client.util.UnsafeUtil; import com.amazonaws.services.lambda.runtime.serialization.PojoSerializer; import com.amazonaws.services.lambda.runtime.serialization.events.LambdaEventSerializers; @@ -18,15 +24,11 @@ import com.amazonaws.services.lambda.runtime.serialization.factories.JacksonFactory; import com.amazonaws.services.lambda.runtime.serialization.util.Functions; import com.amazonaws.services.lambda.runtime.serialization.util.ReflectUtil; -import com.amazonaws.services.lambda.runtime.api.client.LambdaRequestHandler.UserFaultHandler; -import com.amazonaws.services.lambda.runtime.api.client.runtimeapi.InvocationRequest; - +import java.io.ByteArrayInputStream; import java.io.ByteArrayOutputStream; import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; -import java.io.PrintWriter; -import java.io.StringWriter; import java.lang.reflect.Constructor; import java.lang.reflect.InvocationTargetException; import java.lang.reflect.Method; @@ -40,8 +42,8 @@ import java.util.HashMap; import java.util.LinkedList; import java.util.Map; +import java.util.Objects; import java.util.Optional; - import static com.amazonaws.services.lambda.runtime.api.client.UserFault.filterStackTrace; import static com.amazonaws.services.lambda.runtime.api.client.UserFault.makeUserFault; import static com.amazonaws.services.lambda.runtime.api.client.UserFault.trace; @@ -55,41 +57,81 @@ private enum Platform { UNKNOWN } - private static final EnumMap>> typeCache = new EnumMap<>(Platform.class); + private static volatile ThreadLocal> contextSerializer = new ThreadLocal<>(); + private static volatile ThreadLocal> cognitoSerializer = new ThreadLocal<>(); + + 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) { + + //1. Non bridge methods are preferred over bridge methods. + if (!lhs.isBridge() && rhs.isBridge()) { + return -1; + } else if (!rhs.isBridge() && lhs.isBridge()) { + return 1; + } + + //2. We prefer longer signatures to shorter signatures. Except we count a method whose last argument is + //Context as having 1 more argument than it really does. This is a stupid thing to do, but we + //need to keep it for back compat reasons. + Class[] lParams = lhs.getParameterTypes(); + Class[] rParams = rhs.getParameterTypes(); + + int lParamCompareLength = lParams.length; + int rParamCompareLength = rParams.length; + + if (lastParameterIsContext(lParams)) { + ++lParamCompareLength; + } + + if (lastParameterIsContext(rParams)) { + ++rParamCompareLength; + } + + return -Integer.compare(lParamCompareLength, rParamCompareLength); + } + }; - private EventHandlerLoader() { } + private EventHandlerLoader() { + } /** * returns the appropriate serializer for the class based on platform and whether the class is a supported event + * * @param platform enum platform - * @param type Type of object used + * @param type Type of object used * @return PojoSerializer * @see Platform for which platforms are used * @see LambdaEventSerializers for how mixins and modules are added to the serializer */ @SuppressWarnings({"unchecked", "rawtypes"}) private static PojoSerializer getSerializer(Platform platform, Type type) { + PojoSerializer customSerializer = PojoSerializerLoader.getCustomerSerializer(type); + if (customSerializer != null) { + return customSerializer; + } + // if serializing a Class that is a Lambda supported event, use Jackson with customizations if (type instanceof Class) { - Class clazz = ((Class)type); + 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) - switch (platform) { - case ANDROID: - return GsonFactory.getInstance().getSerializer(type); - default: - return JacksonFactory.getInstance().getSerializer(type); + if (Objects.requireNonNull(platform) == Platform.ANDROID) { + return GsonFactory.getInstance().getSerializer(type); } + return JacksonFactory.getInstance().getSerializer(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); @@ -101,21 +143,18 @@ private static PojoSerializer getSerializerCached(Platform platform, Typ return serializer; } - private static volatile PojoSerializer contextSerializer; - private static volatile PojoSerializer cognitoSerializer; - 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(); } @@ -145,445 +184,134 @@ private static Platform getPlatform(Context context) { } private static boolean isVoid(Type type) { - return Void.TYPE.equals(type) || (type instanceof Class) && Void.class.isAssignableFrom((Class)type); + return Void.TYPE.equals(type) || (type instanceof Class) && Void.class.isAssignableFrom((Class) type); } - /** - * Wraps a RequestHandler as a lower level stream handler using supplied types. - * Optional types mean that the input and/or output should be ignored respectiveley - */ - @SuppressWarnings("rawtypes") - private static final class PojoHandlerAsStreamHandler implements RequestStreamHandler { - - public RequestHandler innerHandler; - public final Optional inputType; - public final Optional outputType; - - public PojoHandlerAsStreamHandler( - RequestHandler innerHandler, - Optional inputType, - Optional outputType - ) { - this.innerHandler = innerHandler; - this.inputType = inputType; - this.outputType = outputType; - - - if (inputType.isPresent()) { - getSerializerCached(Platform.UNKNOWN, inputType.get()); + private static Constructor getConstructor(Class clazz) throws Exception { + final Constructor constructor; + try { + constructor = clazz.getConstructor(); + } catch (NoSuchMethodException e) { + if (clazz.getEnclosingClass() != null && !Modifier.isStatic(clazz.getModifiers())) { + throw new Exception("Class " + + clazz.getName() + + " cannot be instantiated because it is a non-static inner class"); + } else { + throw new Exception("Class " + clazz.getName() + " has no public zero-argument constructor", e); } + } + return constructor; + } - if (outputType.isPresent()) { - getSerializerCached(Platform.UNKNOWN, outputType.get()); - } + private static T newInstance(Constructor constructor) { + try { + return constructor.newInstance(); + } catch (UserFault e) { + throw e; + } catch (InvocationTargetException e) { + throw makeUserFault(e.getCause() == null ? e : e.getCause(), true); + } catch (InstantiationException e) { + throw UnsafeUtil.throwException(e.getCause() == null ? e : e.getCause()); + } catch (IllegalAccessException e) { + throw UnsafeUtil.throwException(e); } + } - @SuppressWarnings("unchecked") - @Override - public void handleRequest(InputStream inputStream, OutputStream outputStream, Context context) - throws IOException { - final Object input; - final Platform platform = getPlatform(context); - try { - if (inputType.isPresent()) { - input = getSerializerCached(platform, inputType.get()).fromJson(inputStream); + /** + * perform a breadth-first search for the first parameterized type for iface + * + * @return null of no type found. Otherwise the type found. + */ + private static Type[] findInterfaceParameters(Class clazz, Class iface) { + LinkedList clazzes = new LinkedList<>(); + clazzes.addFirst(new ClassContext(clazz, (Type[]) null)); + while (!clazzes.isEmpty()) { + final ClassContext curContext = clazzes.removeLast(); + Type[] interfaces = curContext.clazz.getGenericInterfaces(); + + for (Type type : interfaces) { + if (type instanceof ParameterizedType) { + ParameterizedType candidate = (ParameterizedType) type; + Type rawType = candidate.getRawType(); + if (!(rawType instanceof Class)) { + //should be impossible + System.err.println("raw type is not a class: " + rawType); + continue; + } + Class rawClass = (Class) rawType; + if (iface.isAssignableFrom(rawClass)) { + return new ClassContext(candidate, curContext).actualTypeArguments; + } else { + clazzes.addFirst(new ClassContext(candidate, curContext)); + } + } else if (type instanceof Class) { + clazzes.addFirst(new ClassContext((Class) type, curContext)); } else { - input = null; + //should never happen? + System.err.println("Unexpected type class " + type.getClass().getName()); } - } catch (Throwable t) { - throw new RuntimeException("An error occurred during JSON parsing", filterStackTrace(t)); - } - - final Object output; - try { - output = innerHandler.handleRequest(input, context); - } catch (Throwable t) { - throw UnsafeUtil.throwException(filterStackTrace(t)); } - try { - if (outputType.isPresent()) { - PojoSerializer serializer = getSerializerCached(platform, outputType.get()); - serializer.toJson(output, outputStream); - } else { - outputStream.write(_JsonNull); - } - } catch (Throwable t) { - throw new RuntimeException("An error occurred during JSON serialization of response", t); + final Type superClass = curContext.clazz.getGenericSuperclass(); + if (superClass instanceof ParameterizedType) { + clazzes.addFirst(new ClassContext((ParameterizedType) superClass, curContext)); + } else if (superClass != null) { + clazzes.addFirst(new ClassContext((Class) superClass, curContext)); } } + return null; } - /** - * Wraps a java.lang.reflect.Method as a POJO RequestHandler - */ - private static final class PojoMethodRequestHandler implements RequestHandler { - public final Method m; - public final Type pType; - public final Object instance; - public final boolean needsContext; - public final int argSize; - - public PojoMethodRequestHandler(Method m, Type pType, Type rType, Object instance, boolean needsContext) { - this.m = m; - this.pType = pType; - this.instance = instance; - this.needsContext = needsContext; - this.argSize = (needsContext ? 1 : 0) + (pType != null ? 1 : 0); - } - - public static PojoMethodRequestHandler fromMethod( - Class clazz, - Method m, - Type pType, - Type rType, - boolean needsContext - ) throws Exception { - final Object instance; - if (Modifier.isStatic(m.getModifiers())) { - instance = null; - } else { - instance = newInstance(getConstructor(clazz)); - } - return new PojoMethodRequestHandler(m, pType, rType, instance, needsContext); + @SuppressWarnings({"rawtypes"}) + private static LambdaRequestHandler wrapRequestHandlerClass(final Class clazz) { + Type[] ptypes = findInterfaceParameters(clazz, RequestHandler.class); + if (ptypes == null) { + return new UserFaultHandler(makeUserFault("Class " + + clazz.getName() + + " does not implement RequestHandler with concrete type parameters")); } - - public static LambdaRequestHandler makeRequestHandler( - Class clazz, - Method m, - Type pType, - Type rType, - boolean needsContext - ) { - try { - return wrapPojoHandler(fromMethod(clazz, m, pType, rType, needsContext), pType, rType); - } catch (UserFault f) { - return new UserFaultHandler(f); - } catch (Throwable t) { - return new UserFaultHandler(makeUserFault(t)); - } + if (ptypes.length != 2) { + return new UserFaultHandler(makeUserFault( + "Invalid class signature for RequestHandler. Expected two generic types, got " + ptypes.length)); } - @Override - public Object handleRequest(Object input, Context context) { - final Object[] args = new Object[argSize]; - int idx = 0; + for (Type t : ptypes) { + if (t instanceof TypeVariable) { + Type[] bounds = ((TypeVariable) t).getBounds(); + boolean foundBound = false; + if (bounds != null) { + for (Type bound : bounds) { + if (!Object.class.equals(bound)) { + foundBound = true; + break; + } + } + } - if (pType != null) { - args[idx++] = input; + if (!foundBound) { + return new UserFaultHandler(makeUserFault("Class " + clazz.getName() + + " does not implement RequestHandler with concrete type parameters: parameter " + + t + " has no upper bound.")); + } } + } - if (this.needsContext) { - args[idx++] = context; - } + final Type pType = ptypes[0]; + final Type rType = ptypes[1]; - try { - return m.invoke(this.instance, args); - } catch (InvocationTargetException e) { - if (e.getCause() != null) { - throw UnsafeUtil.throwException(filterStackTrace(e.getCause())); - } else { - throw UnsafeUtil.throwException(filterStackTrace(e)); - } - } catch (Throwable t) { - throw UnsafeUtil.throwException(filterStackTrace(t)); - } + final Constructor constructor; + try { + constructor = getConstructor(clazz); + return wrapPojoHandler(newInstance(constructor), pType, rType); + } catch (UserFault f) { + return new UserFaultHandler(f); + } catch (Throwable e) { + return new UserFaultHandler(makeUserFault(e)); } } - /** - * Wraps a java.lang.reflect.Method object as a RequestStreamHandler - */ - private static final class StreamMethodRequestHandler implements RequestStreamHandler { - public final Method m; - public final Object instance; - public final boolean needsInput; - public final boolean needsOutput; - public final boolean needsContext; - public final int argSize; - - public StreamMethodRequestHandler( - Method m, - Object instance, - boolean needsInput, - boolean needsOutput, - boolean needsContext - ) { - this.m = m; - this.instance = instance; - this.needsInput = needsInput; - this.needsOutput = needsOutput; - this.needsContext = needsContext; - this.argSize = (needsInput ? 1 : 0) + (needsOutput ? 1 : 0) + (needsContext ? 1 : 0); - } - - public static StreamMethodRequestHandler fromMethod( - Class clazz, - Method m, - boolean needsInput, - boolean needsOutput, - boolean needsContext - ) throws Exception { - if (!isVoid(m.getReturnType())) { - System.err.println("Will ignore return type " + m.getReturnType() + " on byte stream handler"); - } - final Object instance = Modifier.isStatic(m.getModifiers()) - ? null - : newInstance(getConstructor(clazz)); - - return new StreamMethodRequestHandler(m, instance, needsInput, needsOutput, needsContext); - } - - public static LambdaRequestHandler makeRequestHandler( - Class clazz, - Method m, - boolean needsInput, - boolean needsOutput, - boolean needsContext - ) { - try { - return wrapRequestStreamHandler(fromMethod(clazz, m, needsInput, needsOutput, needsContext)); - } catch (UserFault f) { - return new UserFaultHandler(f); - } catch (Throwable t) { - return new UserFaultHandler(makeUserFault(t)); - } - } - - @Override - public void handleRequest(InputStream inputStream, OutputStream outputStream, Context context) - throws IOException { - final Object[] args = new Object[argSize]; - int idx = 0; - - if (needsInput) { - args[idx++] = inputStream; - } else { - inputStream.close(); - } - - if (needsOutput) { - args[idx++] = outputStream; - } - - if (needsContext) { - args[idx++] = context; - } - - try { - m.invoke(this.instance, args); - if (!needsOutput) { - outputStream.write(_JsonNull); - } - } catch (InvocationTargetException e) { - if (e.getCause() != null) { - throw UnsafeUtil.throwException(filterStackTrace(e.getCause())); - } else { - throw UnsafeUtil.throwException(filterStackTrace(e)); - } - } catch (Throwable t) { - throw UnsafeUtil.throwException(filterStackTrace(t)); - } - } - } - - public static Constructor getConstructor(Class clazz) throws Exception { - final Constructor constructor; - try { - constructor = clazz.getConstructor(); - } catch (NoSuchMethodException e) { - if (clazz.getEnclosingClass() != null && !Modifier.isStatic(clazz.getModifiers())) { - throw new Exception("Class " - + clazz.getName() - + " cannot be instantiated because it is a non-static inner class"); - } else { - throw new Exception("Class " + clazz.getName() + " has no public zero-argument constructor", e); - } - } - return constructor; - } - - public static T newInstance(Constructor constructor) { - try { - return constructor.newInstance(); - } catch (UserFault e) { - throw e; - } catch (InvocationTargetException e) { - throw makeUserFault(e.getCause() == null ? e : e.getCause(), true); - } catch (InstantiationException e) { - throw UnsafeUtil.throwException(e.getCause() == null ? e : e.getCause()); - } catch (IllegalAccessException e) { - throw UnsafeUtil.throwException(e); - } - } - - private static final class ClassContext { - public final Class clazz; - public final Type[] actualTypeArguments; - - @SuppressWarnings({"rawtypes"}) - private TypeVariable[] typeParameters; - - public ClassContext(Class clazz, Type[] actualTypeArguments) { - this.clazz = clazz; - this.actualTypeArguments = actualTypeArguments; - } - - @SuppressWarnings({"rawtypes"}) - public ClassContext(Class clazz, ClassContext curContext) { - this.typeParameters = clazz.getTypeParameters(); - if (typeParameters.length == 0 || curContext.actualTypeArguments == null) { - this.clazz = clazz; - this.actualTypeArguments = null; - } else { - Type[] types = new Type[typeParameters.length]; - for (int i = 0; i < types.length; i++) { - types[i] = curContext.resolveTypeVariable(typeParameters[i]); - } - - this.clazz = clazz; - this.actualTypeArguments = types; - } - } - - @SuppressWarnings({"rawtypes"}) - public ClassContext(ParameterizedType type, ClassContext curContext) { - Type[] types = type.getActualTypeArguments(); - for (int i = 0; i < types.length; i++) { - Type t = types[i]; - if (t instanceof TypeVariable) { - types[i] = curContext.resolveTypeVariable((TypeVariable)t); - } - } - - Type t = type.getRawType(); - if (t instanceof Class) { - this.clazz = (Class)t; - } else if (t instanceof TypeVariable) { - this.clazz = (Class)((TypeVariable)t).getGenericDeclaration(); - } else { - throw new RuntimeException("Type " + t + " is of unexpected type " + t.getClass()); - } - this.actualTypeArguments = types; - } - - @SuppressWarnings({"rawtypes"}) - public Type resolveTypeVariable(TypeVariable t) { - TypeVariable[] variables = getTypeParameters(); - for (int i = 0; i < variables.length; i++) { - if (t.getName().equals(variables[i].getName())) { - return actualTypeArguments == null ? variables[i] : actualTypeArguments[i]; - } - } - - return t; - } - - @SuppressWarnings({"rawtypes"}) - private TypeVariable[] getTypeParameters() { - if (typeParameters == null) { - typeParameters = clazz.getTypeParameters(); - } - return typeParameters; - } - } - - /** - * perform a breadth-first search for the first parameterized type for iface - * - * @return null of no type found. Otherwise the type found. - */ - public static Type[] findInterfaceParameters(Class clazz, Class iface) { - LinkedList clazzes = new LinkedList<>(); - clazzes.addFirst(new ClassContext(clazz, (Type[])null)); - while (!clazzes.isEmpty()) { - final ClassContext curContext = clazzes.removeLast(); - Type[] interfaces = curContext.clazz.getGenericInterfaces(); - - for (Type type : interfaces) { - if (type instanceof ParameterizedType) { - ParameterizedType candidate = (ParameterizedType)type; - Type rawType = candidate.getRawType(); - if (!(rawType instanceof Class)) { - //should be impossible - System.err.println("raw type is not a class: " + rawType); - continue; - } - Class rawClass = (Class)rawType; - if (iface.isAssignableFrom(rawClass)) { - return new ClassContext(candidate, curContext).actualTypeArguments; - } else { - clazzes.addFirst(new ClassContext(candidate, curContext)); - } - } else if (type instanceof Class) { - clazzes.addFirst(new ClassContext((Class)type, curContext)); - } else { - //should never happen? - System.err.println("Unexpected type class " + type.getClass().getName()); - } - } - - final Type superClass = curContext.clazz.getGenericSuperclass(); - if (superClass instanceof ParameterizedType) { - clazzes.addFirst(new ClassContext((ParameterizedType)superClass, curContext)); - } else if (superClass != null) { - clazzes.addFirst(new ClassContext((Class)superClass, curContext)); - } - } - return null; - } - - - @SuppressWarnings({"rawtypes"}) - public static LambdaRequestHandler wrapRequestHandlerClass(final Class clazz) { - Type[] ptypes = findInterfaceParameters(clazz, RequestHandler.class); - if (ptypes == null) { - return new UserFaultHandler(makeUserFault("Class " - + clazz.getName() - + " does not implement RequestHandler with concrete type parameters")); - } - if (ptypes.length != 2) { - return new UserFaultHandler(makeUserFault( - "Invalid class signature for RequestHandler. Expected two generic types, got " + ptypes.length)); - } - - for (Type t : ptypes) { - if (t instanceof TypeVariable) { - Type[] bounds = ((TypeVariable)t).getBounds(); - boolean foundBound = false; - if (bounds != null) { - for (Type bound : bounds) { - if (!Object.class.equals(bound)) { - foundBound = true; - break; - } - } - } - - if (!foundBound) { - return new UserFaultHandler(makeUserFault("Class " + clazz.getName() - + " does not implement RequestHandler with concrete type parameters: parameter " - + t + " has no upper bound.")); - } - } - } - - final Type pType = ptypes[0]; - final Type rType = ptypes[1]; - - final Constructor constructor; - try { - constructor = getConstructor(clazz); - return wrapPojoHandler(newInstance(constructor), pType, rType); - } catch (UserFault f) { - return new UserFaultHandler(f); - } catch (Throwable e) { - return new UserFaultHandler(makeUserFault(e)); - } - } - - public static LambdaRequestHandler wrapRequestStreamHandlerClass(final Class clazz) { + private static LambdaRequestHandler wrapRequestStreamHandlerClass(final Class clazz) { final Constructor constructor; try { constructor = getConstructor(clazz); @@ -595,7 +323,7 @@ public static LambdaRequestHandler wrapRequestStreamHandlerClass(final Class clazz) { + private static LambdaRequestHandler loadStreamingRequestHandler(Class clazz) { if (RequestStreamHandler.class.isAssignableFrom(clazz)) { return wrapRequestStreamHandlerClass(clazz.asSubclass(RequestStreamHandler.class)); } else if (RequestHandler.class.isAssignableFrom(clazz)) { @@ -706,14 +434,14 @@ private static Optional getHandlerFromOverload(Class cl } } - private static final boolean isContext(Type t) { + private static boolean isContext(Type t) { return Context.class.equals(t); } /** * Returns true if the last type in params is a lambda context object interface (Context). */ - private static final boolean lastParameterIsContext(Class[] params) { + private static boolean lastParameterIsContext(Class[] params) { return params.length != 0 && isContext(params[params.length - 1]); } @@ -721,184 +449,473 @@ private static final boolean lastParameterIsContext(Class[] params) { * Implement a comparator for Methods. We sort overloaded handler methods using this comparator, and then pick the * lowest sorted method. */ - private static final Comparator methodPriority = new Comparator() { - public int compare(Method lhs, Method rhs) { + + private static LambdaRequestHandler loadEventPojoHandler(HandlerInfo handlerInfo) { + Method[] methods; + try { + methods = handlerInfo.clazz.getMethods(); + } catch (NoClassDefFoundError e) { + return new LambdaRequestHandler.UserFaultHandler(new UserFault( + "Error loading method " + handlerInfo.methodName + " on class " + handlerInfo.clazz.getName(), + e.getClass().getName(), + trace(e) + )); + } + if (methods.length == 0) { + final String msg = "Class " + + handlerInfo.getClass().getName() + + " has no public method named " + + handlerInfo.methodName; + return new UserFaultHandler(makeUserFault(msg)); + } - //1. Non bridge methods are preferred over bridge methods. - if (! lhs.isBridge() && rhs.isBridge()) { - return -1; + /* + * We support the following signatures + * Anything (InputStream, OutputStream, Context) + * Anything (InputStream, OutputStream) + * Anything (OutputStream, Context) + * Anything (InputStream, Context) + * Anything (InputStream) + * Anything (OutputStream) + * Anything (Context) + * Anything (AlmostAnything, Context) + * Anything (AlmostAnything) + * Anything () + * + * where AlmostAnything is any type except InputStream, OutputStream, Context + * Anything represents any type (primitive, void, or Object) + * + * prefer methods with longer signatures, add extra weight to those ending with a Context object + * + */ + + int slide = 0; + + for (int i = 0; i < methods.length; i++) { + Method m = methods[i]; + methods[i - slide] = m; + if (!m.getName().equals(handlerInfo.methodName)) { + slide++; + continue; } - else if (!rhs.isBridge() && lhs.isBridge()) { - return 1; + } + + final int end = methods.length - slide; + Arrays.sort(methods, 0, end, methodPriority); + + for (int i = 0; i < end; i++) { + Method m = methods[i]; + Optional result = getHandlerFromOverload(handlerInfo.clazz, m); + if (result.isPresent()) { + return result.get(); + } else { + continue; } + } - //2. We prefer longer signatures to shorter signatures. Except we count a method whose last argument is - //Context as having 1 more argument than it really does. This is a stupid thing to do, but we - //need to keep it for back compat reasons. - Class[] lParams = lhs.getParameterTypes(); - Class[] rParams = rhs.getParameterTypes(); + return new UserFaultHandler(makeUserFault("No public method named " + + handlerInfo.methodName + + " with appropriate method signature found on class " + + handlerInfo.clazz.getName())); + } - int lParamCompareLength = lParams.length; - int rParamCompareLength = rParams.length; + @SuppressWarnings({"rawtypes"}) + private static LambdaRequestHandler wrapPojoHandler(RequestHandler instance, Type pType, Type rType) { + return wrapRequestStreamHandler(new PojoHandlerAsStreamHandler(instance, Optional.ofNullable(pType), + isVoid(rType) ? Optional.empty() : Optional.of(rType) + )); + } + + private static LambdaRequestHandler wrapRequestStreamHandler(final RequestStreamHandler handler) { + return new LambdaRequestHandler() { + private final ThreadLocal outputBuffers = ThreadLocal.withInitial(() -> new ByteArrayOutputStream(1024)); + private ThreadLocal> log4jContextPutMethod = new ThreadLocal<>(); + + private void safeAddRequestIdToLog4j(String log4jContextClassName, InvocationRequest request, Class contextMapValueClass) { + try { + 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 + } + } + + /** + * Passes the LambdaContext to the logger so that the JSON formatter can include the requestId. + * + * We do casting here because both the LambdaRuntime and the LambdaLogger is in the core package, + * and the setLambdaContext(context) is a method we don't want to publish for customers. That method is + * only implemented on the internal LambdaContextLogger, so we check and cast to be able to call it. + * @param context the LambdaContext + */ + private void safeAddContextToLambdaLogger(LambdaContext context) { + LambdaLogger logger = com.amazonaws.services.lambda.runtime.LambdaRuntime.getLogger(); + if (logger instanceof LambdaContextLogger) { + LambdaContextLogger contextLogger = (LambdaContextLogger) logger; + contextLogger.setLambdaContext(context); + } + } + + public ByteArrayOutputStream call(InvocationRequest request) throws Error, Exception { + ByteArrayOutputStream output = outputBuffers.get(); + output.reset(); + + LambdaCognitoIdentity cognitoIdentity = null; + if (request.getCognitoIdentity() != null && !request.getCognitoIdentity().isEmpty()) { + cognitoIdentity = getCognitoSerializer().fromJson(request.getCognitoIdentity()); + } + + LambdaClientContext clientContext = null; + if (request.getClientContext() != null && !request.getClientContext().isEmpty()) { + //Use GSON here because it handles immutable types without requiring annotations + clientContext = getContextSerializer().fromJson(request.getClientContext()); + } + + LambdaContext context = new LambdaContext( + LambdaEnvironment.MEMORY_LIMIT, + request.getDeadlineTimeInMs(), + request.getId(), + LambdaEnvironment.LOG_GROUP_NAME, + LambdaEnvironment.LOG_STREAM_NAME, + LambdaEnvironment.FUNCTION_NAME, + cognitoIdentity, + LambdaEnvironment.FUNCTION_VERSION, + request.getInvokedFunctionArn(), + request.getTenantId(), + request.getXrayTraceId(), + clientContext + ); + + safeAddContextToLambdaLogger(context); + + if (LambdaRuntimeInternal.getUseLog4jAppender()) { + 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.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."); + } + } + + ByteArrayInputStream bais = new ByteArrayInputStream(request.getContent()); + handler.handleRequest(bais, output, context); + return output; + } + }; + } + + /** + * Wraps a RequestHandler as a lower level stream handler using supplied types. + * Optional types mean that the input and/or output should be ignored respectiveley + */ + @SuppressWarnings("rawtypes") + private static final class PojoHandlerAsStreamHandler implements RequestStreamHandler { + + public RequestHandler innerHandler; + public final Optional inputType; + public final Optional outputType; + + public PojoHandlerAsStreamHandler( + RequestHandler innerHandler, + Optional inputType, + Optional outputType + ) { + this.innerHandler = innerHandler; + this.inputType = inputType; + this.outputType = outputType; + + + if (inputType.isPresent()) { + getSerializerCached(Platform.UNKNOWN, inputType.get()); + } + + if (outputType.isPresent()) { + getSerializerCached(Platform.UNKNOWN, outputType.get()); + } + } + + @SuppressWarnings("unchecked") + @Override + public void handleRequest(InputStream inputStream, OutputStream outputStream, Context context) + throws IOException { + final Object input; + final Platform platform = getPlatform(context); + try { + if (inputType.isPresent()) { + input = getSerializerCached(platform, inputType.get()).fromJson(inputStream); + } else { + input = null; + } + } catch (Throwable t) { + throw new RuntimeException("An error occurred during JSON parsing", filterStackTrace(t)); + } + + final Object output; + try { + output = innerHandler.handleRequest(input, context); + } catch (Throwable t) { + throw UnsafeUtil.throwException(filterStackTrace(t)); + } + + try { + if (outputType.isPresent()) { + PojoSerializer serializer = getSerializerCached(platform, outputType.get()); + serializer.toJson(output, outputStream); + } else { + outputStream.write(_JsonNull); + } + } catch (Throwable t) { + throw new RuntimeException("An error occurred during JSON serialization of response", t); + } + } + } + + /** + * Wraps a java.lang.reflect.Method as a POJO RequestHandler + */ + private static final class PojoMethodRequestHandler implements RequestHandler { + public final Method m; + public final Type pType; + public final Object instance; + public final boolean needsContext; + public final int argSize; + + public PojoMethodRequestHandler(Method m, Type pType, Type rType, Object instance, boolean needsContext) { + this.m = m; + this.pType = pType; + this.instance = instance; + this.needsContext = needsContext; + this.argSize = (needsContext ? 1 : 0) + (pType != null ? 1 : 0); + } + + public static PojoMethodRequestHandler fromMethod( + Class clazz, + Method m, + Type pType, + Type rType, + boolean needsContext + ) throws Exception { + final Object instance; + if (Modifier.isStatic(m.getModifiers())) { + instance = null; + } else { + instance = newInstance(getConstructor(clazz)); + } + + return new PojoMethodRequestHandler(m, pType, rType, instance, needsContext); + } + + public static LambdaRequestHandler makeRequestHandler( + Class clazz, + Method m, + Type pType, + Type rType, + boolean needsContext + ) { + try { + return wrapPojoHandler(fromMethod(clazz, m, pType, rType, needsContext), pType, rType); + } catch (UserFault f) { + return new UserFaultHandler(f); + } catch (Throwable t) { + return new UserFaultHandler(makeUserFault(t)); + } + } + + @Override + public Object handleRequest(Object input, Context context) { + final Object[] args = new Object[argSize]; + int idx = 0; - if (lastParameterIsContext(lParams)) { - ++lParamCompareLength; + if (pType != null) { + args[idx++] = input; } - if (lastParameterIsContext(rParams)) { - ++rParamCompareLength; + if (this.needsContext) { + args[idx++] = context; } - return -Integer.compare(lParamCompareLength, rParamCompareLength); + try { + return m.invoke(this.instance, args); + } catch (InvocationTargetException e) { + if (e.getCause() != null) { + throw UnsafeUtil.throwException(filterStackTrace(e.getCause())); + } else { + throw UnsafeUtil.throwException(filterStackTrace(e)); + } + } catch (Throwable t) { + throw UnsafeUtil.throwException(filterStackTrace(t)); + } } - }; + } - private static LambdaRequestHandler loadEventPojoHandler(HandlerInfo handlerInfo) { - Method[] methods; - try { - methods = handlerInfo.clazz.getMethods(); - } catch (NoClassDefFoundError e) { - return new LambdaRequestHandler.UserFaultHandler(new UserFault( - "Error loading method " + handlerInfo.methodName + " on class " + handlerInfo.clazz.getName(), - e.getClass().getName(), - trace(e) - )); - } - if (methods.length == 0) { - final String msg = "Class " - + handlerInfo.getClass().getName() - + " has no public method named " - + handlerInfo.methodName; - return new UserFaultHandler(makeUserFault(msg)); + /** + * Wraps a java.lang.reflect.Method object as a RequestStreamHandler + */ + private static final class StreamMethodRequestHandler implements RequestStreamHandler { + public final Method m; + public final Object instance; + public final boolean needsInput; + public final boolean needsOutput; + public final boolean needsContext; + public final int argSize; + + public StreamMethodRequestHandler( + Method m, + Object instance, + boolean needsInput, + boolean needsOutput, + boolean needsContext + ) { + this.m = m; + this.instance = instance; + this.needsInput = needsInput; + this.needsOutput = needsOutput; + this.needsContext = needsContext; + this.argSize = (needsInput ? 1 : 0) + (needsOutput ? 1 : 0) + (needsContext ? 1 : 0); } - /* - * We support the following signatures - * Anything (InputStream, OutputStream, Context) - * Anything (InputStream, OutputStream) - * Anything (OutputStream, Context) - * Anything (InputStream, Context) - * Anything (InputStream) - * Anything (OutputStream) - * Anything (Context) - * Anything (AlmostAnything, Context) - * Anything (AlmostAnything) - * Anything () - * - * where AlmostAnything is any type except InputStream, OutputStream, Context - * Anything represents any type (primitive, void, or Object) - * - * prefer methods with longer signatures, add extra weight to those ending with a Context object - * - */ + public static StreamMethodRequestHandler fromMethod( + Class clazz, + Method m, + boolean needsInput, + boolean needsOutput, + boolean needsContext + ) throws Exception { + if (!isVoid(m.getReturnType())) { + System.err.println("Will ignore return type " + m.getReturnType() + " on byte stream handler"); + } + final Object instance = Modifier.isStatic(m.getModifiers()) + ? null + : newInstance(getConstructor(clazz)); - int slide = 0; + return new StreamMethodRequestHandler(m, instance, needsInput, needsOutput, needsContext); + } - for (int i = 0; i < methods.length; i++) { - Method m = methods[i]; - methods[i - slide] = m; - if (!m.getName().equals(handlerInfo.methodName)) { - slide++; - continue; + public static LambdaRequestHandler makeRequestHandler( + Class clazz, + Method m, + boolean needsInput, + boolean needsOutput, + boolean needsContext + ) { + try { + return wrapRequestStreamHandler(fromMethod(clazz, m, needsInput, needsOutput, needsContext)); + } catch (UserFault f) { + return new UserFaultHandler(f); + } catch (Throwable t) { + return new UserFaultHandler(makeUserFault(t)); } } - final int end = methods.length - slide; - Arrays.sort(methods, 0, end, methodPriority); + @Override + public void handleRequest(InputStream inputStream, OutputStream outputStream, Context context) + throws IOException { + final Object[] args = new Object[argSize]; + int idx = 0; - for (int i = 0; i < end; i++) { - Method m = methods[i]; - Optional result = getHandlerFromOverload(handlerInfo.clazz, m); - if (result.isPresent()) { - return result.get(); + if (needsInput) { + args[idx++] = inputStream; } else { - continue; + inputStream.close(); } - } - return new UserFaultHandler(makeUserFault("No public method named " - + handlerInfo.methodName - + " with appropriate method signature found on class " - + handlerInfo.clazz.getName())); - } + if (needsOutput) { + args[idx++] = outputStream; + } - @SuppressWarnings({"rawtypes"}) - public static LambdaRequestHandler wrapPojoHandler(RequestHandler instance, Type pType, Type rType) { - return wrapRequestStreamHandler(new PojoHandlerAsStreamHandler(instance, Optional.ofNullable(pType), - isVoid(rType) ? Optional.empty() : Optional.of(rType) - )); - } + if (needsContext) { + args[idx++] = context; + } - public static String exceptionToString(Throwable t) { - StringWriter writer = new StringWriter(65536); - try (PrintWriter wrapped = new PrintWriter(writer)) { - t.printStackTrace(wrapped); - } - StringBuffer buffer = writer.getBuffer(); - if (buffer.length() > 262144) { - final String extra = " Truncated by Lambda"; - buffer.delete(262144, buffer.length()); - buffer.append(extra); + try { + m.invoke(this.instance, args); + if (!needsOutput) { + outputStream.write(_JsonNull); + } + } catch (InvocationTargetException e) { + if (e.getCause() != null) { + throw UnsafeUtil.throwException(filterStackTrace(e.getCause())); + } else { + throw UnsafeUtil.throwException(filterStackTrace(e)); + } + } catch (Throwable t) { + throw UnsafeUtil.throwException(filterStackTrace(t)); + } } - - return buffer.toString(); } - public static LambdaRequestHandler wrapRequestStreamHandler(final RequestStreamHandler handler) { - return new LambdaRequestHandler() { - private final ByteArrayOutputStream output = new ByteArrayOutputStream(1024); - private Functions.V2 log4jContextPutMethod = null; + private static final class ClassContext { + public final Class clazz; + public final Type[] actualTypeArguments; - 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()); - } catch (Exception e) {} - } + @SuppressWarnings({"rawtypes"}) + private TypeVariable[] typeParameters; - public ByteArrayOutputStream call(InvocationRequest request) throws Error, Exception { - output.reset(); + public ClassContext(Class clazz, Type[] actualTypeArguments) { + this.clazz = clazz; + this.actualTypeArguments = actualTypeArguments; + } - LambdaCognitoIdentity cognitoIdentity = null; - if(request.getCognitoIdentity() != null && !request.getCognitoIdentity().isEmpty()) { - cognitoIdentity = getCognitoSerializer().fromJson(request.getCognitoIdentity()); + @SuppressWarnings({"rawtypes"}) + public ClassContext(Class clazz, ClassContext curContext) { + this.typeParameters = clazz.getTypeParameters(); + if (typeParameters.length == 0 || curContext.actualTypeArguments == null) { + this.clazz = clazz; + this.actualTypeArguments = null; + } else { + Type[] types = new Type[typeParameters.length]; + for (int i = 0; i < types.length; i++) { + types[i] = curContext.resolveTypeVariable(typeParameters[i]); } - LambdaClientContext clientContext = null; - if (request.getClientContext() != null && !request.getClientContext().isEmpty()) { - //Use GSON here because it handles immutable types without requiring annotations - clientContext = getContextSerializer().fromJson(request.getClientContext()); + this.clazz = clazz; + this.actualTypeArguments = types; + } + } + + @SuppressWarnings({"rawtypes"}) + public ClassContext(ParameterizedType type, ClassContext curContext) { + Type[] types = type.getActualTypeArguments(); + for (int i = 0; i < types.length; i++) { + Type t = types[i]; + if (t instanceof TypeVariable) { + types[i] = curContext.resolveTypeVariable((TypeVariable) t); } + } - LambdaContext context = new LambdaContext( - LambdaEnvironment.MEMORY_LIMIT, - request.getDeadlineTimeInMs(), - request.getId(), - LambdaEnvironment.LOG_GROUP_NAME, - LambdaEnvironment.LOG_STREAM_NAME, - LambdaEnvironment.FUNCTION_NAME, - cognitoIdentity, - LambdaEnvironment.FUNCTION_VERSION, - request.getInvokedFunctionArn(), - clientContext - ); + Type t = type.getRawType(); + if (t instanceof Class) { + this.clazz = (Class) t; + } else if (t instanceof TypeVariable) { + this.clazz = (Class) ((TypeVariable) t).getGenericDeclaration(); + } else { + throw new RuntimeException("Type " + t + " is of unexpected type " + t.getClass()); + } + this.actualTypeArguments = types; + } - if (LambdaRuntimeInternal.getUseLog4jAppender()) { - 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) { - 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."); - } + @SuppressWarnings({"rawtypes"}) + public Type resolveTypeVariable(TypeVariable t) { + TypeVariable[] variables = getTypeParameters(); + for (int i = 0; i < variables.length; i++) { + if (t.getName().equals(variables[i].getName())) { + return actualTypeArguments == null ? variables[i] : actualTypeArguments[i]; } + } - handler.handleRequest(request.getContentAsStream(), output, context); - return output; + return t; + } + + @SuppressWarnings({"rawtypes"}) + private TypeVariable[] getTypeParameters() { + if (typeParameters == null) { + typeParameters = clazz.getTypeParameters(); } - }; + return typeParameters; + } } + } diff --git a/aws-lambda-java-runtime-interface-client/src/main/java/com/amazonaws/services/lambda/runtime/api/client/Failure.java b/aws-lambda-java-runtime-interface-client/src/main/java/com/amazonaws/services/lambda/runtime/api/client/Failure.java deleted file mode 100644 index d568d9739..000000000 --- a/aws-lambda-java-runtime-interface-client/src/main/java/com/amazonaws/services/lambda/runtime/api/client/Failure.java +++ /dev/null @@ -1,84 +0,0 @@ -/* Copyright 2019 Amazon.com, Inc. or its affiliates. All Rights Reserved. */ - -package com.amazonaws.services.lambda.runtime.api.client; - -import java.io.IOError; -import java.util.Arrays; -import java.util.Collections; -import java.util.Comparator; -import java.util.List; -import java.util.stream.Collectors; - -public class Failure { - - private static final Class[] reportableExceptionsArray = { - Error.class, - ClassNotFoundException.class, - IOError.class, - Throwable.class, - VirtualMachineError.class, - LinkageError.class, - ExceptionInInitializerError.class, - NoClassDefFoundError.class, - HandlerInfo.InvalidHandlerException.class - }; - - private static final List sortedExceptions = Collections.unmodifiableList( - Arrays.stream(reportableExceptionsArray).sorted(new ClassHierarchyComparator()).collect(Collectors.toList())); - - private final String errorMessage; - private final String errorType; - private final String[] stackTrace; - private final Failure cause; - - public Failure(Throwable t) { - this.errorMessage = t.getLocalizedMessage() == null ? t.getClass().getName() : t.getLocalizedMessage(); - this.errorType = t.getClass().getName(); - StackTraceElement[] trace = t.getStackTrace(); - this.stackTrace = new String[trace.length]; - for( int i = 0; i < trace.length; i++) { - this.stackTrace[i] = trace[i].toString(); - } - Throwable cause = t.getCause(); - this.cause = (cause == null) ? null : new Failure(cause); - } - - public Failure(UserFault userFault) { - this.errorMessage = userFault.msg; - this.errorType = userFault.exception; - // Not setting stacktrace for compatibility with legacy/native runtime - this.stackTrace = null; - this.cause = null; - } - - public static Class getReportableExceptionClass(Throwable customerException) { - return sortedExceptions - .stream() - .filter(e -> e.isAssignableFrom(customerException.getClass())) - .findFirst() - .orElse(Throwable.class); - } - - public static String getReportableExceptionClassName(Throwable f) { - return getReportableExceptionClass(f).getName(); - } - - public static boolean isInvokeFailureFatal(Throwable t) { - return t instanceof VirtualMachineError || t instanceof IOError; - } - - private static class ClassHierarchyComparator implements Comparator { - @Override - public int compare(Class o1, Class o2) { - if (o1.isAssignableFrom(o2)) { - return 1; - } else { - return -1; - } - } - } - - public String getErrorType() { - return errorType; - } -} diff --git a/aws-lambda-java-runtime-interface-client/src/main/java/com/amazonaws/services/lambda/runtime/api/client/HandlerInfo.java b/aws-lambda-java-runtime-interface-client/src/main/java/com/amazonaws/services/lambda/runtime/api/client/HandlerInfo.java index eaca0fd0a..54e2f6710 100644 --- a/aws-lambda-java-runtime-interface-client/src/main/java/com/amazonaws/services/lambda/runtime/api/client/HandlerInfo.java +++ b/aws-lambda-java-runtime-interface-client/src/main/java/com/amazonaws/services/lambda/runtime/api/client/HandlerInfo.java @@ -1,16 +1,17 @@ -/* Copyright 2019 Amazon.com, Inc. or its affiliates. All Rights Reserved. */ +/* +Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +SPDX-License-Identifier: Apache-2.0 +*/ package com.amazonaws.services.lambda.runtime.api.client; public final class HandlerInfo { - public static class InvalidHandlerException extends RuntimeException { - public static final long serialVersionUID = -1; - } - + public final Class clazz; public final String methodName; - public HandlerInfo (Class clazz, String methodName) { + + public HandlerInfo(Class clazz, String methodName) { this.clazz = clazz; this.methodName = methodName; } @@ -19,7 +20,7 @@ public static HandlerInfo fromString(String handler, ClassLoader cl) throws Clas final int colonLoc = handler.lastIndexOf("::"); final String className; final String methodName; - if(colonLoc < 0) { + if (colonLoc < 0) { className = handler; methodName = null; } else { @@ -27,7 +28,7 @@ public static HandlerInfo fromString(String handler, ClassLoader cl) throws Clas methodName = handler.substring(colonLoc + 2); } - if(className.isEmpty() || (methodName != null && methodName.isEmpty())) { + if (className.isEmpty() || (methodName != null && methodName.isEmpty())) { throw new InvalidHandlerException(); } return new HandlerInfo(Class.forName(className, true, cl), methodName); @@ -37,4 +38,8 @@ public static String className(String handler) { final int colonLoc = handler.lastIndexOf("::"); return (colonLoc < 0) ? handler : handler.substring(0, colonLoc); } + + public static class InvalidHandlerException extends RuntimeException { + public static final long serialVersionUID = -1; + } } diff --git a/aws-lambda-java-runtime-interface-client/src/main/java/com/amazonaws/services/lambda/runtime/api/client/LambdaEnvironment.java b/aws-lambda-java-runtime-interface-client/src/main/java/com/amazonaws/services/lambda/runtime/api/client/LambdaEnvironment.java index b2d552049..77838f72a 100644 --- a/aws-lambda-java-runtime-interface-client/src/main/java/com/amazonaws/services/lambda/runtime/api/client/LambdaEnvironment.java +++ b/aws-lambda-java-runtime-interface-client/src/main/java/com/amazonaws/services/lambda/runtime/api/client/LambdaEnvironment.java @@ -1,16 +1,29 @@ -/* Copyright 2019 Amazon.com, Inc. or its affiliates. All Rights Reserved. */ +/* +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 com.amazonaws.services.lambda.runtime.api.client.util.EnvReader; - +import static com.amazonaws.services.lambda.runtime.api.client.ReservedRuntimeEnvironmentVariables.AWS_LAMBDA_FUNCTION_MEMORY_SIZE; +import static com.amazonaws.services.lambda.runtime.api.client.ReservedRuntimeEnvironmentVariables.AWS_LAMBDA_FUNCTION_NAME; +import static com.amazonaws.services.lambda.runtime.api.client.ReservedRuntimeEnvironmentVariables.AWS_LAMBDA_FUNCTION_VERSION; +import static com.amazonaws.services.lambda.runtime.api.client.ReservedRuntimeEnvironmentVariables.AWS_LAMBDA_LOG_FORMAT; +import static com.amazonaws.services.lambda.runtime.api.client.ReservedRuntimeEnvironmentVariables.AWS_LAMBDA_LOG_GROUP_NAME; +import static com.amazonaws.services.lambda.runtime.api.client.ReservedRuntimeEnvironmentVariables.AWS_LAMBDA_LOG_LEVEL; +import static com.amazonaws.services.lambda.runtime.api.client.ReservedRuntimeEnvironmentVariables.AWS_LAMBDA_LOG_STREAM_NAME; +import static com.amazonaws.services.lambda.runtime.api.client.ReservedRuntimeEnvironmentVariables.AWS_LAMBDA_RUNTIME_API; import static java.lang.Integer.parseInt; public class LambdaEnvironment { public static final EnvReader ENV_READER = new EnvReader(); - public static final int MEMORY_LIMIT = parseInt(ENV_READER.getEnvOrDefault(ReservedRuntimeEnvironmentVariables.AWS_LAMBDA_FUNCTION_MEMORY_SIZE, "128")); - public static final String LOG_GROUP_NAME = ENV_READER.getEnv(ReservedRuntimeEnvironmentVariables.AWS_LAMBDA_LOG_GROUP_NAME); - public static final String LOG_STREAM_NAME = ENV_READER.getEnv(ReservedRuntimeEnvironmentVariables.AWS_LAMBDA_LOG_STREAM_NAME); - public static final String FUNCTION_NAME = ENV_READER.getEnv(ReservedRuntimeEnvironmentVariables.AWS_LAMBDA_FUNCTION_NAME); - public static final String FUNCTION_VERSION = ENV_READER.getEnv(ReservedRuntimeEnvironmentVariables.AWS_LAMBDA_FUNCTION_VERSION); + public static final int MEMORY_LIMIT = parseInt(ENV_READER.getEnvOrDefault(AWS_LAMBDA_FUNCTION_MEMORY_SIZE, "128")); + public static final String LOG_GROUP_NAME = ENV_READER.getEnv(AWS_LAMBDA_LOG_GROUP_NAME); + public static final String LOG_STREAM_NAME = ENV_READER.getEnv(AWS_LAMBDA_LOG_STREAM_NAME); + public static final String LAMBDA_LOG_LEVEL = ENV_READER.getEnvOrDefault(AWS_LAMBDA_LOG_LEVEL, "UNDEFINED"); + public static final String LAMBDA_LOG_FORMAT = ENV_READER.getEnvOrDefault(AWS_LAMBDA_LOG_FORMAT, "TEXT"); + public static final String FUNCTION_NAME = ENV_READER.getEnv(AWS_LAMBDA_FUNCTION_NAME); + public static final String FUNCTION_VERSION = ENV_READER.getEnv(AWS_LAMBDA_FUNCTION_VERSION); + public static final String RUNTIME_API = ENV_READER.getEnv(AWS_LAMBDA_RUNTIME_API); } diff --git a/aws-lambda-java-runtime-interface-client/src/main/java/com/amazonaws/services/lambda/runtime/api/client/LambdaRequestHandler.java b/aws-lambda-java-runtime-interface-client/src/main/java/com/amazonaws/services/lambda/runtime/api/client/LambdaRequestHandler.java index d22d6394d..ce9254ef8 100644 --- a/aws-lambda-java-runtime-interface-client/src/main/java/com/amazonaws/services/lambda/runtime/api/client/LambdaRequestHandler.java +++ b/aws-lambda-java-runtime-interface-client/src/main/java/com/amazonaws/services/lambda/runtime/api/client/LambdaRequestHandler.java @@ -1,14 +1,24 @@ -/* Copyright 2019 Amazon.com, Inc. or its affiliates. All Rights Reserved. */ +/* +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 com.amazonaws.services.lambda.runtime.api.client.runtimeapi.InvocationRequest; - +import com.amazonaws.services.lambda.runtime.api.client.runtimeapi.dto.InvocationRequest; import java.io.ByteArrayOutputStream; public interface LambdaRequestHandler { ByteArrayOutputStream call(InvocationRequest request) throws Error, Exception; + static LambdaRequestHandler initErrorHandler(final Throwable e, String className) { + return new UserFaultHandler(UserFault.makeInitErrorUserFault(e, className)); + } + + static LambdaRequestHandler classNotFound(final Throwable e, String className) { + return new UserFaultHandler(UserFault.makeClassNotFoundUserFault(e, className)); + } + class UserFaultHandler implements LambdaRequestHandler { public final UserFault fault; @@ -20,12 +30,4 @@ public ByteArrayOutputStream call(InvocationRequest request) { throw fault; } } - - static LambdaRequestHandler initErrorHandler(final Throwable e, String className) { - return new UserFaultHandler(UserFault.makeInitErrorUserFault(e, className)); - } - - static LambdaRequestHandler classNotFound(final Throwable e, String className) { - return new UserFaultHandler(UserFault.makeClassNotFoundUserFault(e, className)); - } } 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 new file mode 100644 index 000000000..da37f7ca7 --- /dev/null +++ b/aws-lambda-java-runtime-interface-client/src/main/java/com/amazonaws/services/lambda/runtime/api/client/PojoSerializerLoader.java @@ -0,0 +1,77 @@ +/* +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 com.amazonaws.services.lambda.runtime.CustomPojoSerializer; +import com.amazonaws.services.lambda.runtime.serialization.PojoSerializer; +import java.io.InputStream; +import java.io.OutputStream; +import java.lang.reflect.Type; +import java.util.Iterator; +import java.util.ServiceConfigurationError; +import java.util.ServiceLoader; + +public class PojoSerializerLoader { + // The serializer obtained from the provider will always be the same so we can cache it as a filed. + private static CustomPojoSerializer customPojoSerializer; + // If Input and Output type are different, the runtime will try to search for a serializer twice due to + // the getSerializerCached method. Save the initialization state in order to search for the provider only once. + private static boolean initialized = false; + + private static CustomPojoSerializer loadSerializer() + throws ServiceConfigurationError, TooManyServiceProvidersFoundException { + + if (customPojoSerializer != null) { + return customPojoSerializer; + } + + ServiceLoader loader = ServiceLoader.load(CustomPojoSerializer.class, AWSLambda.getCustomerClassLoader()); + Iterator serializers = loader.iterator(); + + if (!serializers.hasNext()) { + initialized = true; + return null; + } + + customPojoSerializer = serializers.next(); + + if (serializers.hasNext()) { + throw new TooManyServiceProvidersFoundException( + "Too many serializers provided inside the META-INF/services folder, only one is allowed" + ); + } + + initialized = true; + return customPojoSerializer; + } + + public static PojoSerializer getCustomerSerializer(Type type) { + if (!initialized) { + customPojoSerializer = loadSerializer(); + } + + if (customPojoSerializer == null) { + return null; + } + + return new PojoSerializer() { + @Override + public Object fromJson(InputStream input) { + return customPojoSerializer.fromJson(input, type); + } + + @Override + public Object fromJson(String input) { + return customPojoSerializer.fromJson(input, type); + } + + @Override + public void toJson(Object value, OutputStream output) { + customPojoSerializer.toJson(value, output, type); + } + }; + } +} 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 fc5006c5d..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 @@ -1,28 +1,18 @@ /* - * Copyright 2017-2020 original authors - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ +Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +SPDX-License-Identifier: Apache-2.0 +*/ + package com.amazonaws.services.lambda.runtime.api.client; /** * Lambda runtimes set several environment variables during initialization. * Most of the environment variables provide information about the function or runtime. * The keys for these environment variables are reserved and cannot be set in your function configuration. - * @see Using AWS Lambda Environment Variables * + * @see Using AWS Lambda Environment Variables + *

* NOTICE: This class is forked from io.micronaut.function.aws.runtime.ReservedRuntimeEnvironments found at https://github.com/micronaut-projects/micronaut-aws - * */ public interface ReservedRuntimeEnvironmentVariables { @@ -66,6 +56,16 @@ public interface ReservedRuntimeEnvironmentVariables { */ String AWS_LAMBDA_LOG_STREAM_NAME = "AWS_LAMBDA_LOG_STREAM_NAME"; + /** + * The logging level set for the function. + */ + String AWS_LAMBDA_LOG_LEVEL = "AWS_LAMBDA_LOG_LEVEL"; + + /** + * The logging format set for the function. + */ + String AWS_LAMBDA_LOG_FORMAT = "AWS_LAMBDA_LOG_FORMAT"; + /** * Access key id obtained from the function's execution role. */ @@ -77,7 +77,6 @@ public interface ReservedRuntimeEnvironmentVariables { String AWS_SECRET_ACCESS_KEY = "AWS_SECRET_ACCESS_KEY"; /** - * * The access keys obtained from the function's execution role. */ String AWS_SESSION_TOKEN = "AWS_SESSION_TOKEN"; @@ -87,6 +86,12 @@ public interface ReservedRuntimeEnvironmentVariables { */ String AWS_LAMBDA_RUNTIME_API = "AWS_LAMBDA_RUNTIME_API"; + + /** + * Initialization type + */ + String AWS_LAMBDA_INITIALIZATION_TYPE = "AWS_LAMBDA_INITIALIZATION_TYPE"; + /** * The path to your Lambda function code. */ @@ -101,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/TooManyServiceProvidersFoundException.java b/aws-lambda-java-runtime-interface-client/src/main/java/com/amazonaws/services/lambda/runtime/api/client/TooManyServiceProvidersFoundException.java new file mode 100644 index 000000000..07fac7170 --- /dev/null +++ b/aws-lambda-java-runtime-interface-client/src/main/java/com/amazonaws/services/lambda/runtime/api/client/TooManyServiceProvidersFoundException.java @@ -0,0 +1,23 @@ +/* +Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +SPDX-License-Identifier: Apache-2.0 +*/ + +package com.amazonaws.services.lambda.runtime.api.client; + +public class TooManyServiceProvidersFoundException extends RuntimeException { + public TooManyServiceProvidersFoundException() { + } + + public TooManyServiceProvidersFoundException(String errorMessage) { + super(errorMessage); + } + + public TooManyServiceProvidersFoundException(Throwable cause) { + super(cause); + } + + public TooManyServiceProvidersFoundException(String message, Throwable cause) { + super(message, cause); + } +} 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 73066ad51..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 @@ -1,20 +1,24 @@ -/* Copyright 2019 Amazon.com, Inc. or its affiliates. All Rights Reserved. */ +/* +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 java.io.PrintWriter; import java.io.StringWriter; +import java.util.HashSet; +import java.util.Set; public final class UserFault extends RuntimeException { - private static final long serialVersionUID = 0; + private static final long serialVersionUID = -479308856905162038L; + private static final String packagePrefix = AWSLambda.class.getPackage().getName(); public final String msg; public final String exception; public final String trace; public final Boolean fatal; - private static final String packagePrefix = AWSLambda.class.getPackage().getName(); - public UserFault(String msg, String exception, String trace) { this.msg = msg; this.exception = exception; @@ -34,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) { @@ -65,9 +69,19 @@ public static String trace(Throwable t) { * the same object for convenience. */ public static T filterStackTrace(T t) { + return filterStackTrace(t, new HashSet<>(), new HashSet<>()); + } + + private static T filterStackTrace(T t, Set visited, Set visitedSuppressed) { + if (visited.contains(t)) { + return t; + } + + visited.add(t); + StackTraceElement[] trace = t.getStackTrace(); - for(int i = 0; i < trace.length; i++) { - if(trace[i].getClassName().startsWith(packagePrefix)) { + for (int i = 0; i < trace.length; i++) { + if (trace[i].getClassName().startsWith(packagePrefix)) { StackTraceElement[] newTrace = new StackTraceElement[i]; System.arraycopy(trace, 0, newTrace, 0, i); t.setStackTrace(newTrace); @@ -77,9 +91,18 @@ public static T filterStackTrace(T t) { Throwable cause = t.getCause(); - if(cause != null) { - filterStackTrace(cause); + if (cause != null) { + filterStackTrace(cause, visited, visitedSuppressed); + } + + Throwable[] suppressedExceptions = t.getSuppressed(); + for (Throwable suppressed: suppressedExceptions) { + if (!visitedSuppressed.contains(suppressed)) { + visitedSuppressed.add(suppressed); + filterStackTrace(suppressed, visited, visitedSuppressed); + } } + return t; } @@ -102,7 +125,7 @@ static UserFault makeClassNotFoundUserFault(Throwable e, String className) { } public String reportableError() { - if(this.exception != null || this.trace != null) { + if (this.exception != null || this.trace != null) { return String.format("%s: %s\n%s\n", this.msg, this.exception == null ? "" : this.exception, diff --git a/aws-lambda-java-runtime-interface-client/src/main/java/com/amazonaws/services/lambda/runtime/api/client/UserMethods.java b/aws-lambda-java-runtime-interface-client/src/main/java/com/amazonaws/services/lambda/runtime/api/client/UserMethods.java deleted file mode 100644 index 4012b3811..000000000 --- a/aws-lambda-java-runtime-interface-client/src/main/java/com/amazonaws/services/lambda/runtime/api/client/UserMethods.java +++ /dev/null @@ -1,13 +0,0 @@ -/* Copyright 2019 Amazon.com, Inc. or its affiliates. All Rights Reserved. */ - -package com.amazonaws.services.lambda.runtime.api.client; - -public final class UserMethods { - public final Runnable initHandler; - public final LambdaRequestHandler requestHandler; - - public UserMethods(Runnable initHandler, LambdaRequestHandler requestHandler) { - this.initHandler = initHandler; - this.requestHandler = requestHandler; - } -} diff --git a/aws-lambda-java-runtime-interface-client/src/main/java/com/amazonaws/services/lambda/runtime/api/client/XRayErrorCause.java b/aws-lambda-java-runtime-interface-client/src/main/java/com/amazonaws/services/lambda/runtime/api/client/XRayErrorCause.java deleted file mode 100644 index 5a05810e5..000000000 --- a/aws-lambda-java-runtime-interface-client/src/main/java/com/amazonaws/services/lambda/runtime/api/client/XRayErrorCause.java +++ /dev/null @@ -1,114 +0,0 @@ -/* Copyright 2019 Amazon.com, Inc. or its affiliates. All Rights Reserved. */ - -package com.amazonaws.services.lambda.runtime.api.client; - -import java.util.Arrays; -import java.util.Collection; -import java.util.Collections; -import java.util.List; -import java.util.stream.Collectors; - -/** - * helper class for serializing an exception in the format expected by XRay's web console. - */ -public class XRayErrorCause { - private final String working_directory; - private final Collection exceptions; - private final Collection paths; - - public XRayErrorCause(Throwable throwable) { - working_directory = System.getProperty("user.dir"); - exceptions = Collections.unmodifiableCollection(Collections.singletonList(new XRayException(throwable))); - paths = Collections.unmodifiableCollection( - Arrays.stream(throwable.getStackTrace()) - .map(XRayErrorCause::determineFileName) - .collect(Collectors.toSet())); - } - - public String getWorking_directory() { - return working_directory; - } - - public Collection getExceptions() { - return exceptions; - } - - public Collection getPaths() { - return paths; - } - - /** - * This method provides compatibility between Java 8 and Java 11 in determining the fileName of the class in the - * StackTraceElement. - * - * If the fileName property of the StackTraceElement is null (as it can be for native methods in Java 11), it - * constructs it using the className by stripping out the package and appending ".java". - */ - private static String determineFileName(StackTraceElement e) { - String fileName = null; - if(e.getFileName() != null) { - fileName = e.getFileName(); - } - if(fileName == null) { - String className = e.getClassName(); - fileName = className == null ? null : className.substring(className.lastIndexOf('.') + 1) + ".java"; - } - return fileName; - } - - public static class XRayException { - private final String message; - private final String type; - private final List stack; - - public XRayException(Throwable throwable) { - this.message = throwable.getMessage(); - this.type = throwable.getClass().getName(); - this.stack = Arrays.stream(throwable.getStackTrace()).map(this::toStackElement).collect(Collectors.toList()); - } - - private StackElement toStackElement(StackTraceElement e) { - return new StackElement( - e.getMethodName(), - determineFileName(e), - e.getLineNumber()); - } - - public String getMessage() { - return message; - } - - public String getType() { - return type; - } - - public List getStack() { - return stack; - } - - public static class StackElement { - private final String label; - private final String path; - private final int line; - - private StackElement(String label, String path, int line) { - this.label = label; - this.path = path; - this.line = line; - } - - public String getLabel() { - return label; - } - - public String getPath() { - return path; - } - - public int getLine() { - return line; - } - } - } - -} diff --git a/aws-lambda-java-runtime-interface-client/src/main/java/com/amazonaws/services/lambda/runtime/api/client/api/LambdaClientContext.java b/aws-lambda-java-runtime-interface-client/src/main/java/com/amazonaws/services/lambda/runtime/api/client/api/LambdaClientContext.java index 8887dfb24..3baa5347b 100644 --- a/aws-lambda-java-runtime-interface-client/src/main/java/com/amazonaws/services/lambda/runtime/api/client/api/LambdaClientContext.java +++ b/aws-lambda-java-runtime-interface-client/src/main/java/com/amazonaws/services/lambda/runtime/api/client/api/LambdaClientContext.java @@ -1,11 +1,13 @@ -/* Copyright 2019 Amazon.com, Inc. or its affiliates. All Rights Reserved. */ +/* +Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +SPDX-License-Identifier: Apache-2.0 +*/ package com.amazonaws.services.lambda.runtime.api.client.api; -import java.util.Map; - -import com.amazonaws.services.lambda.runtime.ClientContext; import com.amazonaws.services.lambda.runtime.Client; +import com.amazonaws.services.lambda.runtime.ClientContext; +import java.util.Map; public class LambdaClientContext implements ClientContext { diff --git a/aws-lambda-java-runtime-interface-client/src/main/java/com/amazonaws/services/lambda/runtime/api/client/api/LambdaClientContextClient.java b/aws-lambda-java-runtime-interface-client/src/main/java/com/amazonaws/services/lambda/runtime/api/client/api/LambdaClientContextClient.java index 66e86d3f0..b76a25f5e 100644 --- a/aws-lambda-java-runtime-interface-client/src/main/java/com/amazonaws/services/lambda/runtime/api/client/api/LambdaClientContextClient.java +++ b/aws-lambda-java-runtime-interface-client/src/main/java/com/amazonaws/services/lambda/runtime/api/client/api/LambdaClientContextClient.java @@ -1,4 +1,7 @@ -/* Copyright 2019 Amazon.com, Inc. or its affiliates. All Rights Reserved. */ +/* +Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +SPDX-License-Identifier: Apache-2.0 +*/ package com.amazonaws.services.lambda.runtime.api.client.api; diff --git a/aws-lambda-java-runtime-interface-client/src/main/java/com/amazonaws/services/lambda/runtime/api/client/api/LambdaCognitoIdentity.java b/aws-lambda-java-runtime-interface-client/src/main/java/com/amazonaws/services/lambda/runtime/api/client/api/LambdaCognitoIdentity.java index 5b9df4f23..89e60d348 100644 --- a/aws-lambda-java-runtime-interface-client/src/main/java/com/amazonaws/services/lambda/runtime/api/client/api/LambdaCognitoIdentity.java +++ b/aws-lambda-java-runtime-interface-client/src/main/java/com/amazonaws/services/lambda/runtime/api/client/api/LambdaCognitoIdentity.java @@ -1,4 +1,7 @@ -/* Copyright 2019 Amazon.com, Inc. or its affiliates. All Rights Reserved. */ +/* +Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +SPDX-License-Identifier: Apache-2.0 +*/ package com.amazonaws.services.lambda.runtime.api.client.api; 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 689c9110d..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 @@ -1,4 +1,7 @@ -/* Copyright 2019 Amazon.com, Inc. or its affiliates. All Rights Reserved. */ +/* +Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +SPDX-License-Identifier: Apache-2.0 +*/ package com.amazonaws.services.lambda.runtime.api.client.api; @@ -19,19 +22,23 @@ public class LambdaContext implements Context { private final long deadlineTimeInMs; private final CognitoIdentity cognitoIdentity; private final ClientContext clientContext; + private final String tenantId; + private final String xrayTraceId; private final LambdaLogger logger; public LambdaContext( - int memoryLimit, - long deadlineTimeInMs, - String requestId, - String logGroupName, - String logStreamName, - String functionName, - CognitoIdentity identity, - String functionVersion, - String invokedFunctionArn, - ClientContext clientContext + int memoryLimit, + long deadlineTimeInMs, + String requestId, + String logGroupName, + String logStreamName, + String functionName, + CognitoIdentity identity, + String functionVersion, + String invokedFunctionArn, + String tenantId, + String xrayTraceId, + ClientContext clientContext ) { this.memoryLimit = memoryLimit; this.deadlineTimeInMs = deadlineTimeInMs; @@ -43,6 +50,8 @@ public LambdaContext( this.clientContext = clientContext; this.functionVersion = functionVersion; this.invokedFunctionArn = invokedFunctionArn; + this.tenantId = tenantId; + this.xrayTraceId = xrayTraceId; this.logger = com.amazonaws.services.lambda.runtime.LambdaRuntime.getLogger(); } @@ -88,6 +97,14 @@ public int getRemainingTimeInMillis() { return delta > 0 ? delta : 0; } + 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/AbstractLambdaLogger.java b/aws-lambda-java-runtime-interface-client/src/main/java/com/amazonaws/services/lambda/runtime/api/client/logging/AbstractLambdaLogger.java new file mode 100644 index 000000000..f987b0bdb --- /dev/null +++ b/aws-lambda-java-runtime-interface-client/src/main/java/com/amazonaws/services/lambda/runtime/api/client/logging/AbstractLambdaLogger.java @@ -0,0 +1,74 @@ +/* +Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +SPDX-License-Identifier: Apache-2.0 +*/ + +package com.amazonaws.services.lambda.runtime.api.client.logging; + +import com.amazonaws.services.lambda.runtime.LambdaLogger; +import com.amazonaws.services.lambda.runtime.api.client.api.LambdaContext; +import com.amazonaws.services.lambda.runtime.logging.LogFormat; +import com.amazonaws.services.lambda.runtime.logging.LogLevel; +import static java.nio.charset.StandardCharsets.UTF_8; + +/** + * Provides default implementation of the convenience logger functions. + * When extending AbstractLambdaLogger, only one function has to be overridden: + * void logMessage(byte[] message, LogLevel logLevel); + */ +public abstract class AbstractLambdaLogger implements LambdaLogger { + protected final LogFormat logFormat; + private final LogFiltering logFiltering; + private final LogFormatter logFormatter; + + public AbstractLambdaLogger(LogLevel logLevel, LogFormat logFormat) { + this.logFiltering = new LogFiltering(logLevel); + + this.logFormat = logFormat; + if (logFormat == LogFormat.JSON) { + logFormatter = new JsonLogFormatter(); + } else { + logFormatter = new TextLogFormatter(); + } + } + + protected abstract void logMessage(byte[] message, LogLevel logLevel); + + protected void logMessage(String message, LogLevel logLevel) { + byte[] messageBytes = message == null ? null : message.getBytes(UTF_8); + logMessage(messageBytes, logLevel); + } + + @Override + public void log(String message, LogLevel logLevel) { + if (logFiltering.isEnabled(logLevel)) { + this.logMessage(logFormatter.format(message, logLevel), logLevel); + } + } + + @Override + public void log(byte[] message, LogLevel logLevel) { + if (logFiltering.isEnabled(logLevel)) { + // there is no formatting for byte[] messages + this.logMessage(message, logLevel); + } + } + + @Override + public void log(String message) { + this.log(message, LogLevel.UNDEFINED); + } + + @Override + public void log(byte[] message) { + this.log(message, LogLevel.UNDEFINED); + } + + public void setLambdaContext(LambdaContext lambdaContext) { + this.logFormatter.setLambdaContext(lambdaContext); + } + + public LogFormat getLogFormat() { + return logFormat; + } +} diff --git a/aws-lambda-java-runtime-interface-client/src/main/java/com/amazonaws/services/lambda/runtime/api/client/logging/FrameType.java b/aws-lambda-java-runtime-interface-client/src/main/java/com/amazonaws/services/lambda/runtime/api/client/logging/FrameType.java index 1284bc64a..f3891ce20 100644 --- a/aws-lambda-java-runtime-interface-client/src/main/java/com/amazonaws/services/lambda/runtime/api/client/logging/FrameType.java +++ b/aws-lambda-java-runtime-interface-client/src/main/java/com/amazonaws/services/lambda/runtime/api/client/logging/FrameType.java @@ -1,10 +1,34 @@ -/* Copyright 2019 Amazon.com, Inc. or its affiliates. All Rights Reserved. */ +/* +Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +SPDX-License-Identifier: Apache-2.0 +*/ package com.amazonaws.services.lambda.runtime.api.client.logging; -public enum FrameType { +import com.amazonaws.services.lambda.runtime.logging.LogFormat; +import com.amazonaws.services.lambda.runtime.logging.LogLevel; - LOG(0xa55a0001); +/** + * The first 4 bytes of the framing protocol is the Frame Type, that's made of a magic number (3 bytes) and 1 byte of flags. + * +-----------------------+ + * | Frame Type - 4 bytes | + * +-----------------------+ + * | a5 5a 00 | flgs | + * + - - - - - + - - - - - + + * \ bit | + * | view| + * +---------+ + + * | | + * v byte 3 v F - free + * +-+-+-+-+-+-+-+-+ J - { JsonLog = 0, PlainTextLog = 1 } + * |F|F|F|L|l|l|T|J| T - { NoTimeStamp = 0, TimeStampPresent = 1 } + * +-+-+-+-+-+-+-+-+ Lll -> Log Level in 3-bit binary (L-> most significant bit) + */ +public class FrameType { + private static final int LOG_MAGIC = 0xa55a0000; + private static final int OFFSET_LOG_FORMAT = 0; + private static final int OFFSET_TIMESTAMP_PRESENT = 1; + private static final int OFFSET_LOG_LEVEL = 2; private final int val; @@ -12,6 +36,13 @@ public enum FrameType { this.val = val; } + public static int getValue(LogLevel logLevel, LogFormat logFormat) { + return LOG_MAGIC + | (logLevel.ordinal() << OFFSET_LOG_LEVEL) + | (1 << OFFSET_TIMESTAMP_PRESENT) + | (logFormat.ordinal() << OFFSET_LOG_FORMAT); + } + public int getValue() { return this.val; } diff --git a/aws-lambda-java-runtime-interface-client/src/main/java/com/amazonaws/services/lambda/runtime/api/client/logging/FramedTelemetryLogSink.java b/aws-lambda-java-runtime-interface-client/src/main/java/com/amazonaws/services/lambda/runtime/api/client/logging/FramedTelemetryLogSink.java index 60f1d18f1..e297d1908 100644 --- a/aws-lambda-java-runtime-interface-client/src/main/java/com/amazonaws/services/lambda/runtime/api/client/logging/FramedTelemetryLogSink.java +++ b/aws-lambda-java-runtime-interface-client/src/main/java/com/amazonaws/services/lambda/runtime/api/client/logging/FramedTelemetryLogSink.java @@ -1,63 +1,82 @@ -/* Copyright 2019 Amazon.com, Inc. or its affiliates. All Rights Reserved. */ +/* +Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +SPDX-License-Identifier: Apache-2.0 +*/ package com.amazonaws.services.lambda.runtime.api.client.logging; -import java.io.File; +import com.amazonaws.services.lambda.runtime.logging.LogFormat; +import com.amazonaws.services.lambda.runtime.logging.LogLevel; +import java.io.FileDescriptor; import java.io.FileOutputStream; import java.io.IOException; import java.nio.ByteBuffer; import java.nio.ByteOrder; +import java.time.Instant; + /** * FramedTelemetryLogSink implements the logging contract between runtimes and the platform. It implements a simple * framing protocol so message boundaries can be determined. Each frame can be visualized as follows: * - *

+ * 
  * {@code
- * +----------------------+------------------------+-----------------------+
- * | Frame Type - 4 bytes | Length (len) - 4 bytes | Message - 'len' bytes |
- * +----------------------+------------------------+-----------------------+
+ * +----------------------+------------------------+---------------------+-----------------------+
+ * | Frame Type - 4 bytes | Length (len) - 4 bytes | Timestamp - 8 bytes | Message - 'len' bytes |
+ * +----------------------+------------------------+---------------------+-----------------------+
  * }
  * 
- * + *

* The first 4 bytes indicate the type of the frame - log frames have a type defined as the hex value 0xa55a0001. The - * second 4 bytes should indicate the message's length. The next 'len' bytes contain the message. The byte order is - * big-endian. + * second 4 bytes should indicate the message's length. The next 8 bytes contain UNIX timestamp of the message in + * microsecond accuracy. The next 'len' bytes contain the message. The byte order is big-endian. */ public class FramedTelemetryLogSink implements LogSink { - private static final int HEADER_LENGTH = 8; + private static final int HEADER_LENGTH = 16; private final FileOutputStream logOutputStream; private final ByteBuffer headerBuf; - public FramedTelemetryLogSink(File file) throws IOException { - this.logOutputStream = new FileOutputStream(file); + public FramedTelemetryLogSink(FileDescriptor fd) throws IOException { + this.logOutputStream = new FileOutputStream(fd); this.headerBuf = ByteBuffer.allocate(HEADER_LENGTH).order(ByteOrder.BIG_ENDIAN); } @Override - public synchronized void log(byte[] message) { + public synchronized void log(LogLevel logLevel, LogFormat logFormat, byte[] message) { try { - writeFrame(message); + writeFrame(logLevel, logFormat, message); } catch (IOException e) { e.printStackTrace(); } } - private void writeFrame(byte[] message) throws IOException { - updateHeader(message.length); + @Override + public void log(byte[] message) { + log(LogLevel.UNDEFINED, LogFormat.TEXT, message); + } + + private void writeFrame(LogLevel logLevel, LogFormat logFormat, byte[] message) throws IOException { + updateHeader(logLevel, logFormat, message.length); this.logOutputStream.write(this.headerBuf.array()); this.logOutputStream.write(message); } + private long timestamp() { + Instant instant = Instant.now(); + // microsecond precision + return instant.getEpochSecond() * 1_000_000 + instant.getNano() / 1000; + } + /** * Updates the header ByteBuffer with the provided length. The header comprises the frame type and message length. */ - private void updateHeader(int length) { + private void updateHeader(LogLevel logLevel, LogFormat logFormat, int length) { this.headerBuf.clear(); - this.headerBuf.putInt(FrameType.LOG.getValue()); + this.headerBuf.putInt(FrameType.getValue(logLevel, logFormat)); this.headerBuf.putInt(length); + this.headerBuf.putLong(timestamp()); this.headerBuf.flip(); } 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 new file mode 100644 index 000000000..f1051a216 --- /dev/null +++ b/aws-lambda-java-runtime-interface-client/src/main/java/com/amazonaws/services/lambda/runtime/api/client/logging/JsonLogFormatter.java @@ -0,0 +1,64 @@ +/* +Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +SPDX-License-Identifier: Apache-2.0 +*/ + +package com.amazonaws.services.lambda.runtime.api.client.logging; + +import com.amazonaws.services.lambda.runtime.api.client.api.LambdaContext; +import com.amazonaws.services.lambda.runtime.logging.LogLevel; +import com.amazonaws.services.lambda.runtime.serialization.PojoSerializer; +import com.amazonaws.services.lambda.runtime.serialization.factories.GsonFactory; +import java.io.ByteArrayOutputStream; +import java.nio.charset.StandardCharsets; +import java.time.LocalDateTime; +import java.time.ZoneId; +import java.time.format.DateTimeFormatter; + +public class JsonLogFormatter implements LogFormatter { + private static final DateTimeFormatter dateFormatter = + DateTimeFormatter. + ofPattern("yyyy-MM-dd'T'HH:mm:ss.SSS'Z'"). + withZone(ZoneId.of("UTC")); + private final PojoSerializer serializer = GsonFactory.getInstance().getSerializer(StructuredLogMessage.class); + + private ThreadLocal lambdaContext = new ThreadLocal<>(); + + @Override + public String format(String message, LogLevel logLevel) { + ByteArrayOutputStream stream = new ByteArrayOutputStream(); + StructuredLogMessage msg = createLogMessage(message, logLevel); + serializer.toJson(msg, stream); + stream.write('\n'); + return new String(stream.toByteArray(), StandardCharsets.UTF_8); + } + + private StructuredLogMessage createLogMessage(String message, LogLevel logLevel) { + StructuredLogMessage msg = new StructuredLogMessage(); + msg.timestamp = dateFormatter.format(LocalDateTime.now()); + msg.message = message; + msg.level = logLevel; + + LambdaContext lambdaContextForCurrentThread = lambdaContext.get(); + if (lambdaContextForCurrentThread != null) { + msg.AWSRequestId = lambdaContextForCurrentThread.getAwsRequestId(); + msg.tenantId = lambdaContextForCurrentThread.getTenantId(); + } + + return msg; + } + + + /** + * Function to set the context for every invocation. + * This way the logger will be able to attach additional information to the log packet. + */ + @Override + public void setLambdaContext(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/LambdaContextLogger.java b/aws-lambda-java-runtime-interface-client/src/main/java/com/amazonaws/services/lambda/runtime/api/client/logging/LambdaContextLogger.java index eaa2e3ea0..dd3569126 100644 --- a/aws-lambda-java-runtime-interface-client/src/main/java/com/amazonaws/services/lambda/runtime/api/client/logging/LambdaContextLogger.java +++ b/aws-lambda-java-runtime-interface-client/src/main/java/com/amazonaws/services/lambda/runtime/api/client/logging/LambdaContextLogger.java @@ -1,34 +1,40 @@ -/* Copyright 2019 Amazon.com, Inc. or its affiliates. All Rights Reserved. */ +/* +Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +SPDX-License-Identifier: Apache-2.0 +*/ package com.amazonaws.services.lambda.runtime.api.client.logging; -import com.amazonaws.services.lambda.runtime.LambdaLogger; - +import com.amazonaws.services.lambda.runtime.logging.LogFormat; +import com.amazonaws.services.lambda.runtime.logging.LogLevel; +import java.io.Closeable; +import java.io.IOException; import static java.nio.charset.StandardCharsets.UTF_8; -public class LambdaContextLogger implements LambdaLogger { +public class LambdaContextLogger extends AbstractLambdaLogger implements Closeable { // If a null string is passed in, replace it with "null", // replicating the behavior of System.out.println(null); private static final byte[] NULL_BYTES_VALUE = "null".getBytes(UTF_8); private final transient LogSink sink; - public LambdaContextLogger(LogSink sink) { + public LambdaContextLogger(LogSink sink, LogLevel logLevel, LogFormat logFormat) { + super(logLevel, logFormat); this.sink = sink; } - public void log(byte[] message) { + @Override + protected void logMessage(byte[] message, LogLevel logLevel) { if (message == null) { - message = NULL_BYTES_VALUE; + sink.log(logLevel, this.logFormat, NULL_BYTES_VALUE); + } else { + sink.log(logLevel, this.logFormat, message); } - sink.log(message); } - public void log(String message) { - if (message == null) { - this.log(NULL_BYTES_VALUE); - } else { - this.log(message.getBytes(UTF_8)); - } + @Override + public void close() throws IOException { + sink.close(); + } } diff --git a/aws-lambda-java-runtime-interface-client/src/main/java/com/amazonaws/services/lambda/runtime/api/client/logging/LogFiltering.java b/aws-lambda-java-runtime-interface-client/src/main/java/com/amazonaws/services/lambda/runtime/api/client/logging/LogFiltering.java new file mode 100644 index 000000000..a9bdec86c --- /dev/null +++ b/aws-lambda-java-runtime-interface-client/src/main/java/com/amazonaws/services/lambda/runtime/api/client/logging/LogFiltering.java @@ -0,0 +1,20 @@ +/* +Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +SPDX-License-Identifier: Apache-2.0 +*/ + +package com.amazonaws.services.lambda.runtime.api.client.logging; + +import com.amazonaws.services.lambda.runtime.logging.LogLevel; + +public class LogFiltering { + private final LogLevel minimumLogLevel; + + public LogFiltering(LogLevel minimumLogLevel) { + this.minimumLogLevel = minimumLogLevel; + } + + boolean isEnabled(LogLevel logLevel) { + return (logLevel == LogLevel.UNDEFINED || logLevel.ordinal() >= minimumLogLevel.ordinal()); + } +} diff --git a/aws-lambda-java-runtime-interface-client/src/main/java/com/amazonaws/services/lambda/runtime/api/client/logging/LogFormatter.java b/aws-lambda-java-runtime-interface-client/src/main/java/com/amazonaws/services/lambda/runtime/api/client/logging/LogFormatter.java new file mode 100644 index 000000000..283b52289 --- /dev/null +++ b/aws-lambda-java-runtime-interface-client/src/main/java/com/amazonaws/services/lambda/runtime/api/client/logging/LogFormatter.java @@ -0,0 +1,16 @@ +/* +Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +SPDX-License-Identifier: Apache-2.0 +*/ + +package com.amazonaws.services.lambda.runtime.api.client.logging; + +import com.amazonaws.services.lambda.runtime.api.client.api.LambdaContext; +import com.amazonaws.services.lambda.runtime.logging.LogLevel; + +public interface LogFormatter { + String format(String message, LogLevel logLevel); + + default void setLambdaContext(LambdaContext context) { + } +} diff --git a/aws-lambda-java-runtime-interface-client/src/main/java/com/amazonaws/services/lambda/runtime/api/client/logging/LogSink.java b/aws-lambda-java-runtime-interface-client/src/main/java/com/amazonaws/services/lambda/runtime/api/client/logging/LogSink.java index 11c3f92ce..769adb77d 100644 --- a/aws-lambda-java-runtime-interface-client/src/main/java/com/amazonaws/services/lambda/runtime/api/client/logging/LogSink.java +++ b/aws-lambda-java-runtime-interface-client/src/main/java/com/amazonaws/services/lambda/runtime/api/client/logging/LogSink.java @@ -1,11 +1,18 @@ -/* Copyright 2019 Amazon.com, Inc. or its affiliates. All Rights Reserved. */ +/* +Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +SPDX-License-Identifier: Apache-2.0 +*/ package com.amazonaws.services.lambda.runtime.api.client.logging; +import com.amazonaws.services.lambda.runtime.logging.LogFormat; +import com.amazonaws.services.lambda.runtime.logging.LogLevel; import java.io.Closeable; public interface LogSink extends Closeable { void log(byte[] message); + void log(LogLevel logLevel, LogFormat logFormat, byte[] message); + } 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 5487c5dd0..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 @@ -1,12 +1,21 @@ -/* Copyright 2019 Amazon.com, Inc. or its affiliates. All Rights Reserved. */ +/* +Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +SPDX-License-Identifier: Apache-2.0 +*/ package com.amazonaws.services.lambda.runtime.api.client.logging; +import com.amazonaws.services.lambda.runtime.logging.LogFormat; +import com.amazonaws.services.lambda.runtime.logging.LogLevel; import java.io.IOException; public class StdOutLogSink implements LogSink { @Override public void log(byte[] message) { + log(LogLevel.UNDEFINED, LogFormat.TEXT, message); + } + + public synchronized void log(LogLevel logLevel, LogFormat logFormat, byte[] message) { try { System.out.write(message); } catch (IOException e) { @@ -15,5 +24,6 @@ public void log(byte[] message) { } @Override - public void close() {} + public void close() { + } } diff --git a/aws-lambda-java-runtime-interface-client/src/main/java/com/amazonaws/services/lambda/runtime/api/client/logging/StructuredLogMessage.java b/aws-lambda-java-runtime-interface-client/src/main/java/com/amazonaws/services/lambda/runtime/api/client/logging/StructuredLogMessage.java new file mode 100644 index 000000000..0ae19961f --- /dev/null +++ b/aws-lambda-java-runtime-interface-client/src/main/java/com/amazonaws/services/lambda/runtime/api/client/logging/StructuredLogMessage.java @@ -0,0 +1,16 @@ +/* +Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +SPDX-License-Identifier: Apache-2.0 +*/ + +package com.amazonaws.services.lambda.runtime.api.client.logging; + +import com.amazonaws.services.lambda.runtime.logging.LogLevel; + +class StructuredLogMessage { + public String timestamp; + public String message; + public LogLevel level; + public String AWSRequestId; + public String tenantId; +} diff --git a/aws-lambda-java-runtime-interface-client/src/main/java/com/amazonaws/services/lambda/runtime/api/client/logging/TextLogFormatter.java b/aws-lambda-java-runtime-interface-client/src/main/java/com/amazonaws/services/lambda/runtime/api/client/logging/TextLogFormatter.java new file mode 100644 index 000000000..5424bd4bd --- /dev/null +++ b/aws-lambda-java-runtime-interface-client/src/main/java/com/amazonaws/services/lambda/runtime/api/client/logging/TextLogFormatter.java @@ -0,0 +1,32 @@ +/* +Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +SPDX-License-Identifier: Apache-2.0 +*/ + +package com.amazonaws.services.lambda.runtime.api.client.logging; + +import com.amazonaws.services.lambda.runtime.logging.LogLevel; +import java.util.HashMap; +import java.util.Map; + +public class TextLogFormatter implements LogFormatter { + private static final Map logLevelMapper = new HashMap() { + { + for (LogLevel logLevel: LogLevel.values()) { + put(logLevel, "[" + logLevel.toString() + "] "); + } + } + }; + + @Override + public String format(String message, LogLevel logLevel) { + if (logLevel == LogLevel.UNDEFINED) { + return message; + } + + return new StringBuilder(). + append(logLevelMapper. + get(logLevel)).append(message). + toString(); + } +} diff --git a/aws-lambda-java-runtime-interface-client/src/main/java/com/amazonaws/services/lambda/runtime/api/client/runtimeapi/DtoSerializers.java b/aws-lambda-java-runtime-interface-client/src/main/java/com/amazonaws/services/lambda/runtime/api/client/runtimeapi/DtoSerializers.java new file mode 100644 index 000000000..9f0045e0d --- /dev/null +++ b/aws-lambda-java-runtime-interface-client/src/main/java/com/amazonaws/services/lambda/runtime/api/client/runtimeapi/DtoSerializers.java @@ -0,0 +1,42 @@ +/* +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; + +import com.amazonaws.services.lambda.runtime.api.client.runtimeapi.dto.ErrorRequest; +import com.amazonaws.services.lambda.runtime.api.client.runtimeapi.dto.XRayErrorCause; +import com.amazonaws.services.lambda.runtime.serialization.PojoSerializer; +import com.amazonaws.services.lambda.runtime.serialization.factories.GsonFactory; +import java.io.ByteArrayOutputStream; +import java.io.IOException; + +public class DtoSerializers { + + public static byte[] serialize(ErrorRequest error) { + return serialize(error, SingletonHelper.LAMBDA_ERROR_SERIALIZER); + } + + public static byte[] serialize(XRayErrorCause xRayErrorCause) { + return serialize(xRayErrorCause, SingletonHelper.X_RAY_ERROR_CAUSE_SERIALIZER); + } + + private static byte[] serialize(T pojo, PojoSerializer serializer) { + try (ByteArrayOutputStream outputStream = new ByteArrayOutputStream()) { + serializer.toJson(pojo, outputStream); + return outputStream.toByteArray(); + } catch (IOException e) { + return null; + } + } + + /** + * Implementation of + * Initialization-on-demand holder idiom + * This way the serializers will be loaded lazily + */ + private static class SingletonHelper { + private static final PojoSerializer LAMBDA_ERROR_SERIALIZER = GsonFactory.getInstance().getSerializer(ErrorRequest.class); + private static final PojoSerializer X_RAY_ERROR_CAUSE_SERIALIZER = GsonFactory.getInstance().getSerializer(XRayErrorCause.class); + } +} diff --git a/aws-lambda-java-runtime-interface-client/src/main/java/com/amazonaws/services/lambda/runtime/api/client/runtimeapi/JniHelper.java b/aws-lambda-java-runtime-interface-client/src/main/java/com/amazonaws/services/lambda/runtime/api/client/runtimeapi/JniHelper.java new file mode 100644 index 000000000..349b4ab07 --- /dev/null +++ b/aws-lambda-java-runtime-interface-client/src/main/java/com/amazonaws/services/lambda/runtime/api/client/runtimeapi/JniHelper.java @@ -0,0 +1,66 @@ +/* +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; + +import java.io.FileNotFoundException; +import java.io.InputStream; +import java.nio.file.Files; +import java.nio.file.Paths; +import java.nio.file.StandardCopyOption; +import java.util.ArrayList; +import java.util.List; + +public class JniHelper { + + private static final String NATIVE_LIB_PATH = "/tmp/.libaws-lambda-jni.so"; + private static final String NATIVE_CLIENT_JNI_PROPERTY = "com.amazonaws.services.lambda.runtime.api.client.runtimeapi.NativeClient.JNI"; + + /** + * Unpacks JNI library from the JAR to a temporary location and tries to load it using System.load() + * Implementation based on AWS CRT + * (ref. ...) + * + * @param libsToTry - array of native libraries to try + */ + public static void load() { + String jniLib = System.getProperty(NATIVE_CLIENT_JNI_PROPERTY); + if (jniLib != null) { + System.load(jniLib); + } else { + String[] libsToTry = new String[]{ + "libaws-lambda-jni.linux-x86_64.so", + "libaws-lambda-jni.linux-aarch_64.so", + "libaws-lambda-jni.linux_musl-x86_64.so", + "libaws-lambda-jni.linux_musl-aarch_64.so" + }; + unpackAndLoad(libsToTry, NativeClient.class); + } + } + + private static void unpackAndLoad(String[] libsToTry, Class clazz) { + List errorMessages = new ArrayList<>(); + for (String libToTry : libsToTry) { + try (InputStream inputStream = clazz.getResourceAsStream( + Paths.get("/jni", libToTry).toString())) { + if (inputStream == null) { + throw new FileNotFoundException("Specified file not in the JAR: " + libToTry); + } + Files.copy(inputStream, Paths.get(NATIVE_LIB_PATH), StandardCopyOption.REPLACE_EXISTING); + System.load(NATIVE_LIB_PATH); + return; + } catch (UnsatisfiedLinkError | Exception e) { + errorMessages.add(e.getMessage()); + } + } + + for (int i = 0; i < libsToTry.length; ++i) { + System.err.println("Failed to load the native runtime interface client library " + + libsToTry[i] + + ". Exception: " + + errorMessages.get(i)); + } + System.exit(-1); + } +} diff --git a/aws-lambda-java-runtime-interface-client/src/main/java/com/amazonaws/services/lambda/runtime/api/client/runtimeapi/LambdaError.java b/aws-lambda-java-runtime-interface-client/src/main/java/com/amazonaws/services/lambda/runtime/api/client/runtimeapi/LambdaError.java new file mode 100644 index 000000000..cb59a8c00 --- /dev/null +++ b/aws-lambda-java-runtime-interface-client/src/main/java/com/amazonaws/services/lambda/runtime/api/client/runtimeapi/LambdaError.java @@ -0,0 +1,27 @@ +/* +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; + +import com.amazonaws.services.lambda.runtime.api.client.runtimeapi.dto.ErrorRequest; +import com.amazonaws.services.lambda.runtime.api.client.runtimeapi.dto.XRayErrorCause; + +public class LambdaError { + + public final ErrorRequest errorRequest; + + public final XRayErrorCause xRayErrorCause; + + public final RapidErrorType errorType; + + public LambdaError(ErrorRequest errorRequest, XRayErrorCause xRayErrorCause, RapidErrorType errorType) { + this.errorRequest = errorRequest; + this.xRayErrorCause = xRayErrorCause; + this.errorType = errorType; + } + + public LambdaError(ErrorRequest errorRequest, RapidErrorType errorType) { + this(errorRequest, null, errorType); + } +} 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 new file mode 100644 index 000000000..a62aeb9b8 --- /dev/null +++ b/aws-lambda-java-runtime-interface-client/src/main/java/com/amazonaws/services/lambda/runtime/api/client/runtimeapi/LambdaRuntimeApiClient.java @@ -0,0 +1,57 @@ +/* +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; + +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; + +/** + * Java interface for + * Lambda Runtime API + */ +public interface LambdaRuntimeApiClient { + + /** + * Report Init error + * @param error error to report + */ + void reportInitError(LambdaError error) throws IOException; + + /** + * Get next invocation + */ + InvocationRequest nextInvocation() throws IOException; + + /** + * Get next invocation with exponential backoff + */ + InvocationRequest nextInvocationWithExponentialBackoff(LambdaContextLogger lambdaLogger) throws Exception; + + /** + * Report invocation success + * @param requestId request id + * @param response byte array representing response + */ + void reportInvocationSuccess(String requestId, byte[] response) throws IOException; + + /** + * Report invocation error + * @param requestId request id + * @param error error to report + */ + void reportInvocationError(String requestId, LambdaError error) throws IOException; + + /** + * SnapStart endpoint to report that beforeCheckoint hooks were executed + */ + void restoreNext() throws IOException; + + /** + * SnapStart endpoint to report errors during afterRestore hooks execution + * @param error error to report + */ + void reportRestoreError(LambdaError error) throws IOException; +} 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 new file mode 100644 index 000000000..caca69aa7 --- /dev/null +++ b/aws-lambda-java-runtime-interface-client/src/main/java/com/amazonaws/services/lambda/runtime/api/client/runtimeapi/LambdaRuntimeApiClientImpl.java @@ -0,0 +1,235 @@ +/* +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; + +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; +import java.net.HttpURLConnection; +import java.net.MalformedURLException; +import java.net.URL; +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; + +public class LambdaRuntimeApiClientImpl implements LambdaRuntimeApiClient { + + static final String USER_AGENT = String.format( + "aws-lambda-java/%s-%s", + System.getProperty("java.vendor.version"), + LambdaRuntimeApiClientImpl.class.getPackage().getImplementationVersion()); + + private static final String DEFAULT_CONTENT_TYPE = "application/json"; + private static final String XRAY_ERROR_CAUSE_HEADER = "Lambda-Runtime-Function-XRay-Error-Cause"; + 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; + + public LambdaRuntimeApiClientImpl(String hostnameAndPort) { + Objects.requireNonNull(hostnameAndPort, "hostnameAndPort cannot be null"); + this.baseUrl = "http://" + hostnameAndPort; + this.invocationEndpoint = this.baseUrl + "/2018-06-01/runtime/invocation/"; + NativeClient.init(hostnameAndPort); + } + + @Override + public void reportInitError(LambdaError error) throws IOException { + String endpoint = this.baseUrl + "/2018-06-01/runtime/init/error"; + reportLambdaError(endpoint, error, XRAY_ERROR_CAUSE_MAX_HEADER_SIZE); + } + + @Override + 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); + } + + @Override + public void reportInvocationError(String requestId, LambdaError error) throws IOException { + String endpoint = invocationEndpoint + requestId + "/error"; + reportLambdaError(endpoint, error, XRAY_ERROR_CAUSE_MAX_HEADER_SIZE); + } + + @Override + public void restoreNext() throws IOException { + String endpoint = this.baseUrl + "/2018-06-01/runtime/restore/next"; + int responseCode = doGet(endpoint); + if (responseCode != HTTP_OK) { + throw new LambdaRuntimeClientException(endpoint, responseCode); + } + } + + @Override + public void reportRestoreError(LambdaError error) throws IOException { + String endpoint = this.baseUrl + "/2018-06-01/runtime/restore/error"; + reportLambdaError(endpoint, error, XRAY_ERROR_CAUSE_MAX_HEADER_SIZE); + } + + void reportLambdaError(String endpoint, LambdaError error, int maxXrayHeaderSize) throws IOException { + Map headers = new HashMap<>(); + headers.put(ERROR_TYPE_HEADER, error.errorType.getRapidError()); + + if (error.xRayErrorCause != null) { + byte[] xRayErrorCauseJson = DtoSerializers.serialize(error.xRayErrorCause); + if (xRayErrorCauseJson != null && xRayErrorCauseJson.length < maxXrayHeaderSize) { + headers.put(XRAY_ERROR_CAUSE_HEADER, new String(xRayErrorCauseJson)); + } + } + + byte[] payload = DtoSerializers.serialize(error.errorRequest); + int responseCode = doPost(endpoint, headers, payload); + if (responseCode != HTTP_ACCEPTED) { + throw new LambdaRuntimeClientException(endpoint, responseCode); + } + } + + private int doPost(String endpoint, + Map headers, + byte[] payload) throws IOException { + URL url = createUrl(endpoint); + HttpURLConnection conn = (HttpURLConnection) url.openConnection(); + conn.setRequestMethod("POST"); + conn.setRequestProperty("Content-Type", DEFAULT_CONTENT_TYPE); + conn.setRequestProperty("User-Agent", USER_AGENT); + + for (Map.Entry header : headers.entrySet()) { + conn.setRequestProperty(header.getKey(), header.getValue()); + } + + conn.setFixedLengthStreamingMode(payload.length); + conn.setDoOutput(true); + + try (OutputStream outputStream = conn.getOutputStream()) { + outputStream.write(payload); + } + + // get response code before closing the stream + int responseCode = conn.getResponseCode(); + // don't need to read the response, close stream to ensure connection re-use + closeInputStreamQuietly(conn); + + return responseCode; + } + + private int doGet(String endpoint) throws IOException { + URL url = createUrl(endpoint); + HttpURLConnection conn = (HttpURLConnection) url.openConnection(); + conn.setRequestMethod("GET"); + conn.setRequestProperty("User-Agent", USER_AGENT); + + int responseCode = conn.getResponseCode(); + closeInputStreamQuietly(conn); + + return responseCode; + } + + private URL createUrl(String endpoint) { + try { + return new URL(endpoint); + } catch (MalformedURLException e) { + throw new RuntimeException(e); + } + } + + private void closeInputStreamQuietly(HttpURLConnection conn) { + + InputStream inputStream; + try { + inputStream = conn.getInputStream(); + } catch (IOException e) { + return; + } + + if (inputStream == null) { + return; + } + try { + inputStream.close(); + } catch (IOException e) { + // ignore + } + } +} diff --git a/aws-lambda-java-runtime-interface-client/src/main/java/com/amazonaws/services/lambda/runtime/api/client/runtimeapi/LambdaRuntimeClient.java b/aws-lambda-java-runtime-interface-client/src/main/java/com/amazonaws/services/lambda/runtime/api/client/runtimeapi/LambdaRuntimeClient.java deleted file mode 100644 index 05aa50a1b..000000000 --- a/aws-lambda-java-runtime-interface-client/src/main/java/com/amazonaws/services/lambda/runtime/api/client/runtimeapi/LambdaRuntimeClient.java +++ /dev/null @@ -1,116 +0,0 @@ -package com.amazonaws.services.lambda.runtime.api.client.runtimeapi; - -import java.io.IOException; -import java.io.InputStream; -import java.io.OutputStream; -import java.net.HttpURLConnection; -import java.net.MalformedURLException; -import java.net.URL; -import java.util.Objects; - -import static java.net.HttpURLConnection.HTTP_ACCEPTED; -import static java.nio.charset.StandardCharsets.UTF_8; - -/** - * LambdaRuntimeClient is a client of the AWS Lambda Runtime HTTP API for custom runtimes. - * - * API definition can be found at https://docs.aws.amazon.com/lambda/latest/dg/runtimes-api.html - * - * Copyright (c) 2019 Amazon. All rights reserved. - */ -public class LambdaRuntimeClient { - - private final String hostname; - private final int port; - private final String invocationEndpoint; - - private static final String DEFAULT_CONTENT_TYPE = "application/json"; - private static final String XRAY_ERROR_CAUSE_HEADER = "Lambda-Runtime-Function-XRay-Error-Cause"; - private static final String ERROR_TYPE_HEADER = "Lambda-Runtime-Function-Error-Type"; - private static final int XRAY_ERROR_CAUSE_MAX_HEADER_SIZE = 1024 * 1024; // 1MiB - - public LambdaRuntimeClient(String hostnamePort) { - Objects.requireNonNull(hostnamePort, "hostnamePort cannot be null"); - String[] parts = hostnamePort.split(":"); - this.hostname = parts[0]; - this.port = Integer.parseInt(parts[1]); - this.invocationEndpoint = invocationEndpoint(); - } - - public InvocationRequest waitForNextInvocation() { - return NativeClient.next(); - } - - public void postInvocationResponse(String requestId, byte[] response) { - NativeClient.postInvocationResponse(requestId.getBytes(UTF_8), response); - } - - public void postInvocationError(String requestId, byte[] errorResponse, String errorType) throws IOException { - postInvocationError(requestId, errorResponse, errorType, null); - } - - public void postInvocationError(String requestId, byte[] errorResponse, String errorType, String errorCause) - throws IOException { - String endpoint = invocationErrorEndpoint(requestId); - post(endpoint, errorResponse, errorType, errorCause); - } - - public void postInitError(byte[] errorResponse, String errorType) throws IOException { - String endpoint = initErrorEndpoint(); - post(endpoint, errorResponse, errorType, null); - } - - private void post(String endpoint, byte[] errorResponse, String errorType, String errorCause) throws IOException { - URL url = createUrl(endpoint); - HttpURLConnection conn = (HttpURLConnection) url.openConnection(); - conn.setRequestMethod("POST"); - conn.setRequestProperty("Content-Type", DEFAULT_CONTENT_TYPE); - if(errorType != null && !errorType.isEmpty()) { - conn.setRequestProperty(ERROR_TYPE_HEADER, errorType); - } - if(errorCause != null && errorCause.getBytes().length < XRAY_ERROR_CAUSE_MAX_HEADER_SIZE) { - conn.setRequestProperty(XRAY_ERROR_CAUSE_HEADER, errorCause); - } - conn.setFixedLengthStreamingMode(errorResponse.length); - conn.setDoOutput(true); - try (OutputStream outputStream = conn.getOutputStream()) { - outputStream.write(errorResponse); - } - - int responseCode = conn.getResponseCode(); - if (responseCode != HTTP_ACCEPTED) { - throw new LambdaRuntimeClientException(endpoint, responseCode); - } - - // don't need to read the response, close stream to ensure connection re-use - closeQuietly(conn.getInputStream()); - } - - private String invocationEndpoint() { - return "http://" + hostname + ":" + port + "/2018-06-01/runtime/invocation/"; - } - - private String invocationErrorEndpoint(String requestId) { - return invocationEndpoint + requestId + "/error"; - } - - private String initErrorEndpoint() { - return "http://" + hostname + ":" + port + "/2018-06-01/runtime/init/error"; - } - - private URL createUrl(String endpoint) { - try { - return new URL(endpoint); - } catch (MalformedURLException e) { - throw new RuntimeException(e); - } - } - - private void closeQuietly(InputStream inputStream) { - if (inputStream == null) return; - try { - inputStream.close(); - } catch (IOException e) { - } - } -} diff --git a/aws-lambda-java-runtime-interface-client/src/main/java/com/amazonaws/services/lambda/runtime/api/client/runtimeapi/LambdaRuntimeClientException.java b/aws-lambda-java-runtime-interface-client/src/main/java/com/amazonaws/services/lambda/runtime/api/client/runtimeapi/LambdaRuntimeClientException.java index 1fc52d2fc..d9f0341ae 100644 --- a/aws-lambda-java-runtime-interface-client/src/main/java/com/amazonaws/services/lambda/runtime/api/client/runtimeapi/LambdaRuntimeClientException.java +++ b/aws-lambda-java-runtime-interface-client/src/main/java/com/amazonaws/services/lambda/runtime/api/client/runtimeapi/LambdaRuntimeClientException.java @@ -1,11 +1,11 @@ +/* +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; -/** - * Copyright (c) 2019 Amazon. All rights reserved. - */ public class LambdaRuntimeClientException extends RuntimeException { public LambdaRuntimeClientException(String message, int responseCode) { - super(message + "Response code: '" + responseCode + "'."); + super(message + " Response code: '" + responseCode + "'."); } - } 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/runtimeapi/NativeClient.java b/aws-lambda-java-runtime-interface-client/src/main/java/com/amazonaws/services/lambda/runtime/api/client/runtimeapi/NativeClient.java index f3cda8bcf..101aea4d0 100644 --- a/aws-lambda-java-runtime-interface-client/src/main/java/com/amazonaws/services/lambda/runtime/api/client/runtimeapi/NativeClient.java +++ b/aws-lambda-java-runtime-interface-client/src/main/java/com/amazonaws/services/lambda/runtime/api/client/runtimeapi/NativeClient.java @@ -1,50 +1,23 @@ -/* Copyright 2019 Amazon.com, Inc. or its affiliates. All Rights Reserved. */ - +/* +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; -import java.io.InputStream; -import java.nio.file.Files; -import java.nio.file.Paths; -import java.nio.file.StandardCopyOption; +import com.amazonaws.services.lambda.runtime.api.client.runtimeapi.dto.InvocationRequest; +import static com.amazonaws.services.lambda.runtime.api.client.runtimeapi.LambdaRuntimeApiClientImpl.USER_AGENT; /** - * This module defines the native Runtime Interface Client which is responsible for all HTTP + * This module defines the native Runtime Interface Client which is responsible for HTTP * interactions with the Runtime API. */ class NativeClient { - private static final String nativeLibPath = "/tmp/.aws-lambda-runtime-interface-client"; - private static final String[] libsToTry = { - "/aws-lambda-runtime-interface-client.glibc.so", - "/aws-lambda-runtime-interface-client.musl.so", - }; - private static final Throwable[] exceptions = new Throwable[libsToTry.length]; - static { - boolean loaded = false; - for (int i = 0; !loaded && i < libsToTry.length; ++i) { - try (InputStream lib = NativeClient.class.getResourceAsStream(libsToTry[i])) { - Files.copy(lib, Paths.get(nativeLibPath), StandardCopyOption.REPLACE_EXISTING); - System.load(nativeLibPath); - loaded = true; - } catch (UnsatisfiedLinkError e) { - exceptions[i] = e; - } catch (Exception e) { - exceptions[i] = e; - } - } - if (!loaded) { - for (int i = 0; i < libsToTry.length; ++i) { - System.err.printf("Failed to load the native runtime interface client library %s. Exception: %s\n", libsToTry[i], exceptions[i].getMessage()); - } - System.exit(-1); - } - String userAgent = String.format( - "aws-lambda-java/%s-%s" , - System.getProperty("java.vendor.version"), - NativeClient.class.getPackage().getImplementationVersion()); - initializeClient(userAgent.getBytes()); + static void init(String awsLambdaRuntimeApi) { + JniHelper.load(); + initializeClient(USER_AGENT.getBytes(), awsLambdaRuntimeApi.getBytes()); } - - static native void initializeClient(byte[] userAgent); + + static native void initializeClient(byte[] userAgent, byte[] awsLambdaRuntimeApi); static native InvocationRequest next(); diff --git a/aws-lambda-java-runtime-interface-client/src/main/java/com/amazonaws/services/lambda/runtime/api/client/runtimeapi/RapidErrorType.java b/aws-lambda-java-runtime-interface-client/src/main/java/com/amazonaws/services/lambda/runtime/api/client/runtimeapi/RapidErrorType.java new file mode 100644 index 000000000..b471ce3f5 --- /dev/null +++ b/aws-lambda-java-runtime-interface-client/src/main/java/com/amazonaws/services/lambda/runtime/api/client/runtimeapi/RapidErrorType.java @@ -0,0 +1,16 @@ +/* +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 enum RapidErrorType { + BadFunctionCode, + UserException, + BeforeCheckpointError, + AfterRestoreError; + + public String getRapidError() { + return "Runtime." + this; + } +} diff --git a/aws-lambda-java-runtime-interface-client/src/main/java/com/amazonaws/services/lambda/runtime/api/client/runtimeapi/converters/LambdaErrorConverter.java b/aws-lambda-java-runtime-interface-client/src/main/java/com/amazonaws/services/lambda/runtime/api/client/runtimeapi/converters/LambdaErrorConverter.java new file mode 100644 index 000000000..a2520bf74 --- /dev/null +++ b/aws-lambda-java-runtime-interface-client/src/main/java/com/amazonaws/services/lambda/runtime/api/client/runtimeapi/converters/LambdaErrorConverter.java @@ -0,0 +1,32 @@ +/* +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.converters; + +import com.amazonaws.services.lambda.runtime.api.client.UserFault; +import com.amazonaws.services.lambda.runtime.api.client.runtimeapi.dto.ErrorRequest; + +public class LambdaErrorConverter { + private LambdaErrorConverter() { + } + + public static ErrorRequest fromUserFault(UserFault userFault) { + // Not setting stacktrace for compatibility with legacy/native runtime + return new ErrorRequest(userFault.msg, userFault.exception, null); + } + + public static ErrorRequest fromThrowable(Throwable throwable) { + String errorMessage = throwable.getLocalizedMessage() == null + ? throwable.getClass().getName() + : throwable.getLocalizedMessage(); + String errorType = throwable.getClass().getName(); + + StackTraceElement[] trace = throwable.getStackTrace(); + String[] stackTrace = new String[trace.length]; + for (int i = 0; i < trace.length; i++) { + stackTrace[i] = trace[i].toString(); + } + return new ErrorRequest(errorMessage, errorType, stackTrace); + } +} diff --git a/aws-lambda-java-runtime-interface-client/src/main/java/com/amazonaws/services/lambda/runtime/api/client/runtimeapi/converters/XRayErrorCauseConverter.java b/aws-lambda-java-runtime-interface-client/src/main/java/com/amazonaws/services/lambda/runtime/api/client/runtimeapi/converters/XRayErrorCauseConverter.java new file mode 100644 index 000000000..7065bc764 --- /dev/null +++ b/aws-lambda-java-runtime-interface-client/src/main/java/com/amazonaws/services/lambda/runtime/api/client/runtimeapi/converters/XRayErrorCauseConverter.java @@ -0,0 +1,58 @@ +/* +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.converters; + +import com.amazonaws.services.lambda.runtime.api.client.runtimeapi.dto.StackElement; +import com.amazonaws.services.lambda.runtime.api.client.runtimeapi.dto.XRayErrorCause; +import com.amazonaws.services.lambda.runtime.api.client.runtimeapi.dto.XRayException; +import java.util.Arrays; +import java.util.Collection; +import java.util.Collections; +import java.util.List; +import java.util.stream.Collectors; + +public class XRayErrorCauseConverter { + private XRayErrorCauseConverter() { + } + + public static XRayErrorCause fromThrowable(Throwable throwable) { + String workingDirectory = System.getProperty("user.dir"); + XRayException xRayException = getXRayExceptionFromThrowable(throwable); + Collection exceptions = Collections.singletonList(xRayException); + Collection paths = Arrays.stream(throwable.getStackTrace()). + map(XRayErrorCauseConverter::determineFileName). + collect(Collectors.toSet()); + + return new XRayErrorCause(workingDirectory, exceptions, paths); + } + + static XRayException getXRayExceptionFromThrowable(Throwable throwable) { + String message = throwable.getMessage(); + String type = throwable.getClass().getName(); + List stack = Arrays.stream(throwable.getStackTrace()). + map(XRayErrorCauseConverter::convertStackTraceElement). + collect(Collectors.toList()); + return new XRayException(message, type, stack); + } + + static String determineFileName(StackTraceElement e) { + String fileName = null; + if (e.getFileName() != null) { + fileName = e.getFileName(); + } + if (fileName == null) { + String className = e.getClassName(); + fileName = className.substring(className.lastIndexOf('.') + 1) + ".java"; + } + return fileName; + } + + static StackElement convertStackTraceElement(StackTraceElement e) { + return new StackElement( + e.getMethodName(), + determineFileName(e), + e.getLineNumber()); + } +} diff --git a/aws-lambda-java-runtime-interface-client/src/main/java/com/amazonaws/services/lambda/runtime/api/client/runtimeapi/dto/ErrorRequest.java b/aws-lambda-java-runtime-interface-client/src/main/java/com/amazonaws/services/lambda/runtime/api/client/runtimeapi/dto/ErrorRequest.java new file mode 100644 index 000000000..d5886a67d --- /dev/null +++ b/aws-lambda-java-runtime-interface-client/src/main/java/com/amazonaws/services/lambda/runtime/api/client/runtimeapi/dto/ErrorRequest.java @@ -0,0 +1,21 @@ +/* +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.dto; + +public class ErrorRequest { + public String errorMessage; + public String errorType; + public String[] stackTrace; + + @SuppressWarnings("unused") + public ErrorRequest() { + } + + public ErrorRequest(String errorMessage, String errorType, String[] stackTrace) { + this.errorMessage = errorMessage; + this.errorType = errorType; + this.stackTrace = stackTrace; + } +} diff --git a/aws-lambda-java-runtime-interface-client/src/main/java/com/amazonaws/services/lambda/runtime/api/client/runtimeapi/InvocationRequest.java b/aws-lambda-java-runtime-interface-client/src/main/java/com/amazonaws/services/lambda/runtime/api/client/runtimeapi/dto/InvocationRequest.java similarity index 54% rename from aws-lambda-java-runtime-interface-client/src/main/java/com/amazonaws/services/lambda/runtime/api/client/runtimeapi/InvocationRequest.java rename to aws-lambda-java-runtime-interface-client/src/main/java/com/amazonaws/services/lambda/runtime/api/client/runtimeapi/dto/InvocationRequest.java index c8df04afd..656945b41 100644 --- a/aws-lambda-java-runtime-interface-client/src/main/java/com/amazonaws/services/lambda/runtime/api/client/runtimeapi/InvocationRequest.java +++ b/aws-lambda-java-runtime-interface-client/src/main/java/com/amazonaws/services/lambda/runtime/api/client/runtimeapi/dto/InvocationRequest.java @@ -1,12 +1,11 @@ -package com.amazonaws.services.lambda.runtime.api.client.runtimeapi; - -import java.io.ByteArrayInputStream; -import java.io.InputStream; +/* +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.dto; /** * An invocation request represents the response of the runtime API's next invocation API. - * - * Copyright (c) 2019 Amazon. All rights reserved. */ public class InvocationRequest { @@ -42,9 +41,9 @@ public class InvocationRequest { private String cognitoIdentity; /** - * An input stream of the invocation's request body. + * The tenant ID associated with the request. */ - private InputStream stream; + private String tenantId; private byte[] content; @@ -52,28 +51,67 @@ public String getId() { return id; } + public void setId(String id) { + this.id = id; + } + public String getXrayTraceId() { return xrayTraceId; } + public void setXrayTraceId(String xrayTraceId) { + this.xrayTraceId = xrayTraceId; + } + public String getInvokedFunctionArn() { return invokedFunctionArn; } + @SuppressWarnings("unused") + public void setInvokedFunctionArn(String invokedFunctionArn) { + this.invokedFunctionArn = invokedFunctionArn; + } + public long getDeadlineTimeInMs() { return deadlineTimeInMs; } + @SuppressWarnings("unused") + public void setDeadlineTimeInMs(long deadlineTimeInMs) { + this.deadlineTimeInMs = deadlineTimeInMs; + } + public String getClientContext() { return clientContext; } + @SuppressWarnings("unused") + public void setClientContext(String clientContext) { + this.clientContext = clientContext; + } + public String getCognitoIdentity() { return cognitoIdentity; } - public InputStream getContentAsStream() { - return new ByteArrayInputStream(content); + @SuppressWarnings("unused") + public void setCognitoIdentity(String cognitoIdentity) { + this.cognitoIdentity = cognitoIdentity; } + public String getTenantId() { + return tenantId; + } + + public void setTenantId(String tenantId) { + this.tenantId = tenantId; + } + + public byte[] getContent() { + return content; + } + + public void setContent(byte[] content) { + this.content = content; + } } diff --git a/aws-lambda-java-runtime-interface-client/src/main/java/com/amazonaws/services/lambda/runtime/api/client/runtimeapi/dto/StackElement.java b/aws-lambda-java-runtime-interface-client/src/main/java/com/amazonaws/services/lambda/runtime/api/client/runtimeapi/dto/StackElement.java new file mode 100644 index 000000000..679f8bf9f --- /dev/null +++ b/aws-lambda-java-runtime-interface-client/src/main/java/com/amazonaws/services/lambda/runtime/api/client/runtimeapi/dto/StackElement.java @@ -0,0 +1,21 @@ +/* +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.dto; + +public class StackElement { + public String label; + public String path; + public int line; + + @SuppressWarnings("unused") + public StackElement() { + } + + public StackElement(String label, String path, int line) { + this.label = label; + this.path = path; + this.line = line; + } +} diff --git a/aws-lambda-java-runtime-interface-client/src/main/java/com/amazonaws/services/lambda/runtime/api/client/runtimeapi/dto/XRayErrorCause.java b/aws-lambda-java-runtime-interface-client/src/main/java/com/amazonaws/services/lambda/runtime/api/client/runtimeapi/dto/XRayErrorCause.java new file mode 100644 index 000000000..cc5bee8a7 --- /dev/null +++ b/aws-lambda-java-runtime-interface-client/src/main/java/com/amazonaws/services/lambda/runtime/api/client/runtimeapi/dto/XRayErrorCause.java @@ -0,0 +1,24 @@ +/* +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.dto; + +import java.util.Collection; + +public class XRayErrorCause { + public String working_directory; + public Collection exceptions; + public Collection paths; + + @SuppressWarnings("unused") + public XRayErrorCause() { + + } + + public XRayErrorCause(String working_directory, Collection exceptions, Collection paths) { + this.working_directory = working_directory; + this.exceptions = exceptions; + this.paths = paths; + } +} diff --git a/aws-lambda-java-runtime-interface-client/src/main/java/com/amazonaws/services/lambda/runtime/api/client/runtimeapi/dto/XRayException.java b/aws-lambda-java-runtime-interface-client/src/main/java/com/amazonaws/services/lambda/runtime/api/client/runtimeapi/dto/XRayException.java new file mode 100644 index 000000000..2b17fd5f2 --- /dev/null +++ b/aws-lambda-java-runtime-interface-client/src/main/java/com/amazonaws/services/lambda/runtime/api/client/runtimeapi/dto/XRayException.java @@ -0,0 +1,23 @@ +/* +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.dto; + +import java.util.List; + +public class XRayException { + public String message; + public String type; + public List stack; + + @SuppressWarnings("unused") + public XRayException() { + } + + public XRayException(String message, String type, List stack) { + this.message = message; + this.type = type; + this.stack = stack; + } +} 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/java/com/amazonaws/services/lambda/runtime/api/client/util/EnvReader.java b/aws-lambda-java-runtime-interface-client/src/main/java/com/amazonaws/services/lambda/runtime/api/client/util/EnvReader.java index 968119aca..840bd440c 100644 --- a/aws-lambda-java-runtime-interface-client/src/main/java/com/amazonaws/services/lambda/runtime/api/client/util/EnvReader.java +++ b/aws-lambda-java-runtime-interface-client/src/main/java/com/amazonaws/services/lambda/runtime/api/client/util/EnvReader.java @@ -1,4 +1,7 @@ -/* Copyright 2019 Amazon.com, Inc. or its affiliates. All Rights Reserved. */ +/* +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; diff --git a/aws-lambda-java-runtime-interface-client/src/main/java/com/amazonaws/services/lambda/runtime/api/client/util/EnvWriter.java b/aws-lambda-java-runtime-interface-client/src/main/java/com/amazonaws/services/lambda/runtime/api/client/util/EnvWriter.java deleted file mode 100644 index ba090078e..000000000 --- a/aws-lambda-java-runtime-interface-client/src/main/java/com/amazonaws/services/lambda/runtime/api/client/util/EnvWriter.java +++ /dev/null @@ -1,85 +0,0 @@ -/* Copyright 2019 Amazon.com, Inc. or its affiliates. All Rights Reserved. */ - -package com.amazonaws.services.lambda.runtime.api.client.util; - -import com.amazonaws.services.lambda.runtime.api.client.ReservedRuntimeEnvironmentVariables; - -import java.lang.reflect.Field; -import java.util.Map; -import java.util.Optional; -import java.util.function.Consumer; - -public class EnvWriter implements AutoCloseable { - - private Map envMap; - private final Field field; - - public EnvWriter(EnvReader envReader) { - Map env = envReader.getEnv(); - try { - field = env.getClass().getDeclaredField("m"); - field.setAccessible(true); - @SuppressWarnings("unchecked") - Map map = (Map) field.get(env); - envMap = map; - } catch (Exception e) { - throw new RuntimeException(e); - } - } - - @Override - public void close() { - if (field != null) { - field.setAccessible(false); - } - } - - public void modifyEnv(Consumer> modifier) { - modifier.accept(envMap); - } - - public void unsetLambdaInternalEnv() { - modifyEnv(env -> env.remove("_LAMBDA_TELEMETRY_LOG_FD")); - } - - public void setupEnvironmentCredentials() { - modifyEnv((env) -> { - // AWS_ACCESS_KEY_ID, AWS_SECRET_ACCESS_KEY, AWS_SESSION_TOKEN are set by the runtime API daemon when - // executing the runtime's bootstrap. Ensure these are not empty values. - removeIfEmpty(env, ReservedRuntimeEnvironmentVariables.AWS_ACCESS_KEY_ID); - removeIfEmpty(env, ReservedRuntimeEnvironmentVariables.AWS_SECRET_ACCESS_KEY); - removeIfEmpty(env, ReservedRuntimeEnvironmentVariables.AWS_SESSION_TOKEN); - - // The AWS Java SDK supports two alternate keys for the aws access and secret keys for compatibility. - // These are not set by the runtime API daemon when executing a runtime's bootstrap so set them here. - addIfNotNull(env, "AWS_ACCESS_KEY", env.get(ReservedRuntimeEnvironmentVariables.AWS_ACCESS_KEY_ID)); - addIfNotNull(env, "AWS_SECRET_KEY", env.get(ReservedRuntimeEnvironmentVariables.AWS_SECRET_ACCESS_KEY)); - }); - } - - public void setupAwsExecutionEnv() { - executionEnvironmentForJavaVersion() - .ifPresent(val -> modifyEnv(env -> env.put(ReservedRuntimeEnvironmentVariables.AWS_EXECUTION_ENV, val))); - } - - private Optional executionEnvironmentForJavaVersion() { - String version = System.getProperty("java.version"); - if (version.startsWith("1.8")) { - return Optional.of("AWS_Lambda_java8"); - } else if (version.startsWith("11")) { - return Optional.of("AWS_Lambda_java11"); - } - return Optional.empty(); - } - - private void addIfNotNull(Map env, String key, String value) { - if (value != null && !value.isEmpty()) { - env.put(key, value); - } - } - - private void removeIfEmpty(Map env, String key) { - env.computeIfPresent(key, (k, v) -> v.isEmpty() ? null : v); - } - -} diff --git a/aws-lambda-java-runtime-interface-client/src/main/java/com/amazonaws/services/lambda/runtime/api/client/util/LambdaOutputStream.java b/aws-lambda-java-runtime-interface-client/src/main/java/com/amazonaws/services/lambda/runtime/api/client/util/LambdaOutputStream.java index 83ffcb9ac..22d01b0aa 100644 --- a/aws-lambda-java-runtime-interface-client/src/main/java/com/amazonaws/services/lambda/runtime/api/client/util/LambdaOutputStream.java +++ b/aws-lambda-java-runtime-interface-client/src/main/java/com/amazonaws/services/lambda/runtime/api/client/util/LambdaOutputStream.java @@ -1,9 +1,12 @@ -/* Copyright 2019 Amazon.com, Inc. or its affiliates. All Rights Reserved. */ +/* +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 java.io.OutputStream; import java.io.IOException; +import java.io.OutputStream; public class LambdaOutputStream extends OutputStream { private final OutputStream inner; @@ -14,7 +17,7 @@ public LambdaOutputStream(OutputStream inner) { @Override public void write(int b) throws IOException { - write(new byte[] {(byte)b}); + write(new byte[]{(byte) b}); } @Override @@ -25,6 +28,6 @@ public void write(byte[] bytes) throws IOException { @Override public void write(byte[] bytes, int offset, int length) throws IOException { - inner.write(bytes, offset, length); + inner.write(bytes, offset, length); } } diff --git a/aws-lambda-java-runtime-interface-client/src/main/java/com/amazonaws/services/lambda/runtime/api/client/util/UnsafeUtil.java b/aws-lambda-java-runtime-interface-client/src/main/java/com/amazonaws/services/lambda/runtime/api/client/util/UnsafeUtil.java index f9351c94c..f11d4357c 100644 --- a/aws-lambda-java-runtime-interface-client/src/main/java/com/amazonaws/services/lambda/runtime/api/client/util/UnsafeUtil.java +++ b/aws-lambda-java-runtime-interface-client/src/main/java/com/amazonaws/services/lambda/runtime/api/client/util/UnsafeUtil.java @@ -1,10 +1,12 @@ -/* Copyright 2019 Amazon.com, Inc. or its affiliates. All Rights Reserved. */ +/* +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 sun.misc.Unsafe; - import java.lang.reflect.Field; +import sun.misc.Unsafe; /** * Utilities for easy access to sun.misc.Unsafe diff --git a/aws-lambda-java-runtime-interface-client/src/main/jni/Dockerfile.glibc b/aws-lambda-java-runtime-interface-client/src/main/jni/Dockerfile.glibc index 9d46d2f96..1cfcfbb1d 100644 --- a/aws-lambda-java-runtime-interface-client/src/main/jni/Dockerfile.glibc +++ b/aws-lambda-java-runtime-interface-client/src/main/jni/Dockerfile.glibc @@ -1,32 +1,25 @@ -# we use centos 7 to build against glibc 2.17 -FROM centos:7 +FROM public.ecr.aws/amazonlinux/amazonlinux:2 ARG CURL_VERSION -# Add Corretto repository -RUN rpm --import https://yum.corretto.aws/corretto.key && \ - curl -L -o /etc/yum.repos.d/corretto.repo https://yum.corretto.aws/corretto.repo - -# aws-lambda-cpp requires cmake3, it's available in EPEL -RUN yum install -y epel-release RUN yum install -y \ cmake3 \ + tar \ + gzip \ make \ + patch \ gcc \ gcc-c++ \ - libstdc++-static \ - glibc-devel \ - gmp-devel \ - libmpc-devel \ - libtool \ - mpfr-devel \ - wget \ - java-1.8.0-amazon-corretto-devel + java-11-amazon-corretto # Install curl dependency COPY ./deps/curl-$CURL_VERSION.tar.gz /src/deps/ -RUN tar xzf /src/deps/curl-$CURL_VERSION.tar.gz -C /src/deps && mv /src/deps/curl-$CURL_VERSION /src/deps/curl -WORKDIR /src/deps/curl +COPY ./deps/curl_001_disable_wakeup.patch /src/deps/ + +RUN tar xzf /src/deps/curl-$CURL_VERSION.tar.gz -C /src/deps + +WORKDIR /src/deps/curl-$CURL_VERSION +RUN patch lib/multihandle.h ../curl_001_disable_wakeup.patch RUN ./configure \ --prefix $(pwd)/../artifacts \ --disable-shared \ @@ -38,7 +31,6 @@ RUN ./configure \ # Install aws-lambda-cpp dependency ADD ./deps/aws-lambda-cpp-* /src/deps/aws-lambda-cpp -RUN sed -i.bak 's/VERSION 3.9/VERSION 3.6/' /src/deps/aws-lambda-cpp/CMakeLists.txt RUN mkdir -p /src/deps/aws-lambda-cpp/build WORKDIR /src/deps/aws-lambda-cpp/build RUN cmake3 .. \ @@ -53,7 +45,7 @@ RUN cmake3 .. \ # Build native client ADD *.cpp *.h /src/ WORKDIR /src -ENV JAVA_HOME=/usr/lib/jvm/java-1.8.0-amazon-corretto +ENV JAVA_HOME=/usr/lib/jvm/java-11-amazon-corretto RUN /usr/bin/c++ -c \ -std=gnu++11 \ -fPIC \ @@ -61,12 +53,20 @@ RUN /usr/bin/c++ -c \ -I${JAVA_HOME}/include/linux \ -I ./deps/artifacts/include \ com_amazonaws_services_lambda_runtime_api_client_runtimeapi_NativeClient.cpp -o com_amazonaws_services_lambda_runtime_api_client_runtimeapi_NativeClient.o && \ + /usr/bin/c++ -c \ + -std=gnu++11 \ + -fPIC \ + -I${JAVA_HOME}/include \ + -I${JAVA_HOME}/include/linux \ + -I ./deps/artifacts/include \ + com_amazonaws_services_lambda_crac_DNSManager.cpp -o com_amazonaws_services_lambda_crac_DNSManager.o && \ /usr/bin/c++ -shared \ -std=gnu++11 \ - -o aws-lambda-runtime-interface-client.so com_amazonaws_services_lambda_runtime_api_client_runtimeapi_NativeClient.o \ + -o aws-lambda-runtime-interface-client.so com_amazonaws_services_lambda_runtime_api_client_runtimeapi_NativeClient.o com_amazonaws_services_lambda_crac_DNSManager.o \ -L ./deps/artifacts/lib64/ \ -L ./deps/artifacts/lib/ \ -laws-lambda-runtime \ -lcurl \ -static-libstdc++ \ - -lrt + -lrt \ + -O2 diff --git a/aws-lambda-java-runtime-interface-client/src/main/jni/Dockerfile.musl b/aws-lambda-java-runtime-interface-client/src/main/jni/Dockerfile.musl index d34253faf..64725c140 100644 --- a/aws-lambda-java-runtime-interface-client/src/main/jni/Dockerfile.musl +++ b/aws-lambda-java-runtime-interface-client/src/main/jni/Dockerfile.musl @@ -1,4 +1,4 @@ -FROM alpine:3 +FROM public.ecr.aws/docker/library/alpine:3 ARG CURL_VERSION @@ -10,13 +10,17 @@ RUN apk update && \ g++ \ gcc \ make \ - libexecinfo-dev \ + patch \ perl # Install curl dependency COPY ./deps/curl-$CURL_VERSION.tar.gz /src/deps/ -RUN tar xzf /src/deps/curl-$CURL_VERSION.tar.gz -C /src/deps && mv /src/deps/curl-$CURL_VERSION /src/deps/curl -WORKDIR /src/deps/curl +COPY ./deps/curl_001_disable_wakeup.patch /src/deps/ + +RUN tar xzf /src/deps/curl-$CURL_VERSION.tar.gz -C /src/deps + +WORKDIR /src/deps/curl-$CURL_VERSION +RUN patch lib/multihandle.h ../curl_001_disable_wakeup.patch RUN ./configure \ --prefix $(pwd)/../artifacts \ --disable-shared \ @@ -50,12 +54,19 @@ RUN /usr/bin/c++ -c \ -I${JAVA_HOME}/include/linux \ -I ./deps/artifacts/include \ com_amazonaws_services_lambda_runtime_api_client_runtimeapi_NativeClient.cpp -o com_amazonaws_services_lambda_runtime_api_client_runtimeapi_NativeClient.o && \ + /usr/bin/c++ -c \ + -std=gnu++11 \ + -fPIC \ + -I${JAVA_HOME}/include \ + -I${JAVA_HOME}/include/linux \ + -I ./deps/artifacts/include \ + com_amazonaws_services_lambda_crac_DNSManager.cpp -o com_amazonaws_services_lambda_crac_DNSManager.o && \ /usr/bin/c++ -shared \ -std=gnu++11 \ - -o aws-lambda-runtime-interface-client.so com_amazonaws_services_lambda_runtime_api_client_runtimeapi_NativeClient.o \ - -L ./deps/artifacts/lib64/ \ + -o aws-lambda-runtime-interface-client.so com_amazonaws_services_lambda_runtime_api_client_runtimeapi_NativeClient.o com_amazonaws_services_lambda_crac_DNSManager.o \ -L ./deps/artifacts/lib/ \ -laws-lambda-runtime \ -lcurl \ -static-libstdc++ \ - -static-libgcc + -static-libgcc \ + -O2 diff --git a/aws-lambda-java-runtime-interface-client/src/main/jni/build-jni-lib.sh b/aws-lambda-java-runtime-interface-client/src/main/jni/build-jni-lib.sh index cac239a6d..b7dbb5a80 100755 --- a/aws-lambda-java-runtime-interface-client/src/main/jni/build-jni-lib.sh +++ b/aws-lambda-java-runtime-interface-client/src/main/jni/build-jni-lib.sh @@ -1,16 +1,130 @@ -#!/bin/bash +#!/bin/bash -x # Copyright 2019 Amazon.com, Inc. or its affiliates. All Rights Reserved. set -euo pipefail SRC_DIR=$(dirname "$0") DST_DIR=${1} -CURL_VERSION=7.77.0 +MULTI_ARCH=${2} +BUILD_OS=${3} +BUILD_ARCH=${4} +CURL_VERSION=7.83.1 -# compile the native library -docker build -f "${SRC_DIR}/Dockerfile.glibc" --build-arg CURL_VERSION=${CURL_VERSION} -t lambda-java-jni-lib-glibc "${SRC_DIR}" -docker run --rm --entrypoint /bin/cat lambda-java-jni-lib-glibc /src/aws-lambda-runtime-interface-client.so > "${DST_DIR}"/classes/aws-lambda-runtime-interface-client.glibc.so +function get_docker_platform() { + arch=$1 -docker build -f "${SRC_DIR}/Dockerfile.musl" --build-arg CURL_VERSION=${CURL_VERSION} -t lambda-java-jni-lib-musl "${SRC_DIR}" -docker run --rm --entrypoint /bin/cat lambda-java-jni-lib-musl /src/aws-lambda-runtime-interface-client.so > "${DST_DIR}"/classes/aws-lambda-runtime-interface-client.musl.so + if [ "${arch}" == "x86_64" ]; then + echo "linux/amd64" + elif [ "${arch}" == "aarch_64" ]; then + echo "linux/arm64/v8" + else + echo "UNKNOWN_DOCKER_PLATFORM" + fi +} +function get_target_os() { + libc_impl=$1 + + if [ "${libc_impl}" == "glibc" ]; then + echo "linux" + elif [ "${libc_impl}" == "musl" ]; then + echo "linux_musl" + else + echo "UNKNOWN_OS" + fi +} + +function build_for_libc_arch() { + libc_impl=$1 + arch=$2 + artifact=$3 + + docker_platform=$(get_docker_platform ${arch}) + + echo "Compiling the native library with libc implementation \`${libc_impl}\` on architecture \`${arch}\` using Docker platform \`${docker_platform}\`" + + if [[ "${MULTI_ARCH}" == "true" ]]; then + docker build --platform="${docker_platform}" -f "${SRC_DIR}/Dockerfile.${libc_impl}" \ + --build-arg CURL_VERSION=${CURL_VERSION} "${SRC_DIR}" -o - \ + | tar -xOf - src/aws-lambda-runtime-interface-client.so > "${artifact}" + else + echo "multi-arch not requested, assuming this is a workaround to goofyness when docker buildx is enabled on Linux CI environments." + echo "enabling docker buildx often updates the docker api version, so assuming that docker cli is also too old to use --output type=tar, so doing alternative build-tag-run approach" + image_name="lambda-java-jni-lib-${libc_impl}-${arch}" + + # GitHub actions is using dockerx build under the hood. We need to pass --load option to be able to run the image + # This args is NOT part of the classic docker build command, so we need to check against a GitHub Action env var not to make local build crash. + if [[ "${GITHUB_RUN_ID:+isset}" == "isset" ]]; then + EXTRA_LOAD_ARG="--load" + else + EXTRA_LOAD_ARG="" + fi + + docker build --platform="${docker_platform}" \ + -t "${image_name}" \ + -f "${SRC_DIR}/Dockerfile.${libc_impl}" \ + --build-arg CURL_VERSION=${CURL_VERSION} "${SRC_DIR}" ${EXTRA_LOAD_ARG} + + echo "Docker image has been successfully built" + + docker run --rm --entrypoint /bin/cat "${image_name}" \ + /src/aws-lambda-runtime-interface-client.so > "${artifact}" + fi + + [ -f "${artifact}" ] + + # file -b ${artifact} produces lines like this: + # x86_64: ELF 64-bit LSB shared object, x86-64, version 1 (GNU/Linux), dynamically linked, BuildID[sha1]=582888b42da34895828e1281cbbae15d279175b7, not stripped + # aarch_64: ELF 64-bit LSB shared object, ARM aarch64, version 1 (GNU/Linux), dynamically linked, BuildID[sha1]=fa54218974fb2c17772b6acf22467a2c67a87011, not stripped + # we need to ensure it has the expected architecture in it + # + # cut -d "," -f2 will extract second field (' x86-64' or ' ARM aarch64') + # tr -d '-' removes '-', so we'll have (' x8664' or ' ARM aarch64') + # grep -q is for quiet mode, no output + # ${arch//_} removes '_' chars from the `aarch` variable, (aarch_64 => aarch64, x86_64 => x8664) + if ! file -b "${artifact}" | cut -d "," -f2 | tr -d '-' | grep -q "${arch//_}"; then + echo "${artifact} did not appear to be the correct architecture, check that Docker buildx is enabled" + exit 1 + fi +} + +function get_target_artifact() { + target_os=$1 + target_arch=$2 + + target_file="${DST_DIR}/classes/jni/libaws-lambda-jni.${target_os}-${target_arch}.so" + target_dir=$(dirname "$target_file") + mkdir -p "$target_dir" + echo "$target_file" +} + + + +if [ -n "$BUILD_OS" ] && [ -n "$BUILD_ARCH" ]; then + # build for the specified arch and libc implementation + libc_impl="glibc" + if [ "$BUILD_OS" == "linux_musl" ]; then + libc_impl="musl" + fi + target_artifact=$(get_target_artifact "$BUILD_OS" "$BUILD_ARCH") + build_for_libc_arch "$libc_impl" "$BUILD_ARCH" "$target_artifact" +else + # build for all architectures and libc implementations + declare -a ARCHITECTURES=("x86_64" "aarch_64") + declare -a LIBC_IMPLS=("glibc" "musl") + + for arch in "${ARCHITECTURES[@]}"; do + + if [[ "${MULTI_ARCH}" != "true" ]] && [[ "$(arch)" != "${arch}" ]]; then + echo "multi arch build not requested and host arch is $(arch), so skipping ${arch}..." + continue + fi + + for libc_impl in "${LIBC_IMPLS[@]}"; do + target_os=$(get_target_os $libc_impl) + target_artifact=$(get_target_artifact "$target_os" "$arch") + build_for_libc_arch "$libc_impl" "$arch" "$target_artifact" + done + + done +fi diff --git a/aws-lambda-java-runtime-interface-client/src/main/jni/com_amazonaws_services_lambda_crac_DNSManager.cpp b/aws-lambda-java-runtime-interface-client/src/main/jni/com_amazonaws_services_lambda_crac_DNSManager.cpp new file mode 100644 index 000000000..ccf5481b9 --- /dev/null +++ b/aws-lambda-java-runtime-interface-client/src/main/jni/com_amazonaws_services_lambda_crac_DNSManager.cpp @@ -0,0 +1,27 @@ +/* +Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +SPDX-License-Identifier: Apache-2.0 +*/ +#include +#include "macro.h" +#include "com_amazonaws_services_lambda_crac_DNSManager.h" + +JNIEXPORT void JNICALL Java_com_amazonaws_services_lambda_crac_DNSManager_clearCache + (JNIEnv *env, jclass thisClass) { + jclass iNetAddressClass; + jclass concurrentMap; + jfieldID cacheFieldID; + jobject cacheObj; + jmethodID clearMethodID; + CHECK_EXCEPTION(env, iNetAddressClass = env->FindClass("java/net/InetAddress")); + CHECK_EXCEPTION(env, concurrentMap = env->FindClass("java/util/concurrent/ConcurrentMap")); + CHECK_EXCEPTION(env, cacheFieldID = env->GetStaticFieldID(iNetAddressClass, "cache", "Ljava/util/concurrent/ConcurrentMap;")); + CHECK_EXCEPTION(env, cacheObj = (jobject) env->GetStaticObjectField(iNetAddressClass, cacheFieldID)); + CHECK_EXCEPTION(env, clearMethodID = env->GetMethodID(concurrentMap, "clear", "()V")); + CHECK_EXCEPTION(env, env->CallVoidMethod(cacheObj, clearMethodID)); + return; + + ERROR: + // we need to fail silently here + env->ExceptionClear(); +} diff --git a/aws-lambda-java-runtime-interface-client/src/main/jni/com_amazonaws_services_lambda_crac_DNSManager.h b/aws-lambda-java-runtime-interface-client/src/main/jni/com_amazonaws_services_lambda_crac_DNSManager.h new file mode 100644 index 000000000..f26639ba9 --- /dev/null +++ b/aws-lambda-java-runtime-interface-client/src/main/jni/com_amazonaws_services_lambda_crac_DNSManager.h @@ -0,0 +1,19 @@ +/* +Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +SPDX-License-Identifier: Apache-2.0 +*/ +#include + +#ifndef _Included_com_amazonaws_services_lambda_crac_DNSManager +#define _Included_com_amazonaws_services_lambda_crac_DNSManager +#ifdef __cplusplus +extern "C" { +#endif + +JNIEXPORT void JNICALL Java_com_amazonaws_services_lambda_crac_DNSManager_clearCache + (JNIEnv *, jclass); + +#ifdef __cplusplus +} +#endif +#endif diff --git a/aws-lambda-java-runtime-interface-client/src/main/jni/com_amazonaws_services_lambda_runtime_api_client_runtimeapi_NativeClient.cpp b/aws-lambda-java-runtime-interface-client/src/main/jni/com_amazonaws_services_lambda_runtime_api_client_runtimeapi_NativeClient.cpp index c6a1dabcf..f06796616 100644 --- a/aws-lambda-java-runtime-interface-client/src/main/jni/com_amazonaws_services_lambda_runtime_api_client_runtimeapi_NativeClient.cpp +++ b/aws-lambda-java-runtime-interface-client/src/main/jni/com_amazonaws_services_lambda_runtime_api_client_runtimeapi_NativeClient.cpp @@ -1,29 +1,59 @@ /* - * Copyright 2019-present Amazon.com, Inc. or its affiliates. All Rights Reserved. - * - * Licensed under the Apache License, Version 2.0 (the "License"). - * You may not use this file except in compliance with the License. - * A copy of the License is located at - * - * http://aws.amazon.com/apache2.0 - * - * or in the "license" file accompanying this file. This file is distributed - * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either - * express or implied. See the License for the specific language governing - * permissions and limitations under the License. - */ +Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +SPDX-License-Identifier: Apache-2.0 +*/ #include +#include "macro.h" #include "com_amazonaws_services_lambda_runtime_api_client_runtimeapi_NativeClient.h" #include "aws/lambda-runtime/runtime.h" #include "aws/lambda-runtime/version.h" -#define CHECK_EXCEPTION(env, expr) \ - expr; \ - if ((env)->ExceptionOccurred()) \ - goto ERROR; - static aws::lambda_runtime::runtime * CLIENT = nullptr; +static jint JNI_VERSION = JNI_VERSION_1_8; + +static jclass invocationRequestClass; +static jfieldID invokedFunctionArnField; +static jfieldID deadlineTimeInMsField; +static jfieldID idField; +static jfieldID contentField; +static jfieldID clientContextField; +static jfieldID cognitoIdentityField; +static jfieldID xrayTraceIdField; +static jfieldID tenantIdField; + + +jint JNI_OnLoad(JavaVM* vm, void* reserved) { + + JNIEnv* env; + if (vm->GetEnv(reinterpret_cast(&env), JNI_VERSION) != JNI_OK) { + return JNI_ERR; + } + + jclass tempInvocationRequestClassRef; + tempInvocationRequestClassRef = env->FindClass("com/amazonaws/services/lambda/runtime/api/client/runtimeapi/dto/InvocationRequest"); + invocationRequestClass = (jclass) env->NewGlobalRef(tempInvocationRequestClassRef); + env->DeleteLocalRef(tempInvocationRequestClassRef); + + idField = env->GetFieldID(invocationRequestClass , "id", "Ljava/lang/String;"); + invokedFunctionArnField = env->GetFieldID(invocationRequestClass , "invokedFunctionArn", "Ljava/lang/String;"); + deadlineTimeInMsField = env->GetFieldID(invocationRequestClass , "deadlineTimeInMs", "J"); + contentField = env->GetFieldID(invocationRequestClass , "content", "[B"); + xrayTraceIdField = env->GetFieldID(invocationRequestClass , "xrayTraceId", "Ljava/lang/String;"); + clientContextField = env->GetFieldID(invocationRequestClass , "clientContext", "Ljava/lang/String;"); + cognitoIdentityField = env->GetFieldID(invocationRequestClass , "cognitoIdentity", "Ljava/lang/String;"); + tenantIdField = env->GetFieldID(invocationRequestClass, "tenantId", "Ljava/lang/String;"); + + return JNI_VERSION; +} + +void JNI_OnUnload(JavaVM *vm, void *reserved) { + JNIEnv* env; + vm->GetEnv(reinterpret_cast(&env), JNI_VERSION); + + env->DeleteGlobalRef(invocationRequestClass); +} + static void throwLambdaRuntimeClientException(JNIEnv *env, std::string message, aws::http::response_code responseCode){ jclass lambdaRuntimeExceptionClass = env->FindClass("com/amazonaws/services/lambda/runtime/api/client/runtimeapi/LambdaRuntimeClientException"); jstring jMessage = env->NewStringUTF(message.c_str()); @@ -41,9 +71,9 @@ static std::string toNativeString(JNIEnv *env, jbyteArray jArray) { return nativeString; } -JNIEXPORT void JNICALL Java_com_amazonaws_services_lambda_runtime_api_client_runtimeapi_NativeClient_initializeClient(JNIEnv *env, jobject thisObject, jbyteArray userAgent) { +JNIEXPORT void JNICALL Java_com_amazonaws_services_lambda_runtime_api_client_runtimeapi_NativeClient_initializeClient(JNIEnv *env, jobject thisObject, jbyteArray userAgent, jbyteArray awsLambdaRuntimeApi) { std::string user_agent = toNativeString(env, userAgent); - std::string endpoint(getenv("AWS_LAMBDA_RUNTIME_API")); + std::string endpoint = toNativeString(env, awsLambdaRuntimeApi); CLIENT = new aws::lambda_runtime::runtime(endpoint, user_agent); } @@ -56,50 +86,35 @@ JNIEXPORT jobject JNICALL Java_com_amazonaws_services_lambda_runtime_api_client_ return NULL; } - jclass invocationRequestClass; - jfieldID invokedFunctionArnField; - jfieldID deadlineTimeInMsField; - jfieldID xrayTraceIdField; - jfieldID idField; jobject invocationRequest; - jfieldID clientContextField; - jfieldID cognitoIdentityField; jbyteArray jArray; const jbyte* bytes; - jfieldID contentField; auto response = outcome.get_result(); - CHECK_EXCEPTION(env, invocationRequestClass = env->FindClass("com/amazonaws/services/lambda/runtime/api/client/runtimeapi/InvocationRequest")); CHECK_EXCEPTION(env, invocationRequest = env->AllocObject(invocationRequestClass)); - - CHECK_EXCEPTION(env, idField = env->GetFieldID(invocationRequestClass , "id", "Ljava/lang/String;")); CHECK_EXCEPTION(env, env->SetObjectField(invocationRequest, idField, env->NewStringUTF(response.request_id.c_str()))); - - CHECK_EXCEPTION(env, invokedFunctionArnField = env->GetFieldID(invocationRequestClass , "invokedFunctionArn", "Ljava/lang/String;")); CHECK_EXCEPTION(env, env->SetObjectField(invocationRequest, invokedFunctionArnField, env->NewStringUTF(response.function_arn.c_str()))); - - CHECK_EXCEPTION(env, deadlineTimeInMsField = env->GetFieldID(invocationRequestClass , "deadlineTimeInMs", "J")); CHECK_EXCEPTION(env, env->SetLongField(invocationRequest, deadlineTimeInMsField, std::chrono::duration_cast(response.deadline.time_since_epoch()).count())); if(response.xray_trace_id != ""){ - CHECK_EXCEPTION(env, xrayTraceIdField = env->GetFieldID(invocationRequestClass , "xrayTraceId", "Ljava/lang/String;")); CHECK_EXCEPTION(env, env->SetObjectField(invocationRequest, xrayTraceIdField, env->NewStringUTF(response.xray_trace_id.c_str()))); } if(response.client_context != ""){ - CHECK_EXCEPTION(env, clientContextField = env->GetFieldID(invocationRequestClass , "clientContext", "Ljava/lang/String;")); CHECK_EXCEPTION(env, env->SetObjectField(invocationRequest, clientContextField, env->NewStringUTF(response.client_context.c_str()))); } if(response.cognito_identity != ""){ - CHECK_EXCEPTION(env, cognitoIdentityField = env->GetFieldID(invocationRequestClass , "cognitoIdentity", "Ljava/lang/String;")); CHECK_EXCEPTION(env, env->SetObjectField(invocationRequest, cognitoIdentityField, env->NewStringUTF(response.cognito_identity.c_str()))); } + if(response.tenant_id != ""){ + CHECK_EXCEPTION(env, env->SetObjectField(invocationRequest, tenantIdField, env->NewStringUTF(response.tenant_id.c_str()))); + } + bytes = reinterpret_cast(response.payload.c_str()); CHECK_EXCEPTION(env, jArray = env->NewByteArray(response.payload.length())); CHECK_EXCEPTION(env, env->SetByteArrayRegion(jArray, 0, response.payload.length(), bytes)); - CHECK_EXCEPTION(env, contentField = env->GetFieldID(invocationRequestClass , "content", "[B")); CHECK_EXCEPTION(env, env->SetObjectField(invocationRequest, contentField, jArray)); return invocationRequest; diff --git a/aws-lambda-java-runtime-interface-client/src/main/jni/com_amazonaws_services_lambda_runtime_api_client_runtimeapi_NativeClient.h b/aws-lambda-java-runtime-interface-client/src/main/jni/com_amazonaws_services_lambda_runtime_api_client_runtimeapi_NativeClient.h index 28a6f444a..7219109b0 100644 --- a/aws-lambda-java-runtime-interface-client/src/main/jni/com_amazonaws_services_lambda_runtime_api_client_runtimeapi_NativeClient.h +++ b/aws-lambda-java-runtime-interface-client/src/main/jni/com_amazonaws_services_lambda_runtime_api_client_runtimeapi_NativeClient.h @@ -1,3 +1,7 @@ +/* +Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +SPDX-License-Identifier: Apache-2.0 +*/ #include #ifndef _Included_com_amazonaws_services_lambda_runtime_api_client_runtimeapi_NativeClient @@ -7,7 +11,7 @@ extern "C" { #endif JNIEXPORT void JNICALL Java_com_amazonaws_services_lambda_runtime_api_client_runtimeapi_NativeClient_initializeClient - (JNIEnv *, jobject, jbyteArray); + (JNIEnv *, jobject, jbyteArray, jbyteArray); JNIEXPORT jobject JNICALL Java_com_amazonaws_services_lambda_runtime_api_client_runtimeapi_NativeClient_next (JNIEnv *, jobject); 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 94e1e22cb..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 @@ -61,6 +61,11 @@ struct invocation_request { */ std::chrono::time_point deadline; + /** + * Tenant ID of the current invocation. + */ + std::string tenant_id; + /** * The number of milliseconds left before lambda terminates the current execution. */ @@ -167,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 91750840f..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 @@ -40,6 +40,8 @@ static constexpr auto CLIENT_CONTEXT_HEADER = "lambda-runtime-client-context"; 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, @@ -162,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 } @@ -226,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); @@ -246,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); } @@ -301,6 +302,10 @@ runtime::next_outcome runtime::get_next() req.payload.c_str(), static_cast(req.get_time_remaining().count())); } + + if (resp.has_header(TENANT_ID_HEADER)) { + req.tenant_id = resp.get_header(TENANT_ID_HEADER); + } return next_outcome(req); } @@ -322,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; @@ -343,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) { @@ -361,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/main/jni/deps/curl-7.77.0.tar.gz b/aws-lambda-java-runtime-interface-client/src/main/jni/deps/curl-7.77.0.tar.gz deleted file mode 100644 index 951f34b72..000000000 Binary files a/aws-lambda-java-runtime-interface-client/src/main/jni/deps/curl-7.77.0.tar.gz and /dev/null differ diff --git a/aws-lambda-java-runtime-interface-client/src/main/jni/deps/curl-7.83.1.tar.gz b/aws-lambda-java-runtime-interface-client/src/main/jni/deps/curl-7.83.1.tar.gz new file mode 100644 index 000000000..b71926a37 Binary files /dev/null and b/aws-lambda-java-runtime-interface-client/src/main/jni/deps/curl-7.83.1.tar.gz differ diff --git a/aws-lambda-java-runtime-interface-client/src/main/jni/deps/curl_001_disable_wakeup.patch b/aws-lambda-java-runtime-interface-client/src/main/jni/deps/curl_001_disable_wakeup.patch new file mode 100644 index 000000000..1bb067054 --- /dev/null +++ b/aws-lambda-java-runtime-interface-client/src/main/jni/deps/curl_001_disable_wakeup.patch @@ -0,0 +1,14 @@ +diff --git a/multihandle.h b/multihandle.h +index a26fb619a..18080f1c3 100644 +--- a/multihandle.h ++++ b/multihandle.h +@@ -70,10 +70,6 @@ typedef enum { + + #define CURLPIPE_ANY (CURLPIPE_MULTIPLEX) + +-#if !defined(CURL_DISABLE_SOCKETPAIR) +-#define ENABLE_WAKEUP +-#endif +- + /* value for MAXIMUM CONCURRENT STREAMS upper limit */ + #define INITIAL_MAX_CONCURRENT_STREAMS ((1U << 31) - 1) \ No newline at end of file diff --git a/aws-lambda-java-runtime-interface-client/src/main/jni/macro.h b/aws-lambda-java-runtime-interface-client/src/main/jni/macro.h new file mode 100644 index 000000000..df5759afe --- /dev/null +++ b/aws-lambda-java-runtime-interface-client/src/main/jni/macro.h @@ -0,0 +1,14 @@ +/* +Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +SPDX-License-Identifier: Apache-2.0 +*/ + +#ifndef _Included_macros +#define _Included_macros + +#define CHECK_EXCEPTION(env, expr) \ + expr; \ + if ((env)->ExceptionOccurred()) \ + goto ERROR; + +#endif diff --git a/aws-lambda-java-runtime-interface-client/src/test/java/com/amazonaws/services/lambda/crac/ContextImplTest.java b/aws-lambda-java-runtime-interface-client/src/test/java/com/amazonaws/services/lambda/crac/ContextImplTest.java new file mode 100644 index 000000000..7a7653dc2 --- /dev/null +++ b/aws-lambda-java-runtime-interface-client/src/test/java/com/amazonaws/services/lambda/crac/ContextImplTest.java @@ -0,0 +1,314 @@ +/* +Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +SPDX-License-Identifier: Apache-2.0 +*/ + +package com.amazonaws.services.lambda.crac; + +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.condition.DisabledOnOs; +import org.junit.jupiter.api.condition.OS; +import org.mockito.ArgumentMatchers; +import org.mockito.InOrder; +import org.mockito.Mockito; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.junit.jupiter.api.Assertions.fail; +import static org.mockito.Mockito.doThrow; + +import com.amazonaws.services.lambda.runtime.api.client.runtimeapi.JniHelper; + +@DisabledOnOs(OS.MAC) +public class ContextImplTest { + + private Resource throwsWithSuppressedException, noop, noop2, throwsException, throwCustomException; + + @BeforeAll + public static void jniLoad() { + JniHelper.load(); + } + + @BeforeEach + public void setup() throws Exception { + + throwsWithSuppressedException = Mockito.mock(Resource.class); + CheckpointException checkpointException = new CheckpointException(); + checkpointException.addSuppressed(new NumberFormatException()); + + RestoreException restoreException = new RestoreException(); + restoreException.addSuppressed(new NumberFormatException()); + + doThrow(checkpointException).when(throwsWithSuppressedException).beforeCheckpoint(ArgumentMatchers.any()); + doThrow(restoreException).when(throwsWithSuppressedException).afterRestore(ArgumentMatchers.any()); + + noop = Mockito.mock(Resource.class); + Mockito.doNothing().when(noop).beforeCheckpoint(ArgumentMatchers.any()); + Mockito.doNothing().when(noop).afterRestore(ArgumentMatchers.any()); + + noop2 = Mockito.mock(Resource.class); + Mockito.doNothing().when(noop2).beforeCheckpoint(ArgumentMatchers.any()); + Mockito.doNothing().when(noop2).afterRestore(ArgumentMatchers.any()); + + throwsException = Mockito.mock(Resource.class); + doThrow(CheckpointException.class).when(throwsException).beforeCheckpoint(ArgumentMatchers.any()); + doThrow(RestoreException.class).when(throwsException).afterRestore(ArgumentMatchers.any()); + + throwCustomException = Mockito.mock(Resource.class); + doThrow(IndexOutOfBoundsException.class).when(throwCustomException).beforeCheckpoint(ArgumentMatchers.any()); + doThrow(UnsupportedOperationException.class).when(throwCustomException).afterRestore(ArgumentMatchers.any()); + + Core.resetGlobalContext(); + } + + static class StatefulResource implements Resource { + + int state = 0; + + @Override + public void afterRestore(Context context) { + state += 1; + } + + @Override + public void beforeCheckpoint(Context context) { + state += 2; + } + + int getValue() { + return state; + } + } + + static int GLOBAL_STATE; + + static class ChangeGlobalStateResource implements Resource { + + ChangeGlobalStateResource() { + GLOBAL_STATE = 0; + } + + @Override + public void afterRestore(Context context) { + GLOBAL_STATE += 1; + } + + @Override + public void beforeCheckpoint(Context context) { + GLOBAL_STATE += 2; + } + } + + /** + * Happy path test with real / not mocked resource + */ + @Test + public void verifyHooksWereExecuted() throws CheckpointException, RestoreException { + StatefulResource resource = new StatefulResource(); + Core.getGlobalContext().register(resource); + + Core.getGlobalContext().beforeCheckpoint(null); + Core.getGlobalContext().afterRestore(null); + + assertEquals(3, resource.getValue()); + } + + /** + * This test is to validate GC intervention + */ + @Test + public void verifyHooksWereExecutedWithGC() throws CheckpointException, RestoreException { + StatefulResource resource = new StatefulResource(); + Core.getGlobalContext().register(resource); + gcAndSleep(); + + Core.getGlobalContext().beforeCheckpoint(null); + Core.getGlobalContext().afterRestore(null); + + assertEquals(3, resource.getValue()); + } + + @Test + public void verifyHooksAreNotExecutedForGarbageCollectedResources() throws CheckpointException, RestoreException { + Core.getGlobalContext().register(new ChangeGlobalStateResource()); + gcAndSleep(); + + Core.getGlobalContext().beforeCheckpoint(null); + Core.getGlobalContext().afterRestore(null); + + + assertEquals(0, GLOBAL_STATE); + } + + private static void gcAndSleep() { + for (int i = 0; i < 10; i++) { + System.gc(); + } + + try { + Thread.sleep(1000); + } catch (InterruptedException e) { + System.out.println("thread was interrupted"); + throw new RuntimeException(e); + } + } + + @Test + public void Should_NotifyResourcesInReverseOrderOfRegistration_When_CheckpointNotification() throws Exception { + // Given + InOrder checkpointNotificationOrder = Mockito.inOrder(noop, noop2); + Core.getGlobalContext().register(noop); + Core.getGlobalContext().register(noop2); + + // When + Core.getGlobalContext().beforeCheckpoint(null); + + // Then + checkpointNotificationOrder.verify(noop2).beforeCheckpoint(ArgumentMatchers.any()); + checkpointNotificationOrder.verify(noop).beforeCheckpoint(ArgumentMatchers.any()); + } + + @Test + public void Should_NotifyResourcesInOrderOfRegistration_When_RestoreNotification() throws Exception { + // Given + InOrder restoreNotificationOrder = Mockito.inOrder(noop, noop2); + Core.getGlobalContext().register(noop); + Core.getGlobalContext().register(noop2); + + // When + Core.getGlobalContext().afterRestore(null); + + // Then + restoreNotificationOrder.verify(noop).afterRestore(ArgumentMatchers.any()); + restoreNotificationOrder.verify(noop2).afterRestore(ArgumentMatchers.any()); + } + + @Test + public void Should_ResourcesAreAlwaysNotified_When_AnyNotificationThrowsException() throws Exception { + + // Given + Core.getGlobalContext().register(throwsWithSuppressedException); + Core.getGlobalContext().register(noop); + Core.getGlobalContext().register(noop2); + Core.getGlobalContext().register(throwsException); + Core.getGlobalContext().register(throwCustomException); + + // When + try { + Core.getGlobalContext().beforeCheckpoint(null); + } catch (Exception ignored) { + } + + try { + Core.getGlobalContext().afterRestore(null); + } catch (Exception ignored) { + } + + // Then + Mockito.verify(throwsWithSuppressedException, Mockito.times(1)).beforeCheckpoint(ArgumentMatchers.any()); + Mockito.verify(noop, Mockito.times(1)).beforeCheckpoint(ArgumentMatchers.any()); + Mockito.verify(noop2, Mockito.times(1)).beforeCheckpoint(ArgumentMatchers.any()); + Mockito.verify(throwsException, Mockito.times(1)).beforeCheckpoint(ArgumentMatchers.any()); + Mockito.verify(throwCustomException, Mockito.times(1)).beforeCheckpoint(ArgumentMatchers.any()); + + Mockito.verify(throwsWithSuppressedException, Mockito.times(1)).afterRestore(ArgumentMatchers.any()); + Mockito.verify(noop, Mockito.times(1)).afterRestore(ArgumentMatchers.any()); + Mockito.verify(noop2, Mockito.times(1)).afterRestore(ArgumentMatchers.any()); + Mockito.verify(throwsException, Mockito.times(1)).afterRestore(ArgumentMatchers.any()); + Mockito.verify(throwCustomException, Mockito.times(1)).afterRestore(ArgumentMatchers.any()); + } + + @Test + public void Should_CatchAndSuppressAnyExceptionsAsCheckpointException_When_CheckpointNotification() { + // Given + Core.getGlobalContext().register(throwsWithSuppressedException); + Core.getGlobalContext().register(throwCustomException); + + // When + try { + Core.getGlobalContext().beforeCheckpoint(null); + } catch (CheckpointException e1) { + // Then + assertEquals(2, e1.getSuppressed().length); + } catch (Throwable e2) { + fail("All exceptions thrown during checkpoint notification should be reported as CheckpointException"); + } + } + + @Test + public void Should_CatchAndSuppressAnyExceptionsAsRestoreException_When_RestoreNotification() { + // Given + Core.getGlobalContext().register(throwsWithSuppressedException); + Core.getGlobalContext().register(throwCustomException); + + // When + try { + Core.getGlobalContext().afterRestore(null); + } catch (RestoreException e1) { + // Then + assertEquals(2, e1.getSuppressed().length); + } catch (Exception e2) { + fail("All exceptions thrown during restore notification should be reported as RestoreException"); + } + } + + @Test + public void Should_SuppressOriginalCheckpointExceptionUnderAnotherCheckpointException_When_ResourceIsAContext() throws Exception { + // Given + Context c0 = Mockito.mock(Context.class); + doThrow(CheckpointException.class).when(c0).beforeCheckpoint(ArgumentMatchers.any()); + + Core.getGlobalContext().register(c0); + + // When + try { + Core.getGlobalContext().beforeCheckpoint(null); + } catch (CheckpointException e1) { + // Then + assertEquals(1, e1.getSuppressed().length); + assertTrue(e1.getSuppressed()[0] instanceof CheckpointException, + "When the Resource is a Context and it throws CheckpointException it should be suppressed under another CheckpointException"); + + } catch (Exception e2) { + fail("All exceptions thrown during checkpoint notification should be reported as CheckpointException"); + } + } + + @Test + public void Should_SuppressOriginalRestoreExceptionUnderAnotherRestoreException_When_ResourceIsAContext() throws Exception { + // Given + Context c0 = Mockito.mock(Context.class); + doThrow(RestoreException.class).when(c0).afterRestore(ArgumentMatchers.any()); + + Core.getGlobalContext().register(c0); + + // When + try { + Core.getGlobalContext().afterRestore(null); + } catch (RestoreException e1) { + // Then + assertEquals(1, e1.getSuppressed().length); + assertTrue(e1.getSuppressed()[0] instanceof RestoreException, + "When the Resource is a Context and it throws RestoreException it should be suppressed under another RestoreException"); + } catch (Exception e2) { + fail("All exceptions thrown during restore notification should be reported as RestoreException"); + } + } + + @Test + public void Should_NotifyOnlyOnce_When_ResourceRegistersMultipleTimes() throws Exception { + // Given + Core.getGlobalContext().register(noop); + Core.getGlobalContext().register(noop); + + // When + Core.getGlobalContext().beforeCheckpoint(null); + Core.getGlobalContext().afterRestore(null); + + // Then + Mockito.verify(noop, Mockito.times(1)).beforeCheckpoint(ArgumentMatchers.any()); + Mockito.verify(noop, Mockito.times(1)).afterRestore(ArgumentMatchers.any()); + } +} diff --git a/aws-lambda-java-runtime-interface-client/src/test/java/com/amazonaws/services/lambda/crac/DNSCacheManagerTest.java b/aws-lambda-java-runtime-interface-client/src/test/java/com/amazonaws/services/lambda/crac/DNSCacheManagerTest.java new file mode 100644 index 000000000..5eb6f749f --- /dev/null +++ b/aws-lambda-java-runtime-interface-client/src/test/java/com/amazonaws/services/lambda/crac/DNSCacheManagerTest.java @@ -0,0 +1,124 @@ +/* +Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +SPDX-License-Identifier: Apache-2.0 +*/ + +package com.amazonaws.services.lambda.crac; + +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.condition.DisabledOnOs; +import org.junit.jupiter.api.condition.OS; + +import com.amazonaws.services.lambda.runtime.api.client.runtimeapi.JniHelper; + +import java.util.Map; +import java.net.InetAddress; +import java.net.UnknownHostException; +import java.lang.reflect.Field; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.fail; + +@DisabledOnOs(OS.MAC) +public class DNSCacheManagerTest { + + static String CACHE_FIELD_NAME = "cache"; + + // this should have no effect, as the DNS cache is cleared explicitly in these tests + static { + java.security.Security.setProperty("networkaddress.cache.ttl" , "10000"); + java.security.Security.setProperty("networkaddress.cache.negative.ttl" , "10000"); + } + + @BeforeAll + public static void jniLoad() { + JniHelper.load(); + } + + @BeforeEach + public void setup() { + Core.resetGlobalContext(); + DNSManager.clearCache(); + } + + static class StatefulResource implements Resource { + + int state = 0; + + @Override + public void afterRestore(Context context) { + state += 1; + } + + @Override + public void beforeCheckpoint(Context context) { + state += 2; + } + + int getValue() { + return state; + } + } + + @Test + public void positiveDnsCacheShouldBeEmpty() throws CheckpointException, RestoreException, UnknownHostException, ReflectiveOperationException { + int baselineDNSEntryCount = getDNSEntryCount(); + + StatefulResource resource = new StatefulResource(); + Core.getGlobalContext().register(resource); + + String[] hosts = {"www.stackoverflow.com", "www.amazon.com", "www.yahoo.com"}; + for(String singleHost : hosts) { + InetAddress address = InetAddress.getByName(singleHost); + } + // n hosts -> n DNS entries + assertEquals(hosts.length, getDNSEntryCount() - baselineDNSEntryCount); + + // this should call the native static method clearDNSCache + Core.getGlobalContext().beforeCheckpoint(null); + + // cache should be cleared + assertEquals(0, getDNSEntryCount()); + } + + @Test + public void negativeDnsCacheShouldBeEmpty() throws CheckpointException, RestoreException, UnknownHostException, ReflectiveOperationException { + int baselineDNSEntryCount = getDNSEntryCount(); + + StatefulResource resource = new StatefulResource(); + Core.getGlobalContext().register(resource); + + String invalidHost = "not.a.valid.host"; + try { + InetAddress address = InetAddress.getByName(invalidHost); + fail(); + } catch(UnknownHostException uhe) { + // this is actually fine + } + + // 1 host -> 1 DNS entry + assertEquals(1, getDNSEntryCount() - baselineDNSEntryCount); + + // this should the native static method clearDNSCache + Core.getGlobalContext().beforeCheckpoint(null); + + // cache should be cleared + assertEquals(0, getDNSEntryCount()); + } + + // helper functions to access the cache via reflection (see maven-surefire-plugin command args) + protected static Map getDNSCache() throws ReflectiveOperationException { + Class klass = InetAddress.class; + Field acf = klass.getDeclaredField(CACHE_FIELD_NAME); + acf.setAccessible(true); + Object addressCache = acf.get(null); + return (Map) acf.get(addressCache); + } + + protected static int getDNSEntryCount() throws ReflectiveOperationException { + Map cache = getDNSCache(); + return cache.size(); + } +} 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/ClasspathLoaderTest.java b/aws-lambda-java-runtime-interface-client/src/test/java/com/amazonaws/services/lambda/runtime/api/client/ClasspathLoaderTest.java new file mode 100644 index 000000000..38147d219 --- /dev/null +++ b/aws-lambda-java-runtime-interface-client/src/test/java/com/amazonaws/services/lambda/runtime/api/client/ClasspathLoaderTest.java @@ -0,0 +1,153 @@ +/* +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 org.junit.jupiter.api.Test; +import org.junit.jupiter.api.io.TempDir; +import java.io.File; +import java.io.FileNotFoundException; +import java.io.FileOutputStream; +import java.io.IOException; +import java.nio.file.Path; +import java.util.Collections; +import java.util.Enumeration; +import java.util.jar.JarEntry; +import java.util.jar.JarFile; +import java.util.jar.JarOutputStream; + +import static org.junit.jupiter.api.Assertions.*; + +class ClasspathLoaderTest { + + @Test + void testLoadAllClassesWithNoClasspath() throws IOException { + String originalClasspath = System.getProperty("java.class.path"); + try { + System.clearProperty("java.class.path"); + ClasspathLoader.main(new String[]{}); + } finally { + if (originalClasspath != null) { + System.setProperty("java.class.path", originalClasspath); + } + } + } + + @Test + void testLoadAllClassesWithEmptyClasspath() { + String originalClasspath = System.getProperty("java.class.path"); + try { + System.setProperty("java.class.path", ""); + assertThrows(FileNotFoundException.class, () -> + ClasspathLoader.main(new String[]{})); + } finally { + if (originalClasspath != null) { + System.setProperty("java.class.path", originalClasspath); + } + } + } + + @Test + void testLoadAllClassesWithInvalidPath() { + String originalClasspath = System.getProperty("java.class.path"); + try { + System.setProperty("java.class.path", "nonexistent/path"); + assertThrows(FileNotFoundException.class, () -> + ClasspathLoader.main(new String[]{})); + } finally { + if (originalClasspath != null) { + System.setProperty("java.class.path", originalClasspath); + } + } + } + + @Test + void testLoadAllClassesWithValidJar(@TempDir Path tempDir) throws IOException { + File jarFile = createSimpleJar(tempDir, "test.jar", "TestClass"); + String originalClasspath = System.getProperty("java.class.path"); + try { + System.setProperty("java.class.path", jarFile.getAbsolutePath()); + ClasspathLoader.main(new String[]{}); + } finally { + if (originalClasspath != null) { + System.setProperty("java.class.path", originalClasspath); + } + } + } + + @Test + void testLoadAllClassesWithDirectory(@TempDir Path tempDir) throws IOException { + String originalClasspath = System.getProperty("java.class.path"); + try { + System.setProperty("java.class.path", tempDir.toString()); + ClasspathLoader.main(new String[]{}); + } finally { + if (originalClasspath != null) { + System.setProperty("java.class.path", originalClasspath); + } + } + } + + @Test + void testLoadAllClassesWithMultipleEntries(@TempDir Path tempDir) throws IOException { + File jarFile1 = createSimpleJar(tempDir, "test1.jar", "TestClass1"); + File jarFile2 = createSimpleJar(tempDir, "test2.jar", "TestClass2"); + + String originalClasspath = System.getProperty("java.class.path"); + try { + String newClasspath = jarFile1.getAbsolutePath() + + File.pathSeparator + + jarFile2.getAbsolutePath(); + System.setProperty("java.class.path", newClasspath); + ClasspathLoader.main(new String[]{}); + } finally { + if (originalClasspath != null) { + System.setProperty("java.class.path", originalClasspath); + } + } + } + + @Test + void testLoadAllClassesWithBlocklistedClass(@TempDir Path tempDir) throws IOException { + File jarFile = tempDir.resolve("blocklist-test.jar").toFile(); + + try (JarOutputStream jos = new JarOutputStream(new FileOutputStream(jarFile))) { + JarEntry blockedEntry = new JarEntry("META-INF/versions/9/module-info.class"); + jos.putNextEntry(blockedEntry); + jos.write("dummy content".getBytes()); + jos.closeEntry(); + + JarEntry normalEntry = new JarEntry("com/test/Normal.class"); + jos.putNextEntry(normalEntry); + jos.write("dummy content".getBytes()); + jos.closeEntry(); + } + + String originalClasspath = System.getProperty("java.class.path"); + try { + System.setProperty("java.class.path", jarFile.getAbsolutePath()); + ClasspathLoader.main(new String[]{}); + // The test passes if no exception is thrown and the blocklisted class is skipped + } finally { + if (originalClasspath != null) { + System.setProperty("java.class.path", originalClasspath); + } + } + } + + private File createSimpleJar(Path tempDir, String jarName, String className) throws IOException { + File jarFile = tempDir.resolve(jarName).toFile(); + + try (JarOutputStream jos = new JarOutputStream(new FileOutputStream(jarFile))) { + // Add a simple non-class file to make it a valid jar + JarEntry entry = new JarEntry("com/test/" + className + ".txt"); + jos.putNextEntry(entry); + jos.write("test content".getBytes()); + jos.closeEntry(); + } + + return jarFile; + } +} diff --git a/aws-lambda-java-runtime-interface-client/src/test/java/com/amazonaws/services/lambda/runtime/api/client/CustomerClassLoaderTest.java b/aws-lambda-java-runtime-interface-client/src/test/java/com/amazonaws/services/lambda/runtime/api/client/CustomerClassLoaderTest.java index 0169d0d6a..71fb013f3 100644 --- a/aws-lambda-java-runtime-interface-client/src/test/java/com/amazonaws/services/lambda/runtime/api/client/CustomerClassLoaderTest.java +++ b/aws-lambda-java-runtime-interface-client/src/test/java/com/amazonaws/services/lambda/runtime/api/client/CustomerClassLoaderTest.java @@ -1,4 +1,7 @@ -/* Copyright 2019 Amazon.com, Inc. or its affiliates. All Rights Reserved. */ +/* +Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +SPDX-License-Identifier: Apache-2.0 +*/ package com.amazonaws.services.lambda.runtime.api.client; 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 new file mode 100644 index 000000000..aae2f1afe --- /dev/null +++ b/aws-lambda-java-runtime-interface-client/src/test/java/com/amazonaws/services/lambda/runtime/api/client/EventHandlerLoaderTest.java @@ -0,0 +1,152 @@ +package com.amazonaws.services.lambda.runtime.api.client; + +import com.amazonaws.services.lambda.runtime.api.client.runtimeapi.dto.InvocationRequest; +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 { + + @Test + void RequestHandlerTest() throws Exception { + String handler = "test.lambda.handlers.RequestHandlerImpl"; + LambdaRequestHandler lambdaRequestHandler = getLambdaRequestHandler(handler); + assertSuccessfulInvocation(lambdaRequestHandler); + } + + @Test + void RequestStreamHandlerTest() throws Exception { + String handler = "test.lambda.handlers.RequestStreamHandlerImpl"; + LambdaRequestHandler lambdaRequestHandler = getLambdaRequestHandler(handler); + assertSuccessfulInvocation(lambdaRequestHandler); + } + + @Test + void PojoHandlerTest_noParams() throws Exception { + String handler = "test.lambda.handlers.POJOHanlderImpl::noParamsHandler"; + LambdaRequestHandler lambdaRequestHandler = getLambdaRequestHandler(handler); + assertSuccessfulInvocation(lambdaRequestHandler); + } + + @Test + void PojoHandlerTest_oneParamEvent() throws Exception { + String handler = "test.lambda.handlers.POJOHanlderImpl::oneParamHandler_event"; + LambdaRequestHandler lambdaRequestHandler = getLambdaRequestHandler(handler); + assertSuccessfulInvocation(lambdaRequestHandler); + } + + @Test + void PojoHandlerTest_oneParamContext() throws Exception { + String handler = "test.lambda.handlers.POJOHanlderImpl::oneParamHandler_context"; + LambdaRequestHandler lambdaRequestHandler = getLambdaRequestHandler(handler); + assertSuccessfulInvocation(lambdaRequestHandler); + } + + @Test + void PojoHandlerTest_twoParams() throws Exception { + String handler = "test.lambda.handlers.POJOHanlderImpl::twoParamsHandler"; + LambdaRequestHandler lambdaRequestHandler = getLambdaRequestHandler(handler); + assertSuccessfulInvocation(lambdaRequestHandler); + } + + private LambdaRequestHandler getLambdaRequestHandler(String handler) throws ClassNotFoundException { + ClassLoader cl = this.getClass().getClassLoader(); + HandlerInfo handlerInfo = HandlerInfo.fromString(handler, cl); + return EventHandlerLoader.loadEventHandler(handlerInfo); + } + + private static void assertSuccessfulInvocation(LambdaRequestHandler lambdaRequestHandler) throws Exception { + InvocationRequest invocationRequest = getTestInvocationRequest(); + + ByteArrayOutputStream resultBytes = lambdaRequestHandler.call(invocationRequest); + String result = resultBytes.toString(); + + assertEquals("\"success\"", result); + } + + private static InvocationRequest getTestInvocationRequest() { + InvocationRequest invocationRequest = new InvocationRequest(); + invocationRequest.setContent("\"Hello\"".getBytes()); + invocationRequest.setId("id"); + invocationRequest.setXrayTraceId("traceId"); + return invocationRequest; + } + + // 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/FailureTest.java b/aws-lambda-java-runtime-interface-client/src/test/java/com/amazonaws/services/lambda/runtime/api/client/FailureTest.java deleted file mode 100644 index d7cc22254..000000000 --- a/aws-lambda-java-runtime-interface-client/src/test/java/com/amazonaws/services/lambda/runtime/api/client/FailureTest.java +++ /dev/null @@ -1,64 +0,0 @@ -/* Copyright 2019 Amazon.com, Inc. or its affiliates. All Rights Reserved. */ - -package com.amazonaws.services.lambda.runtime.api.client; - -import org.junit.jupiter.api.Test; - -import java.io.IOError; - -import static org.junit.jupiter.api.Assertions.assertEquals; - -public class FailureTest { - static class MySecretException extends ClassNotFoundException {} - - @Test - public void doesNotReportCustomException() { - Throwable throwable = new MySecretException(); - assertEquals(ClassNotFoundException.class, Failure.getReportableExceptionClass(throwable)); - - MySecretException mySecretException = new MySecretException(); - assertEquals(ClassNotFoundException.class, Failure.getReportableExceptionClass(mySecretException)); - } - - @Test - public void correctlyReportsExceptionsWeTrack() { - Throwable ioError = new IOError(new Throwable()); - assertEquals(IOError.class, Failure.getReportableExceptionClass(ioError)); - - Throwable error = new Error(new Throwable()); - assertEquals(Error.class, Failure.getReportableExceptionClass(error)); - - ClassNotFoundException classNotFoundException = new ClassNotFoundException(); - assertEquals(ClassNotFoundException.class, Failure.getReportableExceptionClass(classNotFoundException)); - - VirtualMachineError virtualMachineError = new OutOfMemoryError(); - assertEquals(VirtualMachineError.class, Failure.getReportableExceptionClass(virtualMachineError)); - - Throwable linkageError = new LinkageError(); - assertEquals(LinkageError.class, Failure.getReportableExceptionClass(linkageError)); - - Throwable exceptionInInitializerError = new ExceptionInInitializerError(); - assertEquals(ExceptionInInitializerError.class, Failure.getReportableExceptionClass(exceptionInInitializerError)); - - Throwable noClassDefFoundError = new NoClassDefFoundError(); - assertEquals(NoClassDefFoundError.class, Failure.getReportableExceptionClass(noClassDefFoundError)); - - Throwable invalidHandlerException = new HandlerInfo.InvalidHandlerException(); - assertEquals(HandlerInfo.InvalidHandlerException.class, Failure.getReportableExceptionClass(invalidHandlerException)); - - Throwable throwable = new Throwable(); - assertEquals(Throwable.class, Failure.getReportableExceptionClass(throwable)); - } - - @Test - public void verifyCorrectClassName() { - Throwable ioError = new IOError(new Throwable()); - assertEquals("java.io.IOError", Failure.getReportableExceptionClassName(ioError)); - - Throwable error = new Error(new Throwable()); - assertEquals("java.lang.Error", Failure.getReportableExceptionClassName(error)); - - ClassNotFoundException classNotFoundException = new ClassNotFoundException(); - assertEquals("java.lang.ClassNotFoundException", Failure.getReportableExceptionClassName(classNotFoundException)); - } -} diff --git a/aws-lambda-java-runtime-interface-client/src/test/java/com/amazonaws/services/lambda/runtime/api/client/HandlerInfoTest.java b/aws-lambda-java-runtime-interface-client/src/test/java/com/amazonaws/services/lambda/runtime/api/client/HandlerInfoTest.java new file mode 100644 index 000000000..e134ddc8c --- /dev/null +++ b/aws-lambda-java-runtime-interface-client/src/test/java/com/amazonaws/services/lambda/runtime/api/client/HandlerInfoTest.java @@ -0,0 +1,132 @@ +/* +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 org.junit.jupiter.api.Test; +import static org.junit.jupiter.api.Assertions.*; + +class HandlerInfoTest { + + @Test + void testConstructor() { + Class testClass = String.class; + String methodName = "testMethod"; + + HandlerInfo info = new HandlerInfo(testClass, methodName); + + assertNotNull(info); + assertEquals(testClass, info.clazz); + assertEquals(methodName, info.methodName); + } + + @Test + void testFromStringWithoutMethod() throws Exception { + String handler = "java.lang.String"; + HandlerInfo info = HandlerInfo.fromString(handler, ClassLoader.getSystemClassLoader()); + + assertEquals(String.class, info.clazz); + assertNull(info.methodName); + } + + @Test + void testFromStringWithMethod() throws Exception { + String handler = "java.lang.String::length"; + HandlerInfo info = HandlerInfo.fromString(handler, ClassLoader.getSystemClassLoader()); + + assertEquals(String.class, info.clazz); + assertEquals("length", info.methodName); + } + + @Test + void testFromStringWithEmptyClass() { + String handler = "::method"; + + assertThrows(HandlerInfo.InvalidHandlerException.class, () -> + HandlerInfo.fromString(handler, ClassLoader.getSystemClassLoader()) + ); + } + + @Test + void testFromStringWithEmptyMethod() { + String handler = "java.lang.String::"; + + assertThrows(HandlerInfo.InvalidHandlerException.class, () -> + HandlerInfo.fromString(handler, ClassLoader.getSystemClassLoader()) + ); + } + + @Test + void testFromStringWithNonexistentClass() { + String handler = "com.nonexistent.TestClass::method"; + + assertThrows(ClassNotFoundException.class, () -> + HandlerInfo.fromString(handler, ClassLoader.getSystemClassLoader()) + ); + } + + @Test + void testFromStringWithNullHandler() { + assertThrows(NullPointerException.class, () -> + HandlerInfo.fromString(null, ClassLoader.getSystemClassLoader()) + ); + } + + @Test + void testClassNameWithoutMethod() { + String handler = "java.lang.String"; + String className = HandlerInfo.className(handler); + + assertEquals("java.lang.String", className); + } + + @Test + void testClassNameWithMethod() { + String handler = "java.lang.String::length"; + String className = HandlerInfo.className(handler); + + assertEquals("java.lang.String", className); + } + + @Test + void testClassNameWithEmptyString() { + String handler = ""; + String className = HandlerInfo.className(handler); + + assertEquals("", className); + } + + @Test + void testClassNameWithOnlyDelimiter() { + String handler = "::"; + String className = HandlerInfo.className(handler); + + assertEquals("", className); + } + + @Test + void testInvalidHandlerExceptionSerialVersionUID() { + assertEquals(-1L, HandlerInfo.InvalidHandlerException.serialVersionUID); + } + + @Test + void testFromStringWithInnerClass() throws Exception { + // Create a custom class loader that can load our test class + ClassLoader cl = new ClassLoader() { + @Override + public Class loadClass(String name) throws ClassNotFoundException { + if (name.equals("com.test.OuterClass$InnerClass")) { + throw new ClassNotFoundException("Test class not found"); + } + return super.loadClass(name); + } + }; + + String handler = "com.test.OuterClass$InnerClass::method"; + assertThrows(ClassNotFoundException.class, () -> + HandlerInfo.fromString(handler, cl) + ); + } +} diff --git a/aws-lambda-java-runtime-interface-client/src/test/java/com/amazonaws/services/lambda/runtime/api/client/LambdaRequestHandler.java b/aws-lambda-java-runtime-interface-client/src/test/java/com/amazonaws/services/lambda/runtime/api/client/LambdaRequestHandler.java new file mode 100644 index 000000000..d86b73857 --- /dev/null +++ b/aws-lambda-java-runtime-interface-client/src/test/java/com/amazonaws/services/lambda/runtime/api/client/LambdaRequestHandler.java @@ -0,0 +1,142 @@ +/* +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 com.amazonaws.services.lambda.runtime.api.client.runtimeapi.dto.InvocationRequest; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.BeforeEach; +import static org.junit.jupiter.api.Assertions.*; +import static org.mockito.Mockito.*; + +class LambdaRequestHandlerTest { + + private InvocationRequest mockRequest; + + @BeforeEach + void setUp() { + mockRequest = mock(InvocationRequest.class); + } + + @Test + void testInitErrorHandler() { + String className = "com.example.TestClass"; + Exception testException = new RuntimeException("initialization error"); + + LambdaRequestHandler handler = LambdaRequestHandler.initErrorHandler(testException, className); + + assertNotNull(handler); + assertTrue(handler instanceof LambdaRequestHandler.UserFaultHandler); + + LambdaRequestHandler.UserFaultHandler userFaultHandler = (LambdaRequestHandler.UserFaultHandler) handler; + UserFault fault = userFaultHandler.fault; + + assertNotNull(fault); + assertEquals("Error loading class " + className + ": initialization error", fault.msg); + assertEquals("java.lang.RuntimeException", fault.exception); + assertTrue(fault.fatal); + } + + @Test + void testClassNotFound() { + String className = "com.example.MissingClass"; + Exception testException = new ClassNotFoundException("class not found"); + + LambdaRequestHandler handler = LambdaRequestHandler.classNotFound(testException, className); + + assertNotNull(handler); + assertTrue(handler instanceof LambdaRequestHandler.UserFaultHandler); + + LambdaRequestHandler.UserFaultHandler userFaultHandler = (LambdaRequestHandler.UserFaultHandler) handler; + UserFault fault = userFaultHandler.fault; + + assertNotNull(fault); + assertEquals("Class not found: " + className, fault.msg); + assertEquals("java.lang.ClassNotFoundException", fault.exception); + assertFalse(fault.fatal); + } + + @Test + void testUserFaultHandlerConstructor() { + UserFault testFault = new UserFault("test message", "TestException", "test trace"); + LambdaRequestHandler.UserFaultHandler handler = new LambdaRequestHandler.UserFaultHandler(testFault); + + assertNotNull(handler); + assertSame(testFault, handler.fault); + } + + @Test + void testUserFaultHandlerCallThrowsFault() { + UserFault testFault = new UserFault("test message", "TestException", "test trace"); + LambdaRequestHandler.UserFaultHandler handler = new LambdaRequestHandler.UserFaultHandler(testFault); + + UserFault thrownFault = assertThrows(UserFault.class, () -> handler.call(mockRequest)); + assertSame(testFault, thrownFault); + } + + @Test + void testInitErrorHandlerWithNullMessage() { + String className = "com.example.TestClass"; + Exception testException = new RuntimeException(); + + LambdaRequestHandler handler = LambdaRequestHandler.initErrorHandler(testException, className); + + assertNotNull(handler); + assertTrue(handler instanceof LambdaRequestHandler.UserFaultHandler); + + LambdaRequestHandler.UserFaultHandler userFaultHandler = (LambdaRequestHandler.UserFaultHandler) handler; + UserFault fault = userFaultHandler.fault; + + assertNotNull(fault); + assertEquals("Error loading class " + className, fault.msg); + assertEquals("java.lang.RuntimeException", fault.exception); + assertTrue(fault.fatal); + } + + @Test + void testInitErrorHandlerWithNullClassName() { + Exception testException = new RuntimeException("test error"); + + LambdaRequestHandler handler = LambdaRequestHandler.initErrorHandler(testException, null); + + assertNotNull(handler); + assertTrue(handler instanceof LambdaRequestHandler.UserFaultHandler); + + LambdaRequestHandler.UserFaultHandler userFaultHandler = (LambdaRequestHandler.UserFaultHandler) handler; + UserFault fault = userFaultHandler.fault; + + assertNotNull(fault); + assertEquals("Error loading class null: test error", fault.msg); + assertEquals("java.lang.RuntimeException", fault.exception); + assertTrue(fault.fatal); + } + + @Test + void testClassNotFoundWithNullClassName() { + Exception testException = new ClassNotFoundException("test error"); + + LambdaRequestHandler handler = LambdaRequestHandler.classNotFound(testException, null); + + assertNotNull(handler); + assertTrue(handler instanceof LambdaRequestHandler.UserFaultHandler); + + LambdaRequestHandler.UserFaultHandler userFaultHandler = (LambdaRequestHandler.UserFaultHandler) handler; + UserFault fault = userFaultHandler.fault; + + assertNotNull(fault); + assertEquals("Class not found: null", fault.msg); + assertEquals("java.lang.ClassNotFoundException", fault.exception); + assertFalse(fault.fatal); + } + + @Test + void testUserFaultHandlerCallWithNullRequest() { + UserFault testFault = new UserFault("test message", "TestException", "test trace"); + LambdaRequestHandler.UserFaultHandler handler = new LambdaRequestHandler.UserFaultHandler(testFault); + + UserFault thrownFault = assertThrows(UserFault.class, () -> handler.call(null)); + assertSame(testFault, thrownFault); + } +} 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 new file mode 100644 index 000000000..4ebcf5d7e --- /dev/null +++ b/aws-lambda-java-runtime-interface-client/src/test/java/com/amazonaws/services/lambda/runtime/api/client/PojoSerializerLoaderTest.java @@ -0,0 +1,153 @@ +/* +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 com.amazonaws.services.lambda.runtime.CustomPojoSerializer; +import com.amazonaws.services.lambda.runtime.serialization.PojoSerializer; +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; +import org.mockito.junit.jupiter.MockitoExtension; + +import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; +import java.io.InputStream; +import java.io.OutputStream; +import java.lang.reflect.Field; +import java.lang.reflect.Type; + +import static org.junit.jupiter.api.Assertions.*; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.*; + +@ExtendWith(MockitoExtension.class) +class PojoSerializerLoaderTest { + + @Mock + private CustomPojoSerializer mockSerializer; + + @AfterEach + @BeforeEach + void setUp() throws Exception { + resetStaticFields(); + } + + private void resetStaticFields() throws Exception { + Field serializerField = PojoSerializerLoader.class.getDeclaredField("customPojoSerializer"); + serializerField.setAccessible(true); + serializerField.set(null, null); + + Field initializedField = PojoSerializerLoader.class.getDeclaredField("initialized"); + initializedField.setAccessible(true); + initializedField.set(null, false); + } + + + private void setMockSerializer(CustomPojoSerializer serializer) throws Exception { + Field serializerField = PojoSerializerLoader.class.getDeclaredField("customPojoSerializer"); + serializerField.setAccessible(true); + serializerField.set(null, serializer); + } + + @Test + void testGetCustomerSerializerNoSerializerAvailable() throws Exception { + PojoSerializer serializer = PojoSerializerLoader.getCustomerSerializer(String.class); + assertNull(serializer); + Field initializedField = PojoSerializerLoader.class.getDeclaredField("initialized"); + initializedField.setAccessible(true); + assert((Boolean) initializedField.get(null)); + } + + @Test + void testGetCustomerSerializerWithValidSerializer() throws Exception { + setMockSerializer(mockSerializer); + String testInput = "test input"; + String testOutput = "test output"; + Type testType = String.class; + when(mockSerializer.fromJson(any(InputStream.class), eq(testType))).thenReturn(testOutput); + when(mockSerializer.fromJson(eq(testInput), eq(testType))).thenReturn(testOutput); + + PojoSerializer serializer = PojoSerializerLoader.getCustomerSerializer(testType); + assertNotNull(serializer); + + ByteArrayInputStream inputStream = new ByteArrayInputStream(testInput.getBytes()); + Object result1 = serializer.fromJson(inputStream); + assertEquals(testOutput, result1); + + Object result2 = serializer.fromJson(testInput); + assertEquals(testOutput, result2); + + ByteArrayOutputStream outputStream = new ByteArrayOutputStream(); + serializer.toJson(testInput, outputStream); + verify(mockSerializer).toJson(eq(testInput), any(OutputStream.class), eq(testType)); + } + + @Test + void testGetCustomerSerializerCachingBehavior() throws Exception { + setMockSerializer(mockSerializer); + + Type testType = String.class; + PojoSerializer serializer1 = PojoSerializerLoader.getCustomerSerializer(testType); + PojoSerializer serializer2 = PojoSerializerLoader.getCustomerSerializer(testType); + + assertNotNull(serializer1); + assertNotNull(serializer2); + + String testInput = "test"; + serializer1.fromJson(testInput); + serializer2.fromJson(testInput); + + verify(mockSerializer, times(2)).fromJson(eq(testInput), eq(testType)); + } + + @Test + void testGetCustomerSerializerDifferentTypes() throws Exception { + setMockSerializer(mockSerializer); + + PojoSerializer stringSerializer = PojoSerializerLoader.getCustomerSerializer(String.class); + PojoSerializer integerSerializer = PojoSerializerLoader.getCustomerSerializer(Integer.class); + + assertNotNull(stringSerializer); + assertNotNull(integerSerializer); + + String testString = "test"; + Integer testInt = 123; + + stringSerializer.fromJson(testString); + integerSerializer.fromJson(testInt.toString()); + + verify(mockSerializer).fromJson(eq(testString), eq(String.class)); + verify(mockSerializer).fromJson(eq(testInt.toString()), eq(Integer.class)); + } + + @Test + void testGetCustomerSerializerNullType() throws Exception { + setMockSerializer(mockSerializer); + + PojoSerializer serializer = PojoSerializerLoader.getCustomerSerializer(null); + assertNotNull(serializer); + + String testInput = "test"; + serializer.fromJson(testInput); + verify(mockSerializer).fromJson(eq(testInput), eq(null)); + } + + @Test + void testGetCustomerSerializerExceptionHandling() throws Exception { + setMockSerializer(mockSerializer); + + doThrow(new RuntimeException("Test exception")) + .when(mockSerializer) + .fromJson(any(String.class), any(Type.class)); + + PojoSerializer serializer = PojoSerializerLoader.getCustomerSerializer(String.class); + assertNotNull(serializer); + assertThrows(RuntimeException.class, () -> serializer.fromJson("test")); + } +} diff --git a/aws-lambda-java-runtime-interface-client/src/test/java/com/amazonaws/services/lambda/runtime/api/client/TooManyServiceProvidersFoundExceptionTest.java b/aws-lambda-java-runtime-interface-client/src/test/java/com/amazonaws/services/lambda/runtime/api/client/TooManyServiceProvidersFoundExceptionTest.java new file mode 100644 index 000000000..38d33f63b --- /dev/null +++ b/aws-lambda-java-runtime-interface-client/src/test/java/com/amazonaws/services/lambda/runtime/api/client/TooManyServiceProvidersFoundExceptionTest.java @@ -0,0 +1,59 @@ +/* +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 org.junit.jupiter.api.Test; + +import com.amazonaws.services.lambda.runtime.api.client.TooManyServiceProvidersFoundException; + +import static org.junit.jupiter.api.Assertions.*; + +class TooManyServiceProvidersFoundExceptionTest { + + @Test + void testDefaultConstructor() { + TooManyServiceProvidersFoundException exception = new TooManyServiceProvidersFoundException(); + + assertNotNull(exception); + assertNull(exception.getMessage()); + assertNull(exception.getCause()); + } + + @Test + void testMessageConstructor() { + String errorMessage = "Too many service providers found"; + TooManyServiceProvidersFoundException exception = + new TooManyServiceProvidersFoundException(errorMessage); + + assertNotNull(exception); + assertEquals(errorMessage, exception.getMessage()); + assertNull(exception.getCause()); + } + + @Test + void testCauseConstructor() { + Throwable cause = new IllegalStateException("Original error"); + TooManyServiceProvidersFoundException exception = + new TooManyServiceProvidersFoundException(cause); + + assertNotNull(exception); + assertEquals(cause.toString(), exception.getMessage()); + assertSame(cause, exception.getCause()); + } + + @Test + void testMessageAndCauseConstructor() { + String errorMessage = "Too many service providers found"; + Throwable cause = new IllegalStateException("Original error"); + TooManyServiceProvidersFoundException exception = + new TooManyServiceProvidersFoundException(errorMessage, cause); + + assertNotNull(exception); + assertEquals(errorMessage, exception.getMessage()); + assertSame(cause, exception.getCause()); + } + +} diff --git a/aws-lambda-java-runtime-interface-client/src/test/java/com/amazonaws/services/lambda/runtime/api/client/UserFaultTest.java b/aws-lambda-java-runtime-interface-client/src/test/java/com/amazonaws/services/lambda/runtime/api/client/UserFaultTest.java index cdc7a9b1d..479162adf 100644 --- a/aws-lambda-java-runtime-interface-client/src/test/java/com/amazonaws/services/lambda/runtime/api/client/UserFaultTest.java +++ b/aws-lambda-java-runtime-interface-client/src/test/java/com/amazonaws/services/lambda/runtime/api/client/UserFaultTest.java @@ -4,10 +4,9 @@ import org.junit.jupiter.api.Test; -import static org.junit.jupiter.api.Assertions.assertEquals; -import static org.junit.jupiter.api.Assertions.assertTrue; -import static org.junit.jupiter.api.Assertions.fail; +import static org.junit.jupiter.api.Assertions.*; import static testpkg.StackTraceHelper.callThenThrowRuntimeException; +import static testpkg.StackTraceHelper.throwCheckpointExceptionWithTwoSuppressedExceptions; import static testpkg.StackTraceHelper.throwRuntimeException; public class UserFaultTest { @@ -71,4 +70,96 @@ public void testReportableErrorOnlyMessage() { String actual = userFault.reportableError(); assertEquals(expected, actual); } + + @Test + public void testSuppressedExceptionsAreIncluded() { + try{ + throwCheckpointExceptionWithTwoSuppressedExceptions("error 1", "error 2"); + } catch(Exception e1) { + UserFault userFault = UserFault.makeUserFault(e1); + String reportableUserFault = userFault.reportableError(); + + assertTrue(reportableUserFault.contains("com.amazonaws.services.lambda.crac.CheckpointException"), "CheckpointException missing in reported UserFault"); + assertTrue(reportableUserFault.contains("Suppressed: java.lang.RuntimeException: error 1"), "Suppressed error 1 missing in reported UserFault"); + assertTrue(reportableUserFault.contains("Suppressed: java.lang.RuntimeException: error 2"), "Suppressed error 2 missing in reported UserFault"); + } + } + + @Test + public void testCircularExceptionReference() { + RuntimeException e1 = new RuntimeException(); + RuntimeException e2 = new RuntimeException(e1); + e1.initCause(e2); + + try { + throw e2; + } catch (Exception e) { + String stackTrace = UserFault.trace(e); + String expectedStackTrace = "java.lang.RuntimeException: java.lang.RuntimeException\n" + + "Caused by: java.lang.RuntimeException\n" + + "Caused by: [CIRCULAR REFERENCE: java.lang.RuntimeException: java.lang.RuntimeException]\n"; + + assertEquals(expectedStackTrace, stackTrace); + } + } + + @Test + public void testCircularSuppressedExceptionReference() { + RuntimeException e1 = new RuntimeException("Primary Exception"); + RuntimeException e2 = new RuntimeException(e1); + RuntimeException e3 = new RuntimeException("Exception with suppressed"); + + e1.addSuppressed(e2); + e3.addSuppressed(e2); + + try { + throw e3; + } catch (Exception e) { + String stackTrace = UserFault.trace(e); + String expectedStackTrace = "java.lang.RuntimeException: Exception with suppressed\n" + + "\tSuppressed: java.lang.RuntimeException: java.lang.RuntimeException: Primary Exception\n" + + "\tCaused by: java.lang.RuntimeException: Primary Exception\n" + + "\t\tSuppressed: [CIRCULAR REFERENCE: java.lang.RuntimeException: java.lang.RuntimeException: Primary Exception]\n"; + + assertEquals(expectedStackTrace, stackTrace); + } + } + + private Exception createExceptionWithStackTrace() { + try { + throw new RuntimeException("Test exception"); + } catch (RuntimeException e) { + return e; + } + } + + @Test + void testMakeInitErrorUserFault() { + String className = "com.example.TestClass"; + Exception testException = createExceptionWithStackTrace(); + + UserFault initFault = UserFault.makeInitErrorUserFault(testException, className); + UserFault notFoundFault = UserFault.makeClassNotFoundUserFault(testException, className); + + assertNotNull(initFault.trace); + assertNotNull(notFoundFault.trace); + + assertFalse(initFault.trace.contains("com.amazonaws.services.lambda.runtime")); + assertFalse(notFoundFault.trace.contains("com.amazonaws.services.lambda.runtime")); + } + + @Test + void testMakeClassNotFoundUserFault() { + String className = "com.example.MissingClass"; + Exception testException = new ClassNotFoundException("Class not found in classpath"); + + UserFault fault = UserFault.makeClassNotFoundUserFault(testException, className); + + assertNotNull(fault); + assertEquals("Class not found: com.example.MissingClass", fault.msg); + assertEquals("java.lang.ClassNotFoundException", fault.exception); + assertNotNull(fault.trace); + assertFalse(fault.fatal); + assertTrue(fault.trace.contains("ClassNotFoundException")); + } } diff --git a/aws-lambda-java-runtime-interface-client/src/test/java/com/amazonaws/services/lambda/runtime/api/client/XRayErrorCauseTest.java b/aws-lambda-java-runtime-interface-client/src/test/java/com/amazonaws/services/lambda/runtime/api/client/XRayErrorCauseTest.java index a6ad7577f..8de6963a8 100644 --- a/aws-lambda-java-runtime-interface-client/src/test/java/com/amazonaws/services/lambda/runtime/api/client/XRayErrorCauseTest.java +++ b/aws-lambda-java-runtime-interface-client/src/test/java/com/amazonaws/services/lambda/runtime/api/client/XRayErrorCauseTest.java @@ -2,6 +2,9 @@ package com.amazonaws.services.lambda.runtime.api.client; +import com.amazonaws.services.lambda.runtime.api.client.runtimeapi.converters.XRayErrorCauseConverter; +import com.amazonaws.services.lambda.runtime.api.client.runtimeapi.dto.XRayErrorCause; +import com.amazonaws.services.lambda.runtime.api.client.runtimeapi.dto.XRayException; import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; @@ -47,25 +50,25 @@ public void xrayErrorCauseTestNoFileName() { } private void assertXrayErrorCause(Throwable t) { - XRayErrorCause xRayErrorCause = new XRayErrorCause(t); + XRayErrorCause xRayErrorCause = XRayErrorCauseConverter.fromThrowable(t); - assertEquals(TEST_WORKING_DIR, xRayErrorCause.getWorking_directory()); + assertEquals(TEST_WORKING_DIR, xRayErrorCause.working_directory); - assertEquals(1, xRayErrorCause.getPaths().size()); - assertTrue(xRayErrorCause.getPaths().contains("StackTraceHelper.java")); + assertEquals(1, xRayErrorCause.paths.size()); + assertTrue(xRayErrorCause.paths.contains("StackTraceHelper.java")); - assertEquals(1, xRayErrorCause.getExceptions().size()); - XRayErrorCause.XRayException xRayException = xRayErrorCause.getExceptions().iterator().next(); - assertEquals("woops", xRayException.getMessage()); - assertEquals("java.lang.RuntimeException", xRayException.getType()); + assertEquals(1, xRayErrorCause.exceptions.size()); + XRayException xRayException = xRayErrorCause.exceptions.iterator().next(); + assertEquals("woops", xRayException.message); + assertEquals("java.lang.RuntimeException", xRayException.type); - assertEquals("throwRuntimeException", xRayException.getStack().get(0).getLabel()); - assertEquals("StackTraceHelper.java", xRayException.getStack().get(0).getPath()); - assertTrue(xRayException.getStack().get(0).getLine() > 0); + assertEquals("throwRuntimeException", xRayException.stack.get(0).label); + assertEquals("StackTraceHelper.java", xRayException.stack.get(0).path); + assertTrue(xRayException.stack.get(0).line > 0); - assertEquals("callThenThrowRuntimeException", xRayException.getStack().get(1).getLabel()); - assertEquals("StackTraceHelper.java", xRayException.getStack().get(1).getPath()); - assertTrue(xRayException.getStack().get(0).getLine() > 0); + assertEquals("callThenThrowRuntimeException", xRayException.stack.get(1).label); + assertEquals("StackTraceHelper.java", xRayException.stack.get(1).path); + assertTrue(xRayException.stack.get(0).line > 0); } private void clearStackTraceElementsFilename(Throwable t) { 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 19744dd51..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 @@ -18,6 +18,8 @@ public class LambdaContextTest { private static final String INVOKED_FUNCTION_ARN = "invoked-function-arn"; 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() { @@ -54,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, 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 new file mode 100644 index 000000000..3a5ee8d5f --- /dev/null +++ b/aws-lambda-java-runtime-interface-client/src/test/java/com/amazonaws/services/lambda/runtime/api/client/logging/AbstractLambdaLoggerTest.java @@ -0,0 +1,187 @@ +package com.amazonaws.services.lambda.runtime.api.client.logging; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +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; + + +public class AbstractLambdaLoggerTest { + class TestSink implements LogSink { + private List messages = new LinkedList<>(); + + public TestSink() { + } + + @Override + public synchronized void log(byte[] message) { + messages.add(message); + } + + @Override + public synchronized void log(LogLevel logLevel, LogFormat logFormat, byte[] message) { + messages.add(message); + } + + @Override + public void close() { + } + + List getMessages() { + return messages; + } + } + + private void logMessages(LambdaLogger logger) { + logger.log("trace", LogLevel.TRACE); + logger.log("debug", LogLevel.DEBUG); + logger.log("info", LogLevel.INFO); + logger.log("warn", LogLevel.WARN); + logger.log("error", LogLevel.ERROR); + logger.log("fatal", LogLevel.FATAL); + } + + @Test + public void testLoggingNullValuesWithoutLogLevelInText() { + TestSink sink = new TestSink(); + LambdaLogger logger = new LambdaContextLogger(sink, LogLevel.INFO, LogFormat.TEXT); + + String isNullString = null; + byte[] isNullBytes = null; + + logger.log(isNullString); + logger.log(isNullBytes); + + assertEquals("null", new String(sink.getMessages().get(0))); + 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(); + LambdaLogger logger = new LambdaContextLogger(sink, LogLevel.INFO, LogFormat.JSON); + + String isNullString = null; + byte[] isNullBytes = null; + + logger.log(isNullString); + logger.log(isNullBytes); + + assertEquals(2, sink.getMessages().size()); + } + + @Test + public void testLoggingNullValuesWithLogLevelInText() { + TestSink sink = new TestSink(); + LambdaLogger logger = new LambdaContextLogger(sink, LogLevel.INFO, LogFormat.TEXT); + + String isNullString = null; + byte[] isNullBytes = null; + + logger.log(isNullString, LogLevel.ERROR); + logger.log(isNullBytes, LogLevel.ERROR); + + assertEquals("[ERROR] null", new String(sink.getMessages().get(0))); + assertEquals("null", new String(sink.getMessages().get(1))); + } + + @Test + public void testLoggingNullValuesWithLogLevelInJSON() { + TestSink sink = new TestSink(); + LambdaLogger logger = new LambdaContextLogger(sink, LogLevel.INFO, LogFormat.JSON); + + String isNullString = null; + byte[] isNullBytes = null; + + logger.log(isNullString, LogLevel.ERROR); + logger.log(isNullBytes, LogLevel.ERROR); + + assertEquals(2, sink.getMessages().size()); + } + @Test + public void testWithoutFiltering() { + TestSink sink = new TestSink(); + LambdaLogger logger = new LambdaContextLogger(sink, LogLevel.UNDEFINED, LogFormat.TEXT); + logMessages(logger); + + assertEquals(6, sink.getMessages().size()); + } + + @Test + public void testWithFiltering() { + TestSink sink = new TestSink(); + LambdaLogger logger = new LambdaContextLogger(sink, LogLevel.WARN, LogFormat.TEXT); + logMessages(logger); + + assertEquals(3, sink.getMessages().size()); + } + + @Test + public void testUndefinedLogLevelWithFiltering() { + TestSink sink = new TestSink(); + LambdaLogger logger = new LambdaContextLogger(sink, LogLevel.WARN, LogFormat.TEXT); + logger.log("undefined"); + + assertEquals(1, sink.getMessages().size()); + } + + @Test + public void testFormattingLogMessages() { + TestSink sink = new TestSink(); + LambdaLogger logger = new LambdaContextLogger(sink, LogLevel.INFO, LogFormat.TEXT); + logger.log("test message", LogLevel.INFO); + + assertEquals(1, sink.getMessages().size()); + assertEquals("[INFO] test message", new String(sink.getMessages().get(0))); + } +} diff --git a/aws-lambda-java-runtime-interface-client/src/test/java/com/amazonaws/services/lambda/runtime/api/client/logging/FrameTypeTest.java b/aws-lambda-java-runtime-interface-client/src/test/java/com/amazonaws/services/lambda/runtime/api/client/logging/FrameTypeTest.java new file mode 100644 index 000000000..65078790c --- /dev/null +++ b/aws-lambda-java-runtime-interface-client/src/test/java/com/amazonaws/services/lambda/runtime/api/client/logging/FrameTypeTest.java @@ -0,0 +1,39 @@ +package com.amazonaws.services.lambda.runtime.api.client.logging; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +import org.junit.jupiter.api.Test; + +import com.amazonaws.services.lambda.runtime.logging.LogLevel; +import com.amazonaws.services.lambda.runtime.logging.LogFormat; + +public class FrameTypeTest { + + @Test + public void logFrames() { + assertHexEquals( + 0xa55a0003, + FrameType.getValue(LogLevel.UNDEFINED, LogFormat.TEXT) + ); + + assertHexEquals( + 0xa55a001b, + FrameType.getValue(LogLevel.FATAL, LogFormat.TEXT) + ); + } + + + /** + * Helper function to make it easier to debug failing test. + * + * @param expected Expected value as int + * @param actual Actual value as int + */ + private void assertHexEquals(int expected, int actual) { + assertEquals( + Integer.toHexString(expected), + Integer.toHexString(actual) + ); + } + +} diff --git a/aws-lambda-java-runtime-interface-client/src/test/java/com/amazonaws/services/lambda/runtime/api/client/logging/FramedTelemetryLogSinkTest.java b/aws-lambda-java-runtime-interface-client/src/test/java/com/amazonaws/services/lambda/runtime/api/client/logging/FramedTelemetryLogSinkTest.java index a2f5f4b4d..e3e68a693 100644 --- a/aws-lambda-java-runtime-interface-client/src/test/java/com/amazonaws/services/lambda/runtime/api/client/logging/FramedTelemetryLogSinkTest.java +++ b/aws-lambda-java-runtime-interface-client/src/test/java/com/amazonaws/services/lambda/runtime/api/client/logging/FramedTelemetryLogSinkTest.java @@ -6,32 +6,50 @@ import org.junit.jupiter.api.io.TempDir; import java.io.File; +import java.io.FileDescriptor; import java.io.FileInputStream; +import java.io.FileOutputStream; import java.io.IOException; import java.nio.ByteBuffer; import java.nio.channels.ReadableByteChannel; import java.nio.file.Path; +import java.time.Instant; import java.util.Arrays; import static org.junit.jupiter.api.Assertions.assertArrayEquals; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertTrue; +import com.amazonaws.services.lambda.runtime.logging.LogLevel; +import com.amazonaws.services.lambda.runtime.logging.LogFormat; + public class FramedTelemetryLogSinkTest { private static final int DEFAULT_BUFFER_SIZE = 256; private static final byte ZERO_BYTE = (byte) 0; + private long timestamp() { + Instant instant = Instant.now(); + return instant.getEpochSecond() * 1_000_000 + instant.getNano() / 1000; + } + @TempDir public Path tmpFolder; @Test public void logSingleFrame() throws IOException { - byte[] message = "hello world\nsomething on a new line!\n".getBytes(); + byte[] message = "{\"message\": \"hello world\nsomething on a new line!\"}".getBytes(); + LogLevel logLevel = LogLevel.ERROR; + LogFormat logFormat = LogFormat.JSON; + File tmpFile = tmpFolder.resolve("pipe").toFile(); - try (FramedTelemetryLogSink logSink = new FramedTelemetryLogSink(tmpFile)) { - logSink.log(message); + FileOutputStream fos = new FileOutputStream(tmpFile); + FileDescriptor fd = fos.getFD(); + long before = timestamp(); + try (FramedTelemetryLogSink logSink = new FramedTelemetryLogSink(fd)) { + logSink.log(logLevel, logFormat, message); } + long after = timestamp(); ByteBuffer buf = ByteBuffer.allocate(DEFAULT_BUFFER_SIZE); ReadableByteChannel readChannel = new FileInputStream(tmpFile).getChannel(); @@ -42,19 +60,24 @@ public void logSingleFrame() throws IOException { // first 4 bytes indicate the type int type = buf.getInt(); - assertEquals(FrameType.LOG.getValue(), type); + assertEquals(FrameType.getValue(logLevel, logFormat), type); // next 4 bytes indicate the length of the message int len = buf.getInt(); assertEquals(message.length, len); + // next 8 bytes should indicate the timestamp + long timestamp = buf.getLong(); + assertTrue(before <= timestamp); + assertTrue(timestamp <= after); + // use `len` to allocate a byte array to read the logged message into byte[] actual = new byte[len]; buf.get(actual); assertArrayEquals(message, actual); // rest of buffer should be empty - while(buf.hasRemaining()) + while (buf.hasRemaining()) assertEquals(ZERO_BYTE, buf.get()); } @@ -62,11 +85,18 @@ public void logSingleFrame() throws IOException { public void logMultipleFrames() throws IOException { byte[] firstMessage = "hello world\nsomething on a new line!".getBytes(); byte[] secondMessage = "hello again\nhere's another message\n".getBytes(); + LogLevel logLevel = LogLevel.ERROR; + LogFormat logFormat = LogFormat.TEXT; + File tmpFile = tmpFolder.resolve("pipe").toFile(); - try (FramedTelemetryLogSink logSink = new FramedTelemetryLogSink(tmpFile)) { - logSink.log(firstMessage); - logSink.log(secondMessage); + FileOutputStream fos = new FileOutputStream(tmpFile); + FileDescriptor fd = fos.getFD(); + long before = timestamp(); + try (FramedTelemetryLogSink logSink = new FramedTelemetryLogSink(fd)) { + logSink.log(logLevel, logFormat, firstMessage); + logSink.log(logLevel, logFormat, secondMessage); } + long after = timestamp(); ByteBuffer buf = ByteBuffer.allocate(DEFAULT_BUFFER_SIZE); ReadableByteChannel readChannel = new FileInputStream(tmpFile).getChannel(); @@ -75,15 +105,20 @@ public void logMultipleFrames() throws IOException { // reset the position to the start buf.position(0); - for(byte[] message : Arrays.asList(firstMessage, secondMessage)) { + for (byte[] message : Arrays.asList(firstMessage, secondMessage)) { // first 4 bytes indicate the type int type = buf.getInt(); - assertEquals(FrameType.LOG.getValue(), type); + assertEquals(FrameType.getValue(logLevel, logFormat), type); // next 4 bytes indicate the length of the message int len = buf.getInt(); assertEquals(message.length, len); + // next 8 bytes should indicate the timestamp + long timestamp = buf.getLong(); + assertTrue(before <= timestamp); + assertTrue(timestamp <= after); + // use `len` to allocate a byte array to read the logged message into byte[] actual = new byte[len]; buf.get(actual); @@ -91,7 +126,7 @@ public void logMultipleFrames() throws IOException { } // rest of buffer should be empty - while(buf.hasRemaining()) + while (buf.hasRemaining()) assertEquals(ZERO_BYTE, buf.get()); } @@ -99,7 +134,7 @@ public void logMultipleFrames() throws IOException { * The implementation of FramedTelemetryLogSink was based on java.nio.channels.WritableByteChannel which would * throw ClosedByInterruptException if Thread.currentThread.interrupt() was called. The implementation was changed * and this test ensures that logging works even if the current thread was interrupted. - * + *

* https://t.corp.amazon.com/0304370986/ */ @Test @@ -107,23 +142,25 @@ public void interruptedThread() throws IOException { try { byte[] message = "hello world\nsomething on a new line!\n".getBytes(); File tmpFile = tmpFolder.resolve("pipe").toFile(); - try (FramedTelemetryLogSink logSink = new FramedTelemetryLogSink(tmpFile)) { + FileOutputStream fos = new FileOutputStream(tmpFile); + FileDescriptor fd = fos.getFD(); + try (FramedTelemetryLogSink logSink = new FramedTelemetryLogSink(fd)) { Thread.currentThread().interrupt(); - logSink.log(message); + logSink.log(LogLevel.ERROR, LogFormat.TEXT, message); } byte[] buffer = new byte[DEFAULT_BUFFER_SIZE]; FileInputStream logInputStream = new FileInputStream(tmpFile); int readBytes = logInputStream.read(buffer); - int headerSizeBytes = 8; // message type (4 bytes) + len (4 bytes) + int headerSizeBytes = 16; // message type (4 bytes) + len (4 bytes) + timestamp (8 bytes) int expectedBytes = headerSizeBytes + message.length; assertEquals(expectedBytes, readBytes); - for(int i = 0; i < message.length; i++) { - assertEquals(buffer[i + headerSizeBytes], message[i]); + for (int i = 0; i < message.length; i++) { + assertEquals(message[i], buffer[i + headerSizeBytes]); } } finally { // clear interrupted status of the current thread 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 new file mode 100644 index 000000000..91ce9d2a3 --- /dev/null +++ b/aws-lambda-java-runtime-interface-client/src/test/java/com/amazonaws/services/lambda/runtime/api/client/logging/JsonLogFormatterTest.java @@ -0,0 +1,79 @@ +package com.amazonaws.services.lambda.runtime.api.client.logging; + +import com.amazonaws.services.lambda.runtime.api.client.api.LambdaContext; +import com.amazonaws.services.lambda.runtime.serialization.PojoSerializer; +import com.amazonaws.services.lambda.runtime.serialization.factories.GsonFactory; +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; + +import com.amazonaws.services.lambda.runtime.logging.LogLevel; + +public class JsonLogFormatterTest { + + @Test + void testFormattingWithoutLambdaContext() { + assertFormatsString("test log", LogLevel.WARN, null); + } + + @Test + void testFormattingWithLambdaContext() { + LambdaContext context = new LambdaContext( + 0, + 0, + "request-id", + null, + null, + "function-name", + null, + null, + "function-arn", + null, + null, + null + ); + assertFormatsString("test log", LogLevel.WARN, context); + } + + @Test + void testFormattingWithTenantIdInLambdaContext() { + LambdaContext context = new LambdaContext( + 0, + 0, + "request-id", + null, + null, + "function-name", + null, + null, + "function-arn", + "tenant-id", + "xray-trace-id", + null + ); + assertFormatsString("test log", LogLevel.WARN, context); + } + + void assertFormatsString(String message, LogLevel logLevel, LambdaContext context) { + JsonLogFormatter logFormatter = new JsonLogFormatter(); + if (context != null) { + logFormatter.setLambdaContext(context); + } + String output = logFormatter.format(message, logLevel); + + PojoSerializer serializer = GsonFactory.getInstance().getSerializer(StructuredLogMessage.class); + assert_expected_log_message(serializer.fromJson(output), message, logLevel, context); + } + + void assert_expected_log_message(StructuredLogMessage result, String message, LogLevel logLevel, LambdaContext context) { + assertEquals(message, result.message); + assertEquals(logLevel, result.level); + assertNotNull(result.timestamp); + + if (context != null) { + assertEquals(context.getAwsRequestId(), result.AWSRequestId); + assertEquals(context.getTenantId(), result.tenantId); + } + } +} diff --git a/aws-lambda-java-runtime-interface-client/src/test/java/com/amazonaws/services/lambda/runtime/api/client/logging/StdOutLogSinkTest.java b/aws-lambda-java-runtime-interface-client/src/test/java/com/amazonaws/services/lambda/runtime/api/client/logging/StdOutLogSinkTest.java index 83399646b..b1bbefc4c 100644 --- a/aws-lambda-java-runtime-interface-client/src/test/java/com/amazonaws/services/lambda/runtime/api/client/logging/StdOutLogSinkTest.java +++ b/aws-lambda-java-runtime-interface-client/src/test/java/com/amazonaws/services/lambda/runtime/api/client/logging/StdOutLogSinkTest.java @@ -10,6 +10,9 @@ import static org.junit.jupiter.api.Assertions.assertEquals; +import com.amazonaws.services.lambda.runtime.logging.LogFormat; +import com.amazonaws.services.lambda.runtime.logging.LogLevel; + public class StdOutLogSinkTest { private final PrintStream originalOutPrintStream = System.out; @@ -35,6 +38,20 @@ public void testSingleLog() { assertEquals("hello\nworld", bos.toString()); } + @Test + public void testSingleLogWithLogLevel() { + System.setOut(capturedOutPrintStream); + try { + try (StdOutLogSink logSink = new StdOutLogSink()) { + logSink.log(LogLevel.ERROR, LogFormat.TEXT, "hello\nworld".getBytes()); + } + } finally { + System.setOut(originalOutPrintStream); + } + + assertEquals("hello\nworld", bos.toString()); + } + @Test public void testContextLoggerWithStdoutLogSink_logBytes() { System.setOut(capturedOutPrintStream); diff --git a/aws-lambda-java-runtime-interface-client/src/test/java/com/amazonaws/services/lambda/runtime/api/client/logging/TextLogFormatterTest.java b/aws-lambda-java-runtime-interface-client/src/test/java/com/amazonaws/services/lambda/runtime/api/client/logging/TextLogFormatterTest.java new file mode 100644 index 000000000..598074a3b --- /dev/null +++ b/aws-lambda-java-runtime-interface-client/src/test/java/com/amazonaws/services/lambda/runtime/api/client/logging/TextLogFormatterTest.java @@ -0,0 +1,25 @@ +package com.amazonaws.services.lambda.runtime.api.client.logging; + +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +import com.amazonaws.services.lambda.runtime.logging.LogLevel; + +class TextLogFormatterTest { + @Test + void testFormattingStringWithLogLevel() { + assertFormatsString("test log", LogLevel.WARN, "[WARN] test log"); + } + + @Test + void testFormattingStringWithoutLogLevel() { + assertFormatsString("test log", LogLevel.UNDEFINED, "test log"); + } + + void assertFormatsString(String input, LogLevel logLevel, String expected) { + LogFormatter logFormatter = new TextLogFormatter(); + String output = logFormatter.format(input, logLevel); + assertEquals(expected, output); + } +} \ 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/runtimeapi/LambdaRuntimeApiClientImplTest.java b/aws-lambda-java-runtime-interface-client/src/test/java/com/amazonaws/services/lambda/runtime/api/client/runtimeapi/LambdaRuntimeApiClientImplTest.java new file mode 100644 index 000000000..710c1565e --- /dev/null +++ b/aws-lambda-java-runtime-interface-client/src/test/java/com/amazonaws/services/lambda/runtime/api/client/runtimeapi/LambdaRuntimeApiClientImplTest.java @@ -0,0 +1,517 @@ +/* +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; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.condition.DisabledOnOs; +import org.junit.jupiter.api.condition.OS; + +import static org.junit.jupiter.api.Assertions.assertTrue; +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; +import com.amazonaws.services.lambda.runtime.api.client.runtimeapi.dto.XRayErrorCause; +import com.amazonaws.services.lambda.runtime.api.client.runtimeapi.dto.XRayException; + +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; +import static java.net.HttpURLConnection.HTTP_INTERNAL_ERROR; +import okhttp3.mockwebserver.MockWebServer; + +import okhttp3.mockwebserver.MockResponse; +import okhttp3.mockwebserver.RecordedRequest; + +@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; + + String[] errorStackStrace = { "item0", "item1", "item2" }; + ErrorRequest errorRequest = new ErrorRequest("testErrorMessage", "testErrorType", errorStackStrace); + + String requestId = "1234"; + + @BeforeEach + void setUp() { + mockWebServer = new MockWebServer(); + String hostnamePort = getHostnamePort(); + 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 { + RapidErrorType rapidErrorType = RapidErrorType.AfterRestoreError; + + MockResponse mockResponse = new MockResponse(); + mockResponse.setResponseCode(HTTP_ACCEPTED); + mockWebServer.enqueue(mockResponse); + + LambdaError lambdaError = new LambdaError(errorRequest, rapidErrorType); + lambdaRuntimeApiClientImpl.reportInitError(lambdaError); + RecordedRequest recordedRequest = mockWebServer.takeRequest(); + HttpUrl actualUrl = recordedRequest.getRequestUrl(); + String expectedUrl = "http://" + getHostnamePort() + "/2018-06-01/runtime/init/error"; + assertEquals(expectedUrl, actualUrl.toString()); + + String userAgentHeader = recordedRequest.getHeader("User-Agent"); + assertTrue(userAgentHeader.startsWith("aws-lambda-java/")); + + String lambdaRuntimeErrorTypeHeader = recordedRequest.getHeader("Lambda-Runtime-Function-Error-Type"); + assertEquals("Runtime.AfterRestoreError", lambdaRuntimeErrorTypeHeader); + + String actualBody = recordedRequest.getBody().readUtf8(); + assertEquals("{\"errorMessage\":\"testErrorMessage\",\"errorType\":\"testErrorType\",\"stackTrace\":[\"item0\",\"item1\",\"item2\"]}", actualBody); + } catch(Exception e) { + fail(); + } + } + + @Test + public void reportInitErrorTestWrongStatusCode() { + int errorStatusCode = HTTP_INTERNAL_ERROR; + try { + RapidErrorType rapidErrorType = RapidErrorType.AfterRestoreError; + + MockResponse mockResponse = new MockResponse(); + mockResponse.setResponseCode(errorStatusCode); + mockWebServer.enqueue(mockResponse); + + LambdaError lambdaError = new LambdaError(errorRequest, rapidErrorType); + lambdaRuntimeApiClientImpl.reportInitError(lambdaError); + fail(); + } catch(LambdaRuntimeClientException e) { + String expectedUrl = "http://" + getHostnamePort() + "/2018-06-01/runtime/init/error"; + String expectedMessage = expectedUrl + " Response code: '" + errorStatusCode + "'."; + assertEquals(expectedMessage, e.getLocalizedMessage()); + } catch(Exception e) { + fail(); + } + } + + @Test + public void reportRestoreErrorTest() { + try { + RapidErrorType rapidErrorType = RapidErrorType.AfterRestoreError; + + MockResponse mockResponse = new MockResponse(); + mockResponse.setResponseCode(HTTP_ACCEPTED); + mockWebServer.enqueue(mockResponse); + + LambdaError lambdaError = new LambdaError(errorRequest, rapidErrorType); + lambdaRuntimeApiClientImpl.reportRestoreError(lambdaError); + RecordedRequest recordedRequest = mockWebServer.takeRequest(); + HttpUrl actualUrl = recordedRequest.getRequestUrl(); + String expectedUrl = "http://" + getHostnamePort() + "/2018-06-01/runtime/restore/error"; + assertEquals(expectedUrl, actualUrl.toString()); + + String userAgentHeader = recordedRequest.getHeader("User-Agent"); + assertTrue(userAgentHeader.startsWith("aws-lambda-java/")); + + String lambdaRuntimeErrorTypeHeader = recordedRequest.getHeader("Lambda-Runtime-Function-Error-Type"); + assertEquals("Runtime.AfterRestoreError", lambdaRuntimeErrorTypeHeader); + + String actualBody = recordedRequest.getBody().readUtf8(); + assertEquals("{\"errorMessage\":\"testErrorMessage\",\"errorType\":\"testErrorType\",\"stackTrace\":[\"item0\",\"item1\",\"item2\"]}", actualBody); + } catch(Exception e) { + fail(); + } + } + + @Test + public void reportRestoreErrorTestWrongStatusCode() { + int errorStatusCode = HTTP_INTERNAL_ERROR; + try { + RapidErrorType rapidErrorType = RapidErrorType.AfterRestoreError; + + MockResponse mockResponse = new MockResponse(); + mockResponse.setResponseCode(errorStatusCode); + mockWebServer.enqueue(mockResponse); + + LambdaError lambdaError = new LambdaError(errorRequest, rapidErrorType); + lambdaRuntimeApiClientImpl.reportRestoreError(lambdaError); + fail(); + } catch(LambdaRuntimeClientException e) { + String expectedUrl = "http://" + getHostnamePort() + "/2018-06-01/runtime/restore/error"; + String expectedMessage = expectedUrl + " Response code: '" + errorStatusCode + "'."; + assertEquals(expectedMessage, e.getLocalizedMessage()); + } catch(Exception e) { + fail(); + } + } + + @Test + public void reportInvocationErrorTest() { + try { + RapidErrorType rapidErrorType = RapidErrorType.AfterRestoreError; + + MockResponse mockResponse = new MockResponse(); + mockResponse.setResponseCode(HTTP_ACCEPTED); + mockWebServer.enqueue(mockResponse); + + LambdaError lambdaError = new LambdaError(errorRequest, rapidErrorType); + lambdaRuntimeApiClientImpl.reportInvocationError(requestId, lambdaError); + RecordedRequest recordedRequest = mockWebServer.takeRequest(); + HttpUrl actualUrl = recordedRequest.getRequestUrl(); + String expectedUrl = "http://" + getHostnamePort() + "/2018-06-01/runtime/invocation/1234/error"; + assertEquals(expectedUrl, actualUrl.toString()); + + String userAgentHeader = recordedRequest.getHeader("User-Agent"); + assertTrue(userAgentHeader.startsWith("aws-lambda-java/")); + + String lambdaRuntimeErrorTypeHeader = recordedRequest.getHeader("Lambda-Runtime-Function-Error-Type"); + assertEquals("Runtime.AfterRestoreError", lambdaRuntimeErrorTypeHeader); + + String actualBody = recordedRequest.getBody().readUtf8(); + assertEquals("{\"errorMessage\":\"testErrorMessage\",\"errorType\":\"testErrorType\",\"stackTrace\":[\"item0\",\"item1\",\"item2\"]}", actualBody); + } catch(Exception e) { + fail(); + } + } + + @Test + public void reportInvocationErrorTestWrongStatusCode() { + int errorStatusCode = HTTP_INTERNAL_ERROR; + try { + RapidErrorType rapidErrorType = RapidErrorType.AfterRestoreError; + + MockResponse mockResponse = new MockResponse(); + mockResponse.setResponseCode(errorStatusCode); + mockWebServer.enqueue(mockResponse); + + LambdaError lambdaError = new LambdaError(errorRequest, rapidErrorType); + lambdaRuntimeApiClientImpl.reportInvocationError(requestId, lambdaError); + fail(); + } catch(LambdaRuntimeClientException e) { + String expectedUrl = "http://" + getHostnamePort() + "/2018-06-01/runtime/invocation/1234/error"; + String expectedMessage = expectedUrl + " Response code: '" + errorStatusCode + "'."; + assertEquals(expectedMessage, e.getLocalizedMessage()); + } catch(Exception e) { + fail(); + } + } + + @Test + public void reportLambdaErrorWithXRayTest() { + try { + RapidErrorType rapidErrorType = RapidErrorType.AfterRestoreError; + + MockResponse mockResponse = new MockResponse(); + mockResponse.setResponseCode(HTTP_ACCEPTED); + mockWebServer.enqueue(mockResponse); + + String workingDirectory = "my-test-directory"; + List paths = new ArrayList(); + paths.add("path-0"); + paths.add("path-1"); + paths.add("path-2"); + + List stackElements0 = new ArrayList<>(); + stackElements0.add(new StackElement("label0", "path0", 0)); + stackElements0.add(new StackElement("label1", "path1", 1)); + stackElements0.add(new StackElement("label2", "path2", 2)); + XRayException xRayException0 = new XRayException("my-test-message0", "my-test-type0", stackElements0); + + List stackElements1 = new ArrayList<>(); + stackElements1.add(new StackElement("label10", "path10", 0)); + stackElements1.add(new StackElement("label11", "path11", 11)); + stackElements1.add(new StackElement("label12", "path12", 12)); + XRayException xRayException1 = new XRayException("my-test-message1", "my-test-type0", stackElements1); + + List exceptions = new ArrayList<>(); + exceptions.add(xRayException0); + exceptions.add(xRayException1); + + XRayErrorCause xRayErrorCause = new XRayErrorCause(workingDirectory, exceptions, paths); + LambdaError lambdaError = new LambdaError(errorRequest, xRayErrorCause, rapidErrorType); + lambdaRuntimeApiClientImpl.reportInvocationError(requestId, lambdaError); + RecordedRequest recordedRequest = mockWebServer.takeRequest(); + + String xrayErrorCauseHeader = recordedRequest.getHeader("Lambda-Runtime-Function-XRay-Error-Cause"); + assertEquals("{\"working_directory\":\"my-test-directory\",\"exceptions\":[{\"message\":\"my-test-message0\",\"type\":\"my-test-type0\",\"stack\":[{\"label\":\"label0\"," + + "\"path\":\"path0\",\"line\":0},{\"label\":\"label1\",\"path\":\"path1\",\"line\":1},{\"label\":\"label2\",\"path\":\"path2\",\"line\":2}]},{\"message\":\"my-test-message1\"," + + "\"type\":\"my-test-type0\",\"stack\":[{\"label\":\"label10\",\"path\":\"path10\",\"line\":0},{\"label\":\"label11\",\"path\":\"path11\",\"line\":11},{\"label\":\"label12\"," + + "\"path\":\"path12\",\"line\":12}]}],\"paths\":[\"path-0\",\"path-1\",\"path-2\"]}", xrayErrorCauseHeader); + } catch(Exception e) { + fail(); + } + } + + @Test + public void reportInvocationSuccessTest() { + try { + MockResponse mockResponse = new MockResponse(); + mockResponse.setResponseCode(HTTP_ACCEPTED); + mockWebServer.enqueue(mockResponse); + + String response = "{\"msg\":\"test\"}"; + lambdaRuntimeApiClientImpl.reportInvocationSuccess(requestId, response.getBytes()); + RecordedRequest recordedRequest = mockWebServer.takeRequest(); + HttpUrl actualUrl = recordedRequest.getRequestUrl(); + String expectedUrl = "http://" + getHostnamePort() + "/2018-06-01/runtime/invocation/1234/response"; + assertEquals(expectedUrl, actualUrl.toString()); + + String actualBody = recordedRequest.getBody().readUtf8(); + assertEquals("{\"msg\":\"test\"}", actualBody); + } catch(Exception e) { + e.printStackTrace(); + fail(); + } + } + + @Test + public void restoreNextTest() { + try { + MockResponse mockResponse = new MockResponse(); + mockResponse.setResponseCode(HTTP_OK); + mockWebServer.enqueue(mockResponse); + + lambdaRuntimeApiClientImpl.restoreNext(); + RecordedRequest recordedRequest = mockWebServer.takeRequest(); + HttpUrl actualUrl = recordedRequest.getRequestUrl(); + String expectedUrl = "http://" + getHostnamePort() + "/2018-06-01/runtime/restore/next"; + assertEquals(expectedUrl, actualUrl.toString()); + + String actualBody = recordedRequest.getBody().readUtf8(); + assertEquals("", actualBody); + } catch(Exception e) { + e.printStackTrace(); + fail(); + } + } + + @Test + public void restoreNextWrongStatusCodeTest() { + int errorStatusCode = HTTP_INTERNAL_ERROR; + try { + MockResponse mockResponse = new MockResponse(); + mockResponse.setResponseCode(errorStatusCode); + mockWebServer.enqueue(mockResponse); + + lambdaRuntimeApiClientImpl.restoreNext(); + fail(); + } catch(LambdaRuntimeClientException e) { + String expectedUrl = "http://" + getHostnamePort() + "/2018-06-01/runtime/restore/next"; + String expectedMessage = expectedUrl + " Response code: '" + errorStatusCode + "'."; + assertEquals(expectedMessage, e.getLocalizedMessage()); + } catch(Exception e) { + fail(); + } + } + + @Test + public void nextWithoutTenantIdHeaderTest() { + try { + MockResponse mockResponse = buildMockResponseForNextInvocation(); + mockWebServer.enqueue(mockResponse); + + InvocationRequest invocationRequest = lambdaRuntimeApiClientImpl.nextInvocation(); + verifyNextInvocationRequest(); + assertNull(invocationRequest.getTenantId()); + } catch(Exception e) { + fail(); + } + } + + @Test + public void nextWithTenantIdHeaderTest() { + try { + MockResponse mockResponse = buildMockResponseForNextInvocation(); + String expectedTenantId = "my-tenant-id"; + mockResponse.setHeader("lambda-runtime-aws-tenant-id", expectedTenantId); + mockWebServer.enqueue(mockResponse); + + InvocationRequest invocationRequest = lambdaRuntimeApiClientImpl.nextInvocation(); + verifyNextInvocationRequest(); + assertEquals(expectedTenantId, invocationRequest.getTenantId()); + + } catch(Exception e) { + fail(); + } + } + + @Test + public void nextWithEmptyTenantIdHeaderTest() { + try { + MockResponse mockResponse = buildMockResponseForNextInvocation(); + mockResponse.setHeader("lambda-runtime-aws-tenant-id", ""); + mockWebServer.enqueue(mockResponse); + + InvocationRequest invocationRequest = lambdaRuntimeApiClientImpl.nextInvocation(); + verifyNextInvocationRequest(); + assertNull(invocationRequest.getTenantId()); + } catch(Exception e) { + fail(); + } + } + + @Test + public void nextWithNullTenantIdHeaderTest() { + try { + MockResponse mockResponse = buildMockResponseForNextInvocation(); + assertThrows(NullPointerException.class, () -> { + mockResponse.setHeader("lambda-runtime-aws-tenant-id", null); + }); + } catch(Exception e) { + fail(); + } + } + + @Test + public void createUrlMalformedTest() { + RapidErrorType rapidErrorType = RapidErrorType.AfterRestoreError; + LambdaError lambdaError = new LambdaError(errorRequest, rapidErrorType); + RuntimeException thrown = assertThrows(RuntimeException.class, ()->{ + lambdaRuntimeApiClientImpl.reportLambdaError("invalidurl", lambdaError, 100); + }); + assertTrue(thrown.getLocalizedMessage().contains("java.net.MalformedURLException")); + } + + @Test + public void lambdaReportErrorXRayHeaderTooLongTest() { + try { + RapidErrorType rapidErrorType = RapidErrorType.AfterRestoreError; + + MockResponse mockResponse = new MockResponse(); + mockResponse.setResponseCode(HTTP_ACCEPTED); + mockWebServer.enqueue(mockResponse); + + String workingDirectory = "my-test-directory"; + List paths = new ArrayList(); + paths.add("path-0"); + + List stackElements = new ArrayList<>(); + stackElements.add(new StackElement("label0", "path0", 0)); + XRayException xRayException = new XRayException("my-test-message0", "my-test-type0", stackElements); + + List exceptions = new ArrayList<>(); + exceptions.add(xRayException); + + XRayErrorCause xRayErrorCause = new XRayErrorCause(workingDirectory, exceptions, paths); + LambdaError lambdaError = new LambdaError(errorRequest, xRayErrorCause, rapidErrorType); + lambdaRuntimeApiClientImpl.reportLambdaError("http://" + getHostnamePort(), lambdaError, 10); + RecordedRequest recordedRequest = mockWebServer.takeRequest(); + + String xrayErrorCauseHeader = recordedRequest.getHeader("Lambda-Runtime-Function-XRay-Error-Cause"); + assertNull(xrayErrorCauseHeader); + } catch(Exception e) { + fail(); + } + } + + private MockResponse buildMockResponseForNextInvocation() { + MockResponse mockResponse = new MockResponse(); + mockResponse.setResponseCode(HTTP_ACCEPTED); + mockResponse.setHeader("lambda-runtime-aws-request-id", "1234567890"); + mockResponse.setHeader("Content-Type", "application/json"); + return mockResponse; + } + + private void verifyNextInvocationRequest() throws Exception { + RecordedRequest recordedRequest = mockWebServer.takeRequest(); + HttpUrl actualUrl = recordedRequest.getRequestUrl(); + String expectedUrl = "http://" + getHostnamePort() + "/2018-06-01/runtime/invocation/next"; + assertEquals(expectedUrl, actualUrl.toString()); + + String actualBody = recordedRequest.getBody().readUtf8(); + assertEquals("", actualBody); + } + + private String getHostnamePort() { + return mockWebServer.getHostName() + ":" + mockWebServer.getPort(); + } +} \ 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/runtimeapi/converters/LambdaErrorConverterTest.java b/aws-lambda-java-runtime-interface-client/src/test/java/com/amazonaws/services/lambda/runtime/api/client/runtimeapi/converters/LambdaErrorConverterTest.java new file mode 100644 index 000000000..f94bc0c5f --- /dev/null +++ b/aws-lambda-java-runtime-interface-client/src/test/java/com/amazonaws/services/lambda/runtime/api/client/runtimeapi/converters/LambdaErrorConverterTest.java @@ -0,0 +1,112 @@ +/* +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.converters; + +import com.amazonaws.services.lambda.runtime.api.client.UserFault; +import com.amazonaws.services.lambda.runtime.api.client.runtimeapi.dto.ErrorRequest; +import org.junit.jupiter.api.Test; +import static org.junit.jupiter.api.Assertions.*; + +class LambdaErrorConverterTest { + + @Test + void testFromUserFaultWithMessageAndException() { + UserFault userFault = new UserFault("Test error message", "TestException", "Test stack trace"); + ErrorRequest errorRequest = LambdaErrorConverter.fromUserFault(userFault); + + assertNotNull(errorRequest); + assertEquals("Test error message", errorRequest.errorMessage); + assertEquals("TestException", errorRequest.errorType); + assertNull(errorRequest.stackTrace); + } + + @Test + void testFromUserFaultWithNullValues() { + UserFault userFault = new UserFault(null, null, null); + ErrorRequest errorRequest = LambdaErrorConverter.fromUserFault(userFault); + + assertNotNull(errorRequest); + assertNull(errorRequest.errorMessage); + assertNull(errorRequest.errorType); + assertNull(errorRequest.stackTrace); + } + + @Test + void testFromUserFaultWithFatalError() { + UserFault userFault = new UserFault("Fatal error", "FatalException", "Test stack trace", true); + ErrorRequest errorRequest = LambdaErrorConverter.fromUserFault(userFault); + + assertNotNull(errorRequest); + assertEquals("Fatal error", errorRequest.errorMessage); + assertEquals("FatalException", errorRequest.errorType); + assertNull(errorRequest.stackTrace); + } + + @Test + void testFromUserFaultCreatedFromException() { + Exception exception = new RuntimeException("Test exception message"); + UserFault userFault = UserFault.makeUserFault(exception); + ErrorRequest errorRequest = LambdaErrorConverter.fromUserFault(userFault); + + assertNotNull(errorRequest); + assertEquals("Test exception message", errorRequest.errorMessage); + assertEquals("java.lang.RuntimeException", errorRequest.errorType); + assertNull(errorRequest.stackTrace); + } + + @Test + void testFromUserFaultCreatedFromMessage() { + UserFault userFault = UserFault.makeUserFault("Simple message"); + ErrorRequest errorRequest = LambdaErrorConverter.fromUserFault(userFault); + + assertNotNull(errorRequest); + assertEquals("Simple message", errorRequest.errorMessage); + assertNull(errorRequest.errorType); + assertNull(errorRequest.stackTrace); + } + + @Test + void testFromThrowableWithMessage() { + Exception exception = new RuntimeException("Test exception message"); + ErrorRequest errorRequest = LambdaErrorConverter.fromThrowable(exception); + + assertNotNull(errorRequest); + assertEquals("Test exception message", errorRequest.errorMessage); + assertEquals("java.lang.RuntimeException", errorRequest.errorType); + assertNotNull(errorRequest.stackTrace); + assertTrue(errorRequest.stackTrace.length > 0); + } + + @Test + void testFromThrowableWithNullMessage() { + Exception exception = new RuntimeException(); + ErrorRequest errorRequest = LambdaErrorConverter.fromThrowable(exception); + + assertNotNull(errorRequest); + assertEquals("java.lang.RuntimeException", errorRequest.errorMessage); + assertEquals("java.lang.RuntimeException", errorRequest.errorType); + assertNotNull(errorRequest.stackTrace); + assertTrue(errorRequest.stackTrace.length > 0); + } + + @Test + void testFromThrowableStackTraceContent() { + Exception exception = new RuntimeException("Test message"); + ErrorRequest errorRequest = LambdaErrorConverter.fromThrowable(exception); + + String[] stackTrace = errorRequest.stackTrace; + assertNotNull(stackTrace); + assertTrue(stackTrace.length > 0); + + boolean foundTestClass = false; + for (String traceLine : stackTrace) { + if (traceLine.contains(LambdaErrorConverterTest.class.getSimpleName())) { + foundTestClass = true; + break; + } + } + assertTrue(foundTestClass); + } +} 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/src/test/java/com/amazonaws/services/lambda/runtime/api/client/util/EnvWriterTest.java b/aws-lambda-java-runtime-interface-client/src/test/java/com/amazonaws/services/lambda/runtime/api/client/util/EnvWriterTest.java deleted file mode 100644 index cae37317c..000000000 --- a/aws-lambda-java-runtime-interface-client/src/test/java/com/amazonaws/services/lambda/runtime/api/client/util/EnvWriterTest.java +++ /dev/null @@ -1,118 +0,0 @@ -/* Copyright 2019 Amazon.com, Inc. or its affiliates. All Rights Reserved. */ - -package com.amazonaws.services.lambda.runtime.api.client.util; - -import org.junit.jupiter.api.AfterEach; -import org.junit.jupiter.api.Test; - -import java.util.HashMap; -import java.util.Map; - -import static java.util.Collections.unmodifiableMap; -import static org.junit.jupiter.api.Assertions.assertEquals; -import static org.junit.jupiter.api.Assertions.assertNull; - -public class EnvWriterTest { - - private EnvWriter underTest; - - private EnvReader envReader; - - @AfterEach - public void tearDown() { - if(underTest != null) - underTest.close(); - } - - @Test - public void shouldModifyEnv() { - String key = "test"; - String expected = "notNullValue"; - setEnv(expected, key); - - underTest.modifyEnv(env -> env.put(key, expected)); - - assertEquals(envReader.getEnv(key), expected); - } - - @Test - public void shouldRemoveEnvironmentCredentialsWhenTheyAreEmpty() { - setEnv("", "AWS_ACCESS_KEY_ID", "AWS_SECRET_ACCESS_KEY", "AWS_SESSION_TOKEN"); - - underTest.setupEnvironmentCredentials(); - - assertNull(envReader.getEnv("AWS_ACCESS_KEY_ID")); - assertNull(envReader.getEnv("AWS_SECRET_ACCESS_KEY")); - assertNull(envReader.getEnv("AWS_SESSION_TOKEN")); - } - - @Test - public void shouldSetupEnvironmentCredentialsWhenTheyAreNotEmpty() { - setEnv("notEmptyValue", "AWS_ACCESS_KEY_ID", "AWS_SECRET_ACCESS_KEY"); - - underTest.setupEnvironmentCredentials(); - - assertEquals(envReader.getEnv("AWS_ACCESS_KEY"), envReader.getEnv("AWS_ACCESS_KEY_ID")); - assertEquals(envReader.getEnv("AWS_SECRET_KEY"), envReader.getEnv("AWS_SECRET_ACCESS_KEY")); - } - - @Test - public void shouldSetTheAwsExecutionEnvVar_Java8() { - // Given - EnvReaderMock reader = new EnvReaderMock(unmodifiableMap(new HashMap<>())); - EnvWriter writer = new EnvWriter(reader); - - // When - System.setProperty("java.version", "1.8.0_202"); - writer.setupAwsExecutionEnv(); - - // Then - assertEquals("AWS_Lambda_java8", reader.getEnv("AWS_EXECUTION_ENV")); - } - - @Test - public void shouldSetTheAwsExecutionEnvVar_Java11() { - // Given - EnvReaderMock reader = new EnvReaderMock(unmodifiableMap(new HashMap<>())); - EnvWriter writer = new EnvWriter(reader); - - // When - System.setProperty("java.version", "11.0.3"); - writer.setupAwsExecutionEnv(); - - // Then - assertEquals("AWS_Lambda_java11", reader.getEnv("AWS_EXECUTION_ENV")); - } - - private void setEnv(String value, String... keys) { - Map mutableMap = new HashMap<>(); - for (String key : keys) { - mutableMap.put(key, value); - } - - Map unmodifiableMap = unmodifiableMap(mutableMap); - EnvReader envReader = new EnvReaderMock(unmodifiableMap); - this.envReader = envReader; - this.underTest = new EnvWriter(envReader); - } - - private static class EnvReaderMock extends EnvReader { - - private final Map env; - - EnvReaderMock(Map env) { - this.env = env; - } - - @Override - public Map getEnv() { - return env; - } - - @Override - public String getEnv(String envVariableName) { - return env.get(envVariableName); - } - } - -} diff --git a/aws-lambda-java-runtime-interface-client/src/test/java/com/amazonaws/services/lambda/runtime/api/client/util/LambdaOutputStreamTest.java b/aws-lambda-java-runtime-interface-client/src/test/java/com/amazonaws/services/lambda/runtime/api/client/util/LambdaOutputStreamTest.java new file mode 100644 index 000000000..30146ea84 --- /dev/null +++ b/aws-lambda-java-runtime-interface-client/src/test/java/com/amazonaws/services/lambda/runtime/api/client/util/LambdaOutputStreamTest.java @@ -0,0 +1,81 @@ +/* +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 org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; + +import java.io.IOException; +import java.io.OutputStream; + +import static org.mockito.Mockito.*; +import static org.junit.jupiter.api.Assertions.*; + +@ExtendWith(MockitoExtension.class) +public class LambdaOutputStreamTest { + + @Mock + private OutputStream mockInnerStream; + + private LambdaOutputStream lambdaOutputStream; + + @BeforeEach + void setUp() { + lambdaOutputStream = new LambdaOutputStream(mockInnerStream); + } + + @Test + void writeSingleByte() throws IOException { + int testByte = 65; // 'A' + lambdaOutputStream.write(testByte); + verify(mockInnerStream).write(new byte[]{(byte) testByte}, 0, 1); + } + + @Test + void writeByteArray() throws IOException { + byte[] testBytes = "test".getBytes(); + lambdaOutputStream.write(testBytes); + verify(mockInnerStream).write(testBytes, 0, testBytes.length); + } + + @Test + void writeOffsetLength() throws IOException { + byte[] testBytes = "test".getBytes(); + int offset = 1; + int length = 2; + lambdaOutputStream.write(testBytes, offset, length); + verify(mockInnerStream).write(testBytes, offset, length); + } + + @Test + void throwWriteSingleByte() throws IOException { + doThrow(new IOException("Test exception")) + .when(mockInnerStream) + .write(any(byte[].class), anyInt(), anyInt()); + assertThrows(IOException.class, () -> lambdaOutputStream.write(65)); + } + + @Test + void throwWriteByteArray() throws IOException { + byte[] testBytes = "test".getBytes(); + doThrow(new IOException("Test exception")) + .when(mockInnerStream) + .write(any(byte[].class), anyInt(), anyInt()); + assertThrows(IOException.class, () -> lambdaOutputStream.write(testBytes)); + } + + @Test + void throwWriteOffsetLength() throws IOException { + byte[] testBytes = "test".getBytes(); + doThrow(new IOException("Test exception")) + .when(mockInnerStream) + .write(any(byte[].class), anyInt(), anyInt()); + assertThrows(IOException.class, () -> lambdaOutputStream.write(testBytes, 1, 2)); + } +} diff --git a/aws-lambda-java-runtime-interface-client/src/test/java/com/amazonaws/services/lambda/runtime/api/client/util/UnsafeUtilTest.java b/aws-lambda-java-runtime-interface-client/src/test/java/com/amazonaws/services/lambda/runtime/api/client/util/UnsafeUtilTest.java new file mode 100644 index 000000000..b1f0592f0 --- /dev/null +++ b/aws-lambda-java-runtime-interface-client/src/test/java/com/amazonaws/services/lambda/runtime/api/client/util/UnsafeUtilTest.java @@ -0,0 +1,56 @@ +/* +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 org.junit.jupiter.api.Test; +import java.lang.reflect.Field; +import static org.junit.jupiter.api.Assertions.*; + +public class UnsafeUtilTest { + + @Test + void testTheUnsafeIsInitialized() { + assertNotNull(UnsafeUtil.TheUnsafe); + } + + @Test + void testThrowException() { + Exception testException = new Exception("Test exception"); + + try { + UnsafeUtil.throwException(testException); + fail("Should have thrown an exception"); + } catch (Throwable e) { + assertEquals("Test exception", e.getMessage()); + assertSame(testException, e); + } + } + + @Test + void testDisableIllegalAccessWarning() { + assertDoesNotThrow(() -> UnsafeUtil.disableIllegalAccessWarning()); + try { + Class illegalAccessLoggerClass = Class.forName("jdk.internal.module.IllegalAccessLogger"); + Field loggerField = illegalAccessLoggerClass.getDeclaredField("logger"); + loggerField.setAccessible(true); + Object loggerValue = loggerField.get(null); + assertNull(loggerValue); + } catch (ClassNotFoundException e) { + assertTrue(true); + } catch (NoSuchFieldException e) { + assertTrue(true); + } catch (Exception e) { + fail("Unexpected exception: " + e.getMessage()); + } + } + + @Test + void testPrivateConstructor() { + assertThrows(IllegalAccessException.class, () -> { + UnsafeUtil.class.getDeclaredConstructor().newInstance(); + }); + } +} diff --git a/aws-lambda-java-runtime-interface-client/src/test/java/com/amazonaws/services/lambda/runtime/serialization/factories/JacksonFactoryTest.java b/aws-lambda-java-runtime-interface-client/src/test/java/com/amazonaws/services/lambda/runtime/serialization/factories/JacksonFactoryTest.java deleted file mode 100644 index 8fc97f2b0..000000000 --- a/aws-lambda-java-runtime-interface-client/src/test/java/com/amazonaws/services/lambda/runtime/serialization/factories/JacksonFactoryTest.java +++ /dev/null @@ -1,18 +0,0 @@ -/* Copyright 2019 Amazon.com, Inc. or its affiliates. All Rights Reserved. */ - -package com.amazonaws.services.lambda.runtime.serialization.factories; - -import org.junit.jupiter.api.Test; - -import static org.junit.jupiter.api.Assertions.assertNotNull; - -public class JacksonFactoryTest { - - @Test - public void deserializeVoidAsNonNull() throws Exception { - JacksonFactory instance = JacksonFactory.getInstance(); - Void actual = instance.getMapper().readValue("{}", Void.class); - assertNotNull(actual); - } - -} diff --git a/aws-lambda-java-runtime-interface-client/src/test/java/test/lambda/handlers/POJOHanlderImpl.java b/aws-lambda-java-runtime-interface-client/src/test/java/test/lambda/handlers/POJOHanlderImpl.java new file mode 100644 index 000000000..ca1a6bd4f --- /dev/null +++ b/aws-lambda-java-runtime-interface-client/src/test/java/test/lambda/handlers/POJOHanlderImpl.java @@ -0,0 +1,26 @@ +package test.lambda.handlers; + +import com.amazonaws.services.lambda.runtime.Context; + +@SuppressWarnings("unused") +public class POJOHanlderImpl { + @SuppressWarnings("unused") + public String noParamsHandler() { + return "success"; + } + + @SuppressWarnings("unused") + public String oneParamHandler_event(String event) { + return "success"; + } + + @SuppressWarnings("unused") + public String oneParamHandler_context(Context context) { + return "success"; + } + + @SuppressWarnings("unused") + public String twoParamsHandler(String event, Context context) { + return "success"; + } +} diff --git a/aws-lambda-java-runtime-interface-client/src/test/java/test/lambda/handlers/RequestHandlerImpl.java b/aws-lambda-java-runtime-interface-client/src/test/java/test/lambda/handlers/RequestHandlerImpl.java new file mode 100644 index 000000000..47fbade4d --- /dev/null +++ b/aws-lambda-java-runtime-interface-client/src/test/java/test/lambda/handlers/RequestHandlerImpl.java @@ -0,0 +1,12 @@ +package test.lambda.handlers; + +import com.amazonaws.services.lambda.runtime.Context; +import com.amazonaws.services.lambda.runtime.RequestHandler; + + +public class RequestHandlerImpl implements RequestHandler { + @Override + public String handleRequest(String event, Context context) { + return "success"; + } +} diff --git a/aws-lambda-java-runtime-interface-client/src/test/java/test/lambda/handlers/RequestStreamHandlerImpl.java b/aws-lambda-java-runtime-interface-client/src/test/java/test/lambda/handlers/RequestStreamHandlerImpl.java new file mode 100644 index 000000000..2bf2212c1 --- /dev/null +++ b/aws-lambda-java-runtime-interface-client/src/test/java/test/lambda/handlers/RequestStreamHandlerImpl.java @@ -0,0 +1,16 @@ +package test.lambda.handlers; + +import com.amazonaws.services.lambda.runtime.Context; +import com.amazonaws.services.lambda.runtime.RequestStreamHandler; + +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; + +@SuppressWarnings("unused") +public class RequestStreamHandlerImpl implements RequestStreamHandler { + @Override + public void handleRequest(InputStream input, OutputStream output, Context context) throws IOException { + output.write("\"success\"".getBytes()); + } +} diff --git a/aws-lambda-java-runtime-interface-client/src/test/java/testpkg/StackTraceHelper.java b/aws-lambda-java-runtime-interface-client/src/test/java/testpkg/StackTraceHelper.java index b9442c25e..c7d8cb834 100644 --- a/aws-lambda-java-runtime-interface-client/src/test/java/testpkg/StackTraceHelper.java +++ b/aws-lambda-java-runtime-interface-client/src/test/java/testpkg/StackTraceHelper.java @@ -2,9 +2,11 @@ package testpkg; +import com.amazonaws.services.lambda.crac.CheckpointException; + /** - * A helper class for throwing exception which is not in the `lambdainternal` package to avoid the stack traces from - * being filtered out. + * A helper class for throwing exception which is not in the com.amazonaws.services.lambda.runtime.api.client package + * to avoid the stack traces from being filtered out. * */ public class StackTraceHelper { @@ -21,4 +23,11 @@ public static void throwRuntimeException(String msg){ public static void callThenThrowRuntimeException(String msg){ throwRuntimeException(msg); } + + public static void throwCheckpointExceptionWithTwoSuppressedExceptions(String msg1, String msg2) throws CheckpointException { + CheckpointException e1 = new CheckpointException(); + e1.addSuppressed(new RuntimeException(msg1)); + e1.addSuppressed(new RuntimeException(msg2)); + throw e1; + } } 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-local/Dockerfile.agent b/aws-lambda-java-runtime-interface-client/test/integration/codebuild-local/Dockerfile.agent index 120bdad7f..3dbdb3c68 100644 --- a/aws-lambda-java-runtime-interface-client/test/integration/codebuild-local/Dockerfile.agent +++ b/aws-lambda-java-runtime-interface-client/test/integration/codebuild-local/Dockerfile.agent @@ -1,5 +1,14 @@ -FROM amazonlinux:2 +FROM public.ecr.aws/amazoncorretto/amazoncorretto:8 +# Install docker and buildx extension RUN amazon-linux-extras enable docker && \ yum clean metadata && \ - yum install -y docker tar maven + yum install -y docker tar gzip unzip file + +COPY --from=docker/buildx-bin:latest /buildx /usr/libexec/docker/cli-plugins/docker-buildx + +# Install maven from apache.org, as version in the yum repository doesn't support latest maven plugins +ENV PATH="$PATH:/apache-maven/bin" +RUN mkdir /apache-maven && \ + curl https://archive.apache.org/dist/maven/maven-3/3.8.7/binaries/apache-maven-3.8.7-bin.tar.gz | \ + tar -xz -C /apache-maven --strip-components 1 \ No newline at end of file diff --git a/aws-lambda-java-runtime-interface-client/test/integration/codebuild-local/codebuild_build.sh b/aws-lambda-java-runtime-interface-client/test/integration/codebuild-local/codebuild_build.sh index ffadfa308..2a6ffa972 100755 --- a/aws-lambda-java-runtime-interface-client/test/integration/codebuild-local/codebuild_build.sh +++ b/aws-lambda-java-runtime-interface-client/test/integration/codebuild-local/codebuild_build.sh @@ -1,5 +1,5 @@ #!/bin/bash -# This file is copied from https://github.com/aws/aws-codebuild-docker-images/blob/f0912e4b16e427da35351fc102f0f56f4ceb938a/local_builds/codebuild_build.sh +# This file is copied from https://github.com/aws/aws-codebuild-docker-images/blob/282c6634e8c83c2a9841719b09aabfced3461981/local_builds/codebuild_build.sh function allOSRealPath() { if isOSWindows @@ -36,6 +36,7 @@ function usage { echo " -a Used to specify an artifact output directory." echo "Options:" echo " -l IMAGE Used to override the default local agent image." + echo " -r Used to specify a report output directory." echo " -s Used to specify source information. Defaults to the current working directory for primary source." echo " * First (-s) is for primary source" echo " * Use additional (-s) in : format for secondary source" @@ -61,10 +62,11 @@ awsconfig_flag=false mount_src_dir_flag=false docker_privileged_mode_flag=false -while getopts "cmdi:a:s:b:e:l:p:h" opt; do +while getopts "cmdi:a:r:s:b:e:l:p:h" opt; do case $opt in i ) image_flag=true; image_name=$OPTARG;; a ) artifact_flag=true; artifact_dir=$OPTARG;; + r ) report_dir=$OPTARG;; b ) buildspec=$OPTARG;; c ) awsconfig_flag=true;; m ) mount_src_dir_flag=true;; @@ -106,6 +108,11 @@ fi docker_command+="\"IMAGE_NAME=$image_name\" -e \ \"ARTIFACTS=$(allOSRealPath "$artifact_dir")\"" +if [ -n "$report_dir" ] +then + docker_command+=" -e \"REPORTS=$(allOSRealPath "$report_dir")\"" +fi + if [ -z "$source_dirs" ] then docker_command+=" -e \"SOURCE=$(allOSRealPath "$PWD")\"" @@ -176,7 +183,12 @@ else docker_command+=" -e \"INITIATOR=$USER\"" fi -docker_command+=" amazon/aws-codebuild-local:latest" +if [ -n "$local_agent_image" ] +then + docker_command+=" $local_agent_image" +else + docker_command+=" public.ecr.aws/codebuild/local-builds:latest" +fi # Note we do not expose the AWS_SECRET_ACCESS_KEY or the AWS_SESSION_TOKEN exposed_command=$docker_command diff --git a/aws-lambda-java-runtime-interface-client/test/integration/codebuild-local/test_all.sh b/aws-lambda-java-runtime-interface-client/test/integration/codebuild-local/test_all.sh index bd0eeda34..77bcb5933 100755 --- a/aws-lambda-java-runtime-interface-client/test/integration/codebuild-local/test_all.sh +++ b/aws-lambda-java-runtime-interface-client/test/integration/codebuild-local/test_all.sh @@ -18,16 +18,19 @@ do_one_yaml() { OS_DISTRIBUTION=$(grep -oE 'OS_DISTRIBUTION:\s*(\S+)' "$YML" | cut -d' ' -f2) DISTRO_VERSIONS=$(sed '1,/DISTRO_VERSION/d;/RUNTIME_VERSION/,$d' "$YML" | tr -d '\-" ') - RUNTIME_VERSIONS=$(sed '1,/RUNTIME_VERSION/d;/phases/,$d' "$YML" | sed '/#.*$/d' | tr -d '\-" ') + RUNTIME_VERSIONS=$(sed '1,/RUNTIME_VERSION/d;/PLATFORM/,$d' "$YML" | sed '/#.*$/d' | tr -d '\-" ') + PLATFORMS=$(sed '1,/PLATFORM/d;/phases/,$d' "$YML" | tr -d '\-" ') - for DISTRO_VERSION in $DISTRO_VERSIONS ; do - for RUNTIME_VERSION in $RUNTIME_VERSIONS ; do - if (( DRYRUN == 1 )) ; then - echo DRYRUN test_one_combination "$YML" "$OS_DISTRIBUTION" "$DISTRO_VERSION" "$RUNTIME_VERSION" - else - test_one_combination "$YML" "$OS_DISTRIBUTION" "$DISTRO_VERSION" "$RUNTIME_VERSION" - fi + for DISTRO_VERSION in $DISTRO_VERSIONS; do + for RUNTIME_VERSION in $RUNTIME_VERSIONS; do + for PLATFORM in $PLATFORMS; do + if (( DRYRUN == 1 )); then + echo DRYRUN test_one_combination "$YML" "$OS_DISTRIBUTION" "$DISTRO_VERSION" "$RUNTIME_VERSION" "$PLATFORM" + else + test_one_combination "$YML" "$OS_DISTRIBUTION" "$DISTRO_VERSION" "$RUNTIME_VERSION" "$PLATFORM" + fi done + done done } @@ -36,13 +39,15 @@ test_one_combination() { local -r OS_DISTRIBUTION="$2" local -r DISTRO_VERSION="$3" local -r RUNTIME_VERSION="$4" - + local -r PLATFORM="$5" + local -r PLATFORM_SANITIZED=$(echo "$PLATFORM" | tr "/" ".") + echo Testing: echo " BUILDSPEC" "$YML" - echo " with" "$OS_DISTRIBUTION"-"$DISTRO_VERSION" "$RUNTIME_VERSION" + echo " with" "$OS_DISTRIBUTION"-"$DISTRO_VERSION" "$RUNTIME_VERSION" "$PLATFORM" - "$(dirname "$0")"/test_one.sh "$YML" "$OS_DISTRIBUTION" "$DISTRO_VERSION" "$RUNTIME_VERSION" \ - > >(sed "s/^/$OS_DISTRIBUTION$DISTRO_VERSION-$RUNTIME_VERSION: /") 2> >(sed "s/^/$OS_DISTRIBUTION-$DISTRO_VERSION:$RUNTIME_VERSION: /" >&2) + "$(dirname "$0")"/test_one.sh "$YML" "$OS_DISTRIBUTION" "$DISTRO_VERSION" "$RUNTIME_VERSION" "$PLATFORM" \ + > >(sed "s/^/$OS_DISTRIBUTION$DISTRO_VERSION-$RUNTIME_VERSION-$PLATFORM_SANITIZED: /") 2> >(sed "s/^/$OS_DISTRIBUTION-$DISTRO_VERSION:$RUNTIME_VERSION:$PLATFORM_SANITIZED: /" >&2) } main() { diff --git a/aws-lambda-java-runtime-interface-client/test/integration/codebuild-local/test_one.sh b/aws-lambda-java-runtime-interface-client/test/integration/codebuild-local/test_one.sh index ddc009745..ba49e826e 100755 --- a/aws-lambda-java-runtime-interface-client/test/integration/codebuild-local/test_one.sh +++ b/aws-lambda-java-runtime-interface-client/test/integration/codebuild-local/test_one.sh @@ -13,12 +13,22 @@ function usage { >&2 echo " os_distribution Used to specify the OS distribution to build." >&2 echo " distro_version Used to specify the distro version of ." >&2 echo " runtime_version Used to specify the runtime version to test on the selected ." + >&2 echo " platform Used to specify the architecture platform to test on the selected ." >&2 echo "Optional:" >&2 echo " env Additional environment variables file." } +# codebuild/local-builds images are not multi-architectural +function get_local_agent_image() { + if [[ "$(arch)" == "aarch64" ]]; then + echo "public.ecr.aws/codebuild/local-builds:aarch64" + else + echo "public.ecr.aws/codebuild/local-builds:latest" + fi +} + main() { - if (( $# != 3 && $# != 4)); then + if (( $# != 5 && $# != 6)); then >&2 echo "Invalid number of parameters." usage exit 1 @@ -28,9 +38,11 @@ main() { OS_DISTRIBUTION="$2" DISTRO_VERSION="$3" RUNTIME_VERSION="$4" - EXTRA_ENV="${5-}" + PLATFORM="$5" + PLATFORM_SANITIZED=$(echo "$PLATFORM" | tr "/" ".") + EXTRA_ENV="${6-}" - CODEBUILD_TEMP_DIR=$(mktemp -d codebuild."$OS_DISTRIBUTION"-"$DISTRO_VERSION"-"$RUNTIME_VERSION".XXXXXXXXXX) + CODEBUILD_TEMP_DIR=$(mktemp -d codebuild."$OS_DISTRIBUTION"-"$DISTRO_VERSION"-"$RUNTIME_VERSION"-"$PLATFORM_SANITIZED".XXXXXXXXXX) trap 'rm -rf $CODEBUILD_TEMP_DIR' EXIT # Create an env file for codebuild_build. @@ -43,8 +55,9 @@ main() { echo "OS_DISTRIBUTION=$OS_DISTRIBUTION" echo "DISTRO_VERSION=$DISTRO_VERSION" echo "RUNTIME_VERSION=$RUNTIME_VERSION" + echo "PLATFORM=$PLATFORM" } >> "$ENVFILE" - + ARTIFACTS_DIR="$CODEBUILD_TEMP_DIR/artifacts" mkdir -p "$ARTIFACTS_DIR" # Run CodeBuild local agent. @@ -53,7 +66,8 @@ main() { -a "$ARTIFACTS_DIR" \ -e "$ENVFILE" \ -b "$BUILDSPEC_YML" \ - -s "$(dirname $PWD)" + -s "$(dirname $PWD)" \ + -l "$(get_local_agent_image)" } main "$@" 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 910fa8aa3..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 @@ -4,6 +4,8 @@ env: variables: OS_DISTRIBUTION: alpine JAVA_BINARY_LOCATION: "/usr/bin/java" + DOCKER_CLI_EXPERIMENTAL: "enabled" + DOCKER_CLI_PLUGIN_DIR: "/root/.docker/cli-plugins" batch: build-matrix: static: @@ -14,12 +16,14 @@ batch: env: variables: DISTRO_VERSION: - - "3.9" - - "3.10" - - "3.11" - - "3.12" + - "3.13" + - "3.14" + - "3.15" RUNTIME_VERSION: - "corretto11" + PLATFORM: + - "linux/amd64" + - "linux/arm64/v8" phases: install: commands: @@ -31,26 +35,29 @@ phases: echo "Performing DockerHub login . . ." docker login -u $DOCKERHUB_USERNAME -p $DOCKERHUB_PASSWORD fi + - aws-lambda-java-runtime-interface-client/test/integration/codebuild/scripts/configure_multi_arch_env.sh pre_build: commands: + # Log some environment variables for troubleshooting + - (mvn -v) # 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 && mvn install -DargLineForReflectionTestOnly="") - (cd aws-lambda-java-runtime-interface-client/test/integration/test-handler && mvn install) - export IMAGE_TAG="java-${OS_DISTRIBUTION}-${DISTRO_VERSION}:${RUNTIME_VERSION}" - echo "Extracting and including Runtime Interface Emulator" - SCRATCH_DIR=".scratch" - mkdir "${SCRATCH_DIR}" - - ARCHITECTURE=$(arch) - > - if [[ "$ARCHITECTURE" == "x86_64" ]]; then + if [[ "$PLATFORM" == "linux/amd64" ]]; then RIE="aws-lambda-rie" - elif [[ "$ARCHITECTURE" == "aarch64" ]]; then + elif [[ "$PLATFORM" == "linux/arm64/v8" ]]; then RIE="aws-lambda-rie-arm64" else - echo "Architecture $ARCHITECTURE is not currently supported." + echo "Platform $PLATFORM is not currently supported." exit 1 fi - tar -xvf aws-lambda-java-runtime-interface-client/test/integration/resources/${RIE}.tar.gz --directory "${SCRATCH_DIR}" @@ -68,53 +75,12 @@ phases: docker build . \ -f "${SCRATCH_DIR}/Dockerfile.function.${OS_DISTRIBUTION}.tmp" \ -t "${IMAGE_TAG}" \ + --platform="${PLATFORM}" \ --build-arg RUNTIME_VERSION="${RUNTIME_VERSION}" \ --build-arg DISTRO_VERSION="${DISTRO_VERSION}" build: commands: - - set -x - - echo "Running Image ${IMAGE_TAG}" - - docker network create "${OS_DISTRIBUTION}-network" - - > - docker run \ - --detach \ - --name "${OS_DISTRIBUTION}-app" \ - --network "${OS_DISTRIBUTION}-network" \ - --entrypoint="" \ - "${IMAGE_TAG}" \ - sh -c "/usr/bin/${RIE} ${JAVA_BINARY_LOCATION} -jar ./HelloWorld-1.0.jar helloworld.App" - - sleep 2 - - > - docker run \ - --name "${OS_DISTRIBUTION}-tester" \ - --env "TARGET=${OS_DISTRIBUTION}-app" \ - --network "${OS_DISTRIBUTION}-network" \ - --entrypoint="" \ - "${IMAGE_TAG}" \ - sh -c 'curl -X POST "http://${TARGET}:8080/2015-03-31/functions/function/invocations" -d "{}" --max-time 10' - - actual="$(docker logs --tail 1 "${OS_DISTRIBUTION}-tester" | xargs)" - - expected='success' - - | - echo "Response: ${actual}" - if [[ "$actual" != "$expected" ]]; then - echo "fail! runtime: $RUNTIME - expected output $expected - got $actual" - exit -1 - fi + - aws-lambda-java-runtime-interface-client/test/integration/codebuild/scripts/run_invocation_test.sh finally: - - | - echo "---------Container Logs: ${OS_DISTRIBUTION}-app----------" - echo - docker logs "${OS_DISTRIBUTION}-app" || true - echo - echo "---------------------------------------------------" - echo "--------Container Logs: ${OS_DISTRIBUTION}-tester--------" - echo - docker logs "${OS_DISTRIBUTION}-tester" || true - echo - echo "---------------------------------------------------" - - echo "Cleaning up..." - - docker stop "${OS_DISTRIBUTION}-app" || true - - docker rm --force "${OS_DISTRIBUTION}-app" || true - - docker stop "${OS_DISTRIBUTION}-tester" || true - - docker rm --force "${OS_DISTRIBUTION}-tester" || true - - docker network rm "${OS_DISTRIBUTION}-network" || true \ No newline at end of file + - aws-lambda-java-runtime-interface-client/test/integration/codebuild/scripts/fetch_test_container_logs.sh + - aws-lambda-java-runtime-interface-client/test/integration/codebuild/scripts/clean_up.sh 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 b12b59aff..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 @@ -4,6 +4,8 @@ env: variables: OS_DISTRIBUTION: amazoncorretto JAVA_BINARY_LOCATION: "/usr/bin/java" + DOCKER_CLI_EXPERIMENTAL: "enabled" + DOCKER_CLI_PLUGIN_DIR: "/root/.docker/cli-plugins" batch: build-matrix: static: @@ -18,6 +20,9 @@ batch: RUNTIME_VERSION: - "8" - "11" + PLATFORM: + - "linux/amd64" + - "linux/arm64/v8" phases: install: commands: @@ -29,26 +34,29 @@ phases: echo "Performing DockerHub login . . ." docker login -u $DOCKERHUB_USERNAME -p $DOCKERHUB_PASSWORD fi + - aws-lambda-java-runtime-interface-client/test/integration/codebuild/scripts/configure_multi_arch_env.sh pre_build: commands: + # Log some environment variables for troubleshooting + - (mvn -v) # 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 && mvn install -DargLineForReflectionTestOnly="") - (cd aws-lambda-java-runtime-interface-client/test/integration/test-handler && mvn install) - export IMAGE_TAG="java-${OS_DISTRIBUTION}-${DISTRO_VERSION}:${RUNTIME_VERSION}" - echo "Extracting and including Runtime Interface Emulator" - SCRATCH_DIR=".scratch" - mkdir "${SCRATCH_DIR}" - - ARCHITECTURE=$(arch) - > - if [[ "$ARCHITECTURE" == "x86_64" ]]; then + if [[ "$PLATFORM" == "linux/amd64" ]]; then RIE="aws-lambda-rie" - elif [[ "$ARCHITECTURE" == "aarch64" ]]; then + elif [[ "$PLATFORM" == "linux/arm64/v8" ]]; then RIE="aws-lambda-rie-arm64" else - echo "Architecture $ARCHITECTURE is not currently supported." + echo "Platform $PLATFORM is not currently supported." exit 1 fi - tar -xvf aws-lambda-java-runtime-interface-client/test/integration/resources/${RIE}.tar.gz --directory "${SCRATCH_DIR}" @@ -63,53 +71,12 @@ phases: docker build . \ -f "${SCRATCH_DIR}/Dockerfile.function.${OS_DISTRIBUTION}.tmp" \ -t "${IMAGE_TAG}" \ + --platform="${PLATFORM}" \ --build-arg RUNTIME_VERSION="${RUNTIME_VERSION}" \ --build-arg DISTRO_VERSION="${DISTRO_VERSION}" build: commands: - - set -x - - echo "Running Image ${IMAGE_TAG}" - - docker network create "${OS_DISTRIBUTION}-network" - - > - docker run \ - --detach \ - --name "${OS_DISTRIBUTION}-app" \ - --network "${OS_DISTRIBUTION}-network" \ - --entrypoint="" \ - "${IMAGE_TAG}" \ - sh -c "/usr/bin/${RIE} ${JAVA_BINARY_LOCATION} -jar ./HelloWorld-1.0.jar helloworld.App" - - sleep 2 - - > - docker run \ - --name "${OS_DISTRIBUTION}-tester" \ - --env "TARGET=${OS_DISTRIBUTION}-app" \ - --network "${OS_DISTRIBUTION}-network" \ - --entrypoint="" \ - "${IMAGE_TAG}" \ - sh -c 'curl -X POST "http://${TARGET}:8080/2015-03-31/functions/function/invocations" -d "{}" --max-time 10' - - actual="$(docker logs --tail 1 "${OS_DISTRIBUTION}-tester" | xargs)" - - expected='success' - - | - echo "Response: ${actual}" - if [[ "$actual" != "$expected" ]]; then - echo "fail! runtime: $RUNTIME - expected output $expected - got $actual" - exit -1 - fi + - aws-lambda-java-runtime-interface-client/test/integration/codebuild/scripts/run_invocation_test.sh finally: - - | - echo "---------Container Logs: ${OS_DISTRIBUTION}-app----------" - echo - docker logs "${OS_DISTRIBUTION}-app" || true - echo - echo "---------------------------------------------------" - echo "--------Container Logs: ${OS_DISTRIBUTION}-tester--------" - echo - docker logs "${OS_DISTRIBUTION}-tester" || true - echo - echo "---------------------------------------------------" - - echo "Cleaning up..." - - docker stop "${OS_DISTRIBUTION}-app" || true - - docker rm --force "${OS_DISTRIBUTION}-app" || true - - docker stop "${OS_DISTRIBUTION}-tester" || true - - docker rm --force "${OS_DISTRIBUTION}-tester" || true - - docker network rm "${OS_DISTRIBUTION}-network" || true \ No newline at end of file + - aws-lambda-java-runtime-interface-client/test/integration/codebuild/scripts/fetch_test_container_logs.sh + - aws-lambda-java-runtime-interface-client/test/integration/codebuild/scripts/clean_up.sh 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 e49ee0f5a..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 @@ -17,6 +17,8 @@ batch: - "1" RUNTIME_VERSION: - "openjdk8" + PLATFORM: + - "linux/amd64" phases: install: commands: @@ -30,26 +32,20 @@ phases: fi pre_build: commands: + # Log some environment variables for troubleshooting + - (mvn -v) # 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 && mvn install -DmultiArch=false -DargLineForReflectionTestOnly="") - (cd aws-lambda-java-runtime-interface-client/test/integration/test-handler && mvn install) - export IMAGE_TAG="java-${OS_DISTRIBUTION}-${DISTRO_VERSION}:${RUNTIME_VERSION}" - echo "Extracting and including Runtime Interface Emulator" - SCRATCH_DIR=".scratch" - mkdir "${SCRATCH_DIR}" - - ARCHITECTURE=$(arch) - - > - if [[ "$ARCHITECTURE" == "x86_64" ]]; then - RIE="aws-lambda-rie" - elif [[ "$ARCHITECTURE" == "aarch64" ]]; then - RIE="aws-lambda-rie-arm64" - else - echo "Architecture $ARCHITECTURE is not currently supported." - exit 1 - fi + - RIE="aws-lambda-rie" - tar -xvf aws-lambda-java-runtime-interface-client/test/integration/resources/${RIE}.tar.gz --directory "${SCRATCH_DIR}" - > cp "aws-lambda-java-runtime-interface-client/test/integration/docker/Dockerfile.function.${OS_DISTRIBUTION}" \ @@ -66,49 +62,7 @@ phases: --build-arg DISTRO_VERSION="${DISTRO_VERSION}" build: commands: - - set -x - - echo "Running Image ${IMAGE_TAG}" - - docker network create "${OS_DISTRIBUTION}-network" - - > - docker run \ - --detach \ - --name "${OS_DISTRIBUTION}-app" \ - --network "${OS_DISTRIBUTION}-network" \ - --entrypoint="" \ - "${IMAGE_TAG}" \ - sh -c "/usr/bin/${RIE} ${JAVA_BINARY_LOCATION} -jar ./HelloWorld-1.0.jar helloworld.App" - - sleep 2 - - > - docker run \ - --name "${OS_DISTRIBUTION}-tester" \ - --env "TARGET=${OS_DISTRIBUTION}-app" \ - --network "${OS_DISTRIBUTION}-network" \ - --entrypoint="" \ - "${IMAGE_TAG}" \ - sh -c 'curl -X POST "http://${TARGET}:8080/2015-03-31/functions/function/invocations" -d "{}" --max-time 10' - - actual="$(docker logs --tail 1 "${OS_DISTRIBUTION}-tester" | xargs)" - - expected='success' - - | - echo "Response: ${actual}" - if [[ "$actual" != "$expected" ]]; then - echo "fail! runtime: $RUNTIME - expected output $expected - got $actual" - exit -1 - fi + - aws-lambda-java-runtime-interface-client/test/integration/codebuild/scripts/run_invocation_test.sh finally: - - | - echo "---------Container Logs: ${OS_DISTRIBUTION}-app----------" - echo - docker logs "${OS_DISTRIBUTION}-app" || true - echo - echo "---------------------------------------------------" - echo "--------Container Logs: ${OS_DISTRIBUTION}-tester--------" - echo - docker logs "${OS_DISTRIBUTION}-tester" || true - echo - echo "---------------------------------------------------" - - echo "Cleaning up..." - - docker stop "${OS_DISTRIBUTION}-app" || true - - docker rm --force "${OS_DISTRIBUTION}-app" || true - - docker stop "${OS_DISTRIBUTION}-tester" || true - - docker rm --force "${OS_DISTRIBUTION}-tester" || true - - docker network rm "${OS_DISTRIBUTION}-network" || true \ No newline at end of file + - aws-lambda-java-runtime-interface-client/test/integration/codebuild/scripts/fetch_test_container_logs.sh + - aws-lambda-java-runtime-interface-client/test/integration/codebuild/scripts/clean_up.sh 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 00a34e59a..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 @@ -4,6 +4,8 @@ env: variables: OS_DISTRIBUTION: amazonlinux JAVA_BINARY_LOCATION: "/usr/bin/java" + DOCKER_CLI_EXPERIMENTAL: "enabled" + DOCKER_CLI_PLUGIN_DIR: "/root/.docker/cli-plugins" batch: build-matrix: static: @@ -17,6 +19,9 @@ batch: - "2" RUNTIME_VERSION: - "openjdk8" + PLATFORM: + - "linux/amd64" + - "linux/arm64/v8" phases: install: commands: @@ -28,26 +33,29 @@ phases: echo "Performing DockerHub login . . ." docker login -u $DOCKERHUB_USERNAME -p $DOCKERHUB_PASSWORD fi + - aws-lambda-java-runtime-interface-client/test/integration/codebuild/scripts/configure_multi_arch_env.sh pre_build: commands: + # Log some environment variables for troubleshooting + - (mvn -v) # 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 && mvn install -DargLineForReflectionTestOnly="") - (cd aws-lambda-java-runtime-interface-client/test/integration/test-handler && mvn install) - export IMAGE_TAG="java-${OS_DISTRIBUTION}-${DISTRO_VERSION}:${RUNTIME_VERSION}" - echo "Extracting and including Runtime Interface Emulator" - SCRATCH_DIR=".scratch" - mkdir "${SCRATCH_DIR}" - - ARCHITECTURE=$(arch) - > - if [[ "$ARCHITECTURE" == "x86_64" ]]; then + if [[ "$PLATFORM" == "linux/amd64" ]]; then RIE="aws-lambda-rie" - elif [[ "$ARCHITECTURE" == "aarch64" ]]; then + elif [[ "$PLATFORM" == "linux/arm64/v8" ]]; then RIE="aws-lambda-rie-arm64" else - echo "Architecture $ARCHITECTURE is not currently supported." + echo "Platform $PLATFORM is not currently supported." exit 1 fi - tar -xvf aws-lambda-java-runtime-interface-client/test/integration/resources/${RIE}.tar.gz --directory "${SCRATCH_DIR}" @@ -62,53 +70,12 @@ phases: docker build . \ -f "${SCRATCH_DIR}/Dockerfile.function.${OS_DISTRIBUTION}.tmp" \ -t "${IMAGE_TAG}" \ + --platform="${PLATFORM}" \ --build-arg RUNTIME_VERSION="${RUNTIME_VERSION}" \ --build-arg DISTRO_VERSION="${DISTRO_VERSION}" build: commands: - - set -x - - echo "Running Image ${IMAGE_TAG}" - - docker network create "${OS_DISTRIBUTION}-network" - - > - docker run \ - --detach \ - --name "${OS_DISTRIBUTION}-app" \ - --network "${OS_DISTRIBUTION}-network" \ - --entrypoint="" \ - "${IMAGE_TAG}" \ - sh -c "/usr/bin/${RIE} ${JAVA_BINARY_LOCATION} -jar ./HelloWorld-1.0.jar helloworld.App" - - sleep 2 - - > - docker run \ - --name "${OS_DISTRIBUTION}-tester" \ - --env "TARGET=${OS_DISTRIBUTION}-app" \ - --network "${OS_DISTRIBUTION}-network" \ - --entrypoint="" \ - "${IMAGE_TAG}" \ - sh -c 'curl -X POST "http://${TARGET}:8080/2015-03-31/functions/function/invocations" -d "{}" --max-time 10' - - actual="$(docker logs --tail 1 "${OS_DISTRIBUTION}-tester" | xargs)" - - expected='success' - - | - echo "Response: ${actual}" - if [[ "$actual" != "$expected" ]]; then - echo "fail! runtime: $RUNTIME - expected output $expected - got $actual" - exit -1 - fi + - aws-lambda-java-runtime-interface-client/test/integration/codebuild/scripts/run_invocation_test.sh finally: - - | - echo "---------Container Logs: ${OS_DISTRIBUTION}-app----------" - echo - docker logs "${OS_DISTRIBUTION}-app" || true - echo - echo "---------------------------------------------------" - echo "--------Container Logs: ${OS_DISTRIBUTION}-tester--------" - echo - docker logs "${OS_DISTRIBUTION}-tester" || true - echo - echo "---------------------------------------------------" - - echo "Cleaning up..." - - docker stop "${OS_DISTRIBUTION}-app" || true - - docker rm --force "${OS_DISTRIBUTION}-app" || true - - docker stop "${OS_DISTRIBUTION}-tester" || true - - docker rm --force "${OS_DISTRIBUTION}-tester" || true - - docker network rm "${OS_DISTRIBUTION}-network" || true \ No newline at end of file + - aws-lambda-java-runtime-interface-client/test/integration/codebuild/scripts/fetch_test_container_logs.sh + - aws-lambda-java-runtime-interface-client/test/integration/codebuild/scripts/clean_up.sh 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 5fc211ed6..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 @@ -4,6 +4,8 @@ env: variables: OS_DISTRIBUTION: centos JAVA_BINARY_LOCATION: "/usr/bin/java" + DOCKER_CLI_EXPERIMENTAL: "enabled" + DOCKER_CLI_PLUGIN_DIR: "/root/.docker/cli-plugins" batch: build-matrix: static: @@ -15,9 +17,11 @@ batch: variables: DISTRO_VERSION: - "7" - - "8" RUNTIME_VERSION: - "corretto11" + PLATFORM: + - "linux/amd64" + - "linux/arm64/v8" phases: install: commands: @@ -29,11 +33,15 @@ phases: echo "Performing DockerHub login . . ." docker login -u $DOCKERHUB_USERNAME -p $DOCKERHUB_PASSWORD fi + - aws-lambda-java-runtime-interface-client/test/integration/codebuild/scripts/configure_multi_arch_env.sh pre_build: commands: + # Log some environment variables for troubleshooting + - (mvn -v) # 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) @@ -41,14 +49,13 @@ phases: - echo "Extracting and including Runtime Interface Emulator" - SCRATCH_DIR=".scratch" - mkdir "${SCRATCH_DIR}" - - ARCHITECTURE=$(arch) - > - if [[ "$ARCHITECTURE" == "x86_64" ]]; then + if [[ "$PLATFORM" == "linux/amd64" ]]; then RIE="aws-lambda-rie" - elif [[ "$ARCHITECTURE" == "aarch64" ]]; then + elif [[ "$PLATFORM" == "linux/arm64/v8" ]]; then RIE="aws-lambda-rie-arm64" else - echo "Architecture $ARCHITECTURE is not currently supported." + echo "Platform $PLATFORM is not currently supported." exit 1 fi - tar -xvf aws-lambda-java-runtime-interface-client/test/integration/resources/${RIE}.tar.gz --directory "${SCRATCH_DIR}" @@ -63,53 +70,12 @@ phases: docker build . \ -f "${SCRATCH_DIR}/Dockerfile.function.${OS_DISTRIBUTION}.tmp" \ -t "${IMAGE_TAG}" \ + --platform="${PLATFORM}" \ --build-arg RUNTIME_VERSION="${RUNTIME_VERSION}" \ --build-arg DISTRO_VERSION="${DISTRO_VERSION}" build: commands: - - set -x - - echo "Running Image ${IMAGE_TAG}" - - docker network create "${OS_DISTRIBUTION}-network" - - > - docker run \ - --detach \ - --name "${OS_DISTRIBUTION}-app" \ - --network "${OS_DISTRIBUTION}-network" \ - --entrypoint="" \ - "${IMAGE_TAG}" \ - sh -c "/usr/bin/${RIE} ${JAVA_BINARY_LOCATION} -jar ./HelloWorld-1.0.jar helloworld.App" - - sleep 2 - - > - docker run \ - --name "${OS_DISTRIBUTION}-tester" \ - --env "TARGET=${OS_DISTRIBUTION}-app" \ - --network "${OS_DISTRIBUTION}-network" \ - --entrypoint="" \ - "${IMAGE_TAG}" \ - sh -c 'curl -X POST "http://${TARGET}:8080/2015-03-31/functions/function/invocations" -d "{}" --max-time 10' - - actual="$(docker logs --tail 1 "${OS_DISTRIBUTION}-tester" | xargs)" - - expected='success' - - | - echo "Response: ${actual}" - if [[ "$actual" != "$expected" ]]; then - echo "fail! runtime: $RUNTIME - expected output $expected - got $actual" - exit -1 - fi + - aws-lambda-java-runtime-interface-client/test/integration/codebuild/scripts/run_invocation_test.sh finally: - - | - echo "---------Container Logs: ${OS_DISTRIBUTION}-app----------" - echo - docker logs "${OS_DISTRIBUTION}-app" || true - echo - echo "---------------------------------------------------" - echo "--------Container Logs: ${OS_DISTRIBUTION}-tester--------" - echo - docker logs "${OS_DISTRIBUTION}-tester" || true - echo - echo "---------------------------------------------------" - - echo "Cleaning up..." - - docker stop "${OS_DISTRIBUTION}-app" || true - - docker rm --force "${OS_DISTRIBUTION}-app" || true - - docker stop "${OS_DISTRIBUTION}-tester" || true - - docker rm --force "${OS_DISTRIBUTION}-tester" || true - - docker network rm "${OS_DISTRIBUTION}-network" || true \ No newline at end of file + - aws-lambda-java-runtime-interface-client/test/integration/codebuild/scripts/fetch_test_container_logs.sh + - aws-lambda-java-runtime-interface-client/test/integration/codebuild/scripts/clean_up.sh 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 1150ff3f8..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 @@ -4,6 +4,8 @@ env: variables: OS_DISTRIBUTION: debian JAVA_BINARY_LOCATION: "/usr/lib/jvm/java-11-amazon-corretto/bin/java" + DOCKER_CLI_EXPERIMENTAL: "enabled" + DOCKER_CLI_PLUGIN_DIR: "/root/.docker/cli-plugins" batch: build-matrix: static: @@ -15,8 +17,12 @@ batch: variables: DISTRO_VERSION: - "buster" + - "bullseye" RUNTIME_VERSION: - "corretto11" + PLATFORM: + - "linux/amd64" + - "linux/arm64/v8" phases: install: commands: @@ -28,11 +34,15 @@ phases: echo "Performing DockerHub login . . ." docker login -u $DOCKERHUB_USERNAME -p $DOCKERHUB_PASSWORD fi + - aws-lambda-java-runtime-interface-client/test/integration/codebuild/scripts/configure_multi_arch_env.sh pre_build: commands: + # Log some environment variables for troubleshooting + - (mvn -v) # 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) @@ -40,14 +50,13 @@ phases: - echo "Extracting and including Runtime Interface Emulator" - SCRATCH_DIR=".scratch" - mkdir "${SCRATCH_DIR}" - - ARCHITECTURE=$(arch) - > - if [[ "$ARCHITECTURE" == "x86_64" ]]; then + if [[ "$PLATFORM" == "linux/amd64" ]]; then RIE="aws-lambda-rie" - elif [[ "$ARCHITECTURE" == "aarch64" ]]; then + elif [[ "$PLATFORM" == "linux/arm64/v8" ]]; then RIE="aws-lambda-rie-arm64" else - echo "Architecture $ARCHITECTURE is not currently supported." + echo "Platform $PLATFORM is not currently supported." exit 1 fi - tar -xvf aws-lambda-java-runtime-interface-client/test/integration/resources/${RIE}.tar.gz --directory "${SCRATCH_DIR}" @@ -65,53 +74,12 @@ phases: docker build . \ -f "${SCRATCH_DIR}/Dockerfile.function.${OS_DISTRIBUTION}.tmp" \ -t "${IMAGE_TAG}" \ + --platform="${PLATFORM}" \ --build-arg RUNTIME_VERSION="${RUNTIME_VERSION}" \ --build-arg DISTRO_VERSION="${DISTRO_VERSION}" build: commands: - - set -x - - echo "Running Image ${IMAGE_TAG}" - - docker network create "${OS_DISTRIBUTION}-network" - - > - docker run \ - --detach \ - --name "${OS_DISTRIBUTION}-app" \ - --network "${OS_DISTRIBUTION}-network" \ - --entrypoint="" \ - "${IMAGE_TAG}" \ - sh -c "/usr/bin/${RIE} ${JAVA_BINARY_LOCATION} -jar ./HelloWorld-1.0.jar helloworld.App" - - sleep 2 - - > - docker run \ - --name "${OS_DISTRIBUTION}-tester" \ - --env "TARGET=${OS_DISTRIBUTION}-app" \ - --network "${OS_DISTRIBUTION}-network" \ - --entrypoint="" \ - "${IMAGE_TAG}" \ - sh -c 'curl -X POST "http://${TARGET}:8080/2015-03-31/functions/function/invocations" -d "{}" --max-time 10' - - actual="$(docker logs --tail 1 "${OS_DISTRIBUTION}-tester" | xargs)" - - expected='success' - - | - echo "Response: ${actual}" - if [[ "$actual" != "$expected" ]]; then - echo "fail! runtime: $RUNTIME - expected output $expected - got $actual" - exit -1 - fi + - aws-lambda-java-runtime-interface-client/test/integration/codebuild/scripts/run_invocation_test.sh finally: - - | - echo "---------Container Logs: ${OS_DISTRIBUTION}-app----------" - echo - docker logs "${OS_DISTRIBUTION}-app" || true - echo - echo "---------------------------------------------------" - echo "--------Container Logs: ${OS_DISTRIBUTION}-tester--------" - echo - docker logs "${OS_DISTRIBUTION}-tester" || true - echo - echo "---------------------------------------------------" - - echo "Cleaning up..." - - docker stop "${OS_DISTRIBUTION}-app" || true - - docker rm --force "${OS_DISTRIBUTION}-app" || true - - docker stop "${OS_DISTRIBUTION}-tester" || true - - docker rm --force "${OS_DISTRIBUTION}-tester" || true - - docker network rm "${OS_DISTRIBUTION}-network" || true \ No newline at end of file + - aws-lambda-java-runtime-interface-client/test/integration/codebuild/scripts/fetch_test_container_logs.sh + - aws-lambda-java-runtime-interface-client/test/integration/codebuild/scripts/clean_up.sh 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 ef3478ed1..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 @@ -4,6 +4,8 @@ env: variables: OS_DISTRIBUTION: ubuntu JAVA_BINARY_LOCATION: "/usr/lib/jvm/java-11-amazon-corretto/bin/java" + DOCKER_CLI_EXPERIMENTAL: "enabled" + DOCKER_CLI_PLUGIN_DIR: "/root/.docker/cli-plugins" batch: build-matrix: static: @@ -14,11 +16,15 @@ batch: env: variables: DISTRO_VERSION: - - "16.04" - "18.04" - "20.04" + - "21.10" + - "22.04" RUNTIME_VERSION: - "corretto11" + PLATFORM: + - "linux/amd64" + - "linux/arm64/v8" phases: install: commands: @@ -30,11 +36,15 @@ phases: echo "Performing DockerHub login . . ." docker login -u $DOCKERHUB_USERNAME -p $DOCKERHUB_PASSWORD fi + - aws-lambda-java-runtime-interface-client/test/integration/codebuild/scripts/configure_multi_arch_env.sh pre_build: commands: + # Log some environment variables for troubleshooting + - (mvn -v) # 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) @@ -42,14 +52,13 @@ phases: - echo "Extracting and including Runtime Interface Emulator" - SCRATCH_DIR=".scratch" - mkdir "${SCRATCH_DIR}" - - ARCHITECTURE=$(arch) - > - if [[ "$ARCHITECTURE" == "x86_64" ]]; then + if [[ "$PLATFORM" == "linux/amd64" ]]; then RIE="aws-lambda-rie" - elif [[ "$ARCHITECTURE" == "aarch64" ]]; then + elif [[ "$PLATFORM" == "linux/arm64/v8" ]]; then RIE="aws-lambda-rie-arm64" else - echo "Architecture $ARCHITECTURE is not currently supported." + echo "Platform $PLATFORM is not currently supported." exit 1 fi - tar -xvf aws-lambda-java-runtime-interface-client/test/integration/resources/${RIE}.tar.gz --directory "${SCRATCH_DIR}" @@ -67,53 +76,12 @@ phases: docker build . \ -f "${SCRATCH_DIR}/Dockerfile.function.${OS_DISTRIBUTION}.tmp" \ -t "${IMAGE_TAG}" \ + --platform="${PLATFORM}" \ --build-arg RUNTIME_VERSION="${RUNTIME_VERSION}" \ --build-arg DISTRO_VERSION="${DISTRO_VERSION}" build: commands: - - set -x - - echo "Running Image ${IMAGE_TAG}" - - docker network create "${OS_DISTRIBUTION}-network" - - > - docker run \ - --detach \ - --name "${OS_DISTRIBUTION}-app" \ - --network "${OS_DISTRIBUTION}-network" \ - --entrypoint="" \ - "${IMAGE_TAG}" \ - sh -c "/usr/bin/${RIE} ${JAVA_BINARY_LOCATION} -jar ./HelloWorld-1.0.jar helloworld.App" - - sleep 2 - - > - docker run \ - --name "${OS_DISTRIBUTION}-tester" \ - --env "TARGET=${OS_DISTRIBUTION}-app" \ - --network "${OS_DISTRIBUTION}-network" \ - --entrypoint="" \ - "${IMAGE_TAG}" \ - sh -c 'curl -X POST "http://${TARGET}:8080/2015-03-31/functions/function/invocations" -d "{}" --max-time 10' - - actual="$(docker logs --tail 1 "${OS_DISTRIBUTION}-tester" | xargs)" - - expected='success' - - | - echo "Response: ${actual}" - if [[ "$actual" != "$expected" ]]; then - echo "fail! runtime: $RUNTIME - expected output $expected - got $actual" - exit -1 - fi + - aws-lambda-java-runtime-interface-client/test/integration/codebuild/scripts/run_invocation_test.sh finally: - - | - echo "---------Container Logs: ${OS_DISTRIBUTION}-app----------" - echo - docker logs "${OS_DISTRIBUTION}-app" || true - echo - echo "---------------------------------------------------" - echo "--------Container Logs: ${OS_DISTRIBUTION}-tester--------" - echo - docker logs "${OS_DISTRIBUTION}-tester" || true - echo - echo "---------------------------------------------------" - - echo "Cleaning up..." - - docker stop "${OS_DISTRIBUTION}-app" || true - - docker rm --force "${OS_DISTRIBUTION}-app" || true - - docker stop "${OS_DISTRIBUTION}-tester" || true - - docker rm --force "${OS_DISTRIBUTION}-tester" || true - - docker network rm "${OS_DISTRIBUTION}-network" || true \ No newline at end of file + - aws-lambda-java-runtime-interface-client/test/integration/codebuild/scripts/fetch_test_container_logs.sh + - aws-lambda-java-runtime-interface-client/test/integration/codebuild/scripts/clean_up.sh diff --git a/aws-lambda-java-runtime-interface-client/test/integration/codebuild/scripts/clean_up.sh b/aws-lambda-java-runtime-interface-client/test/integration/codebuild/scripts/clean_up.sh new file mode 100755 index 000000000..236fa7b26 --- /dev/null +++ b/aws-lambda-java-runtime-interface-client/test/integration/codebuild/scripts/clean_up.sh @@ -0,0 +1,11 @@ +#!/bin/bash +# Copyright 2022 Amazon.com, Inc. or its affiliates. All Rights Reserved. + +set -euo pipefail + +echo "Cleaning up..." +docker stop "${OS_DISTRIBUTION}-app" || true +docker rm --force "${OS_DISTRIBUTION}-app" || true +docker stop "${OS_DISTRIBUTION}-tester" || true +docker rm --force "${OS_DISTRIBUTION}-tester" || true +docker network rm "${OS_DISTRIBUTION}-network" || true diff --git a/aws-lambda-java-runtime-interface-client/test/integration/codebuild/scripts/configure_multi_arch_env.sh b/aws-lambda-java-runtime-interface-client/test/integration/codebuild/scripts/configure_multi_arch_env.sh new file mode 100755 index 000000000..76153f184 --- /dev/null +++ b/aws-lambda-java-runtime-interface-client/test/integration/codebuild/scripts/configure_multi_arch_env.sh @@ -0,0 +1,22 @@ +#!/bin/bash +# Copyright 2022 Amazon.com, Inc. or its affiliates. All Rights Reserved. + +set -euo pipefail + +echo "Setting up multi-arch build environment" +ARCHITECTURE=$(arch) +if [[ "$ARCHITECTURE" == "x86_64" ]]; then + TARGET_EMULATOR="arm64" +elif [[ "$ARCHITECTURE" == "aarch64" ]]; then + TARGET_EMULATOR="amd64" +else + echo "Architecture $ARCHITECTURE is not currently supported." + exit 1 +fi + +echo "Installing ${TARGET_EMULATOR} emulator" +docker pull public.ecr.aws/eks-distro-build-tooling/binfmt-misc:qemu-v6.1.0 +docker run --rm --privileged public.ecr.aws/eks-distro-build-tooling/binfmt-misc:qemu-v6.1.0 --install ${TARGET_EMULATOR} +echo "Setting docker build command to default to buildx" +echo "Docker buildx version: $(docker buildx version)" +docker buildx install diff --git a/aws-lambda-java-runtime-interface-client/test/integration/codebuild/scripts/fetch_test_container_logs.sh b/aws-lambda-java-runtime-interface-client/test/integration/codebuild/scripts/fetch_test_container_logs.sh new file mode 100755 index 000000000..4bc809dc1 --- /dev/null +++ b/aws-lambda-java-runtime-interface-client/test/integration/codebuild/scripts/fetch_test_container_logs.sh @@ -0,0 +1,15 @@ +#!/bin/bash +# Copyright 2022 Amazon.com, Inc. or its affiliates. All Rights Reserved. + +set -euo pipefail + +echo "---------Container Logs: ${OS_DISTRIBUTION}-app----------" +echo +docker logs "${OS_DISTRIBUTION}-app" || true +echo +echo "---------------------------------------------------" +echo "--------Container Logs: ${OS_DISTRIBUTION}-tester--------" +echo +docker logs "${OS_DISTRIBUTION}-tester" || true +echo +echo "---------------------------------------------------" diff --git a/aws-lambda-java-runtime-interface-client/test/integration/codebuild/scripts/run_invocation_test.sh b/aws-lambda-java-runtime-interface-client/test/integration/codebuild/scripts/run_invocation_test.sh new file mode 100755 index 000000000..f5ebf0687 --- /dev/null +++ b/aws-lambda-java-runtime-interface-client/test/integration/codebuild/scripts/run_invocation_test.sh @@ -0,0 +1,43 @@ +#!/bin/bash +# Copyright 2022 Amazon.com, Inc. or its affiliates. All Rights Reserved. + +set -euxo pipefail + +echo "Running Image ${IMAGE_TAG}" +docker network create "${OS_DISTRIBUTION}-network" + +function_jar="./HelloWorld-1.0.jar" +function_handler="helloworld.App" +docker run \ + --detach \ + --name "${OS_DISTRIBUTION}-app" \ + --network "${OS_DISTRIBUTION}-network" \ + --entrypoint="" \ + --platform="${PLATFORM}" \ + "${IMAGE_TAG}" \ + sh -c "/usr/bin/${RIE} ${JAVA_BINARY_LOCATION} -jar ${function_jar} ${function_handler}" +sleep 2 + +# running on arm64 hosts with x86_64 being emulated takes significantly more time than any other combination +if [[ "$(arch)" == "aarch64" ]] && [[ "${PLATFORM}" == "linux/amd64" ]]; then + declare -i time_out=150 +else + declare -i time_out=10 +fi + +docker run \ + --name "${OS_DISTRIBUTION}-tester" \ + --env "TARGET=${OS_DISTRIBUTION}-app" \ + --env "MAX_TIME=${time_out}" \ + --network "${OS_DISTRIBUTION}-network" \ + --entrypoint="" \ + --platform="${PLATFORM}" \ + "${IMAGE_TAG}" \ + sh -c 'curl -X POST "http://${TARGET}:8080/2015-03-31/functions/function/invocations" -d "{}" --max-time ${MAX_TIME}' +actual="$(docker logs --tail 1 "${OS_DISTRIBUTION}-tester" | xargs)" +expected='success' +echo "Response: ${actual}" +if [[ "${actual}" != "${expected}" ]]; then + echo "fail! runtime: ${RUNTIME} - expected output ${expected} - got ${actual}" + exit 1 +fi diff --git a/aws-lambda-java-runtime-interface-client/test/integration/docker/Dockerfile.function.alpine b/aws-lambda-java-runtime-interface-client/test/integration/docker/Dockerfile.function.alpine index 60c744093..bfd288901 100644 --- a/aws-lambda-java-runtime-interface-client/test/integration/docker/Dockerfile.function.alpine +++ b/aws-lambda-java-runtime-interface-client/test/integration/docker/Dockerfile.function.alpine @@ -1,6 +1,6 @@ ARG DISTRO_VERSION -FROM alpine:${DISTRO_VERSION} +FROM public.ecr.aws/docker/library/alpine:${DISTRO_VERSION} RUN apk update && \ apk add openjdk8 diff --git a/aws-lambda-java-runtime-interface-client/test/integration/docker/Dockerfile.function.amazoncorretto b/aws-lambda-java-runtime-interface-client/test/integration/docker/Dockerfile.function.amazoncorretto index c27810962..a6958cb05 100644 --- a/aws-lambda-java-runtime-interface-client/test/integration/docker/Dockerfile.function.amazoncorretto +++ b/aws-lambda-java-runtime-interface-client/test/integration/docker/Dockerfile.function.amazoncorretto @@ -1,6 +1,6 @@ ARG RUNTIME_VERSION -FROM amazoncorretto:${RUNTIME_VERSION} +FROM public.ecr.aws/amazoncorretto/amazoncorretto:${RUNTIME_VERSION} ADD aws-lambda-java-runtime-interface-client/test/integration/test-handler/target/HelloWorld-1.0.jar . diff --git a/aws-lambda-java-runtime-interface-client/test/integration/docker/Dockerfile.function.amazonlinux b/aws-lambda-java-runtime-interface-client/test/integration/docker/Dockerfile.function.amazonlinux index 4e58ac452..b3d152fc1 100644 --- a/aws-lambda-java-runtime-interface-client/test/integration/docker/Dockerfile.function.amazonlinux +++ b/aws-lambda-java-runtime-interface-client/test/integration/docker/Dockerfile.function.amazonlinux @@ -1,6 +1,6 @@ ARG DISTRO_VERSION -FROM amazonlinux:${DISTRO_VERSION} +FROM public.ecr.aws/amazonlinux/amazonlinux:${DISTRO_VERSION} RUN yum install -y java-1.8.0-openjdk diff --git a/aws-lambda-java-runtime-interface-client/test/integration/docker/Dockerfile.function.centos b/aws-lambda-java-runtime-interface-client/test/integration/docker/Dockerfile.function.centos index 72036c6fa..865092393 100644 --- a/aws-lambda-java-runtime-interface-client/test/integration/docker/Dockerfile.function.centos +++ b/aws-lambda-java-runtime-interface-client/test/integration/docker/Dockerfile.function.centos @@ -1,6 +1,6 @@ ARG DISTRO_VERSION -FROM centos:${DISTRO_VERSION} +FROM public.ecr.aws/docker/library/centos:centos${DISTRO_VERSION} RUN rpm --import https://yum.corretto.aws/corretto.key && \ curl -L -o /etc/yum.repos.d/corretto.repo https://yum.corretto.aws/corretto.repo && \ diff --git a/aws-lambda-java-runtime-interface-client/test/integration/docker/Dockerfile.function.debian b/aws-lambda-java-runtime-interface-client/test/integration/docker/Dockerfile.function.debian index 5c655186f..1ef477cc9 100644 --- a/aws-lambda-java-runtime-interface-client/test/integration/docker/Dockerfile.function.debian +++ b/aws-lambda-java-runtime-interface-client/test/integration/docker/Dockerfile.function.debian @@ -1,6 +1,6 @@ ARG DISTRO_VERSION -FROM debian:${DISTRO_VERSION} as build-image +FROM public.ecr.aws/debian/debian:${DISTRO_VERSION} as build-image RUN apt-get update && \ apt-get install -y wget gpg software-properties-common && \ @@ -9,7 +9,7 @@ RUN apt-get update && \ apt-get update && \ apt-get install -y java-11-amazon-corretto-jdk -FROM debian:${DISTRO_VERSION} +FROM public.ecr.aws/debian/debian:${DISTRO_VERSION} COPY --from=build-image /usr/lib/jvm /usr/lib/jvm diff --git a/aws-lambda-java-runtime-interface-client/test/integration/docker/Dockerfile.function.ubuntu b/aws-lambda-java-runtime-interface-client/test/integration/docker/Dockerfile.function.ubuntu index 217185cf6..09107c469 100644 --- a/aws-lambda-java-runtime-interface-client/test/integration/docker/Dockerfile.function.ubuntu +++ b/aws-lambda-java-runtime-interface-client/test/integration/docker/Dockerfile.function.ubuntu @@ -1,6 +1,6 @@ ARG DISTRO_VERSION -FROM ubuntu:${DISTRO_VERSION} as build-image +FROM public.ecr.aws/ubuntu/ubuntu:${DISTRO_VERSION} as build-image RUN apt-get update && \ apt-get install -y apt-transport-https ca-certificates && \ @@ -10,7 +10,7 @@ RUN apt-get update && \ apt-get update && \ apt-get install -y java-11-amazon-corretto-jdk -FROM ubuntu:${DISTRO_VERSION} +FROM public.ecr.aws/ubuntu/ubuntu:${DISTRO_VERSION} COPY --from=build-image /usr/lib/jvm /usr/lib/jvm 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 5fdc98baa..e854831e7 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.0.0 + 2.8.7 @@ -50,4 +50,3 @@ - diff --git a/aws-lambda-java-serialization/RELEASE.CHANGELOG.md b/aws-lambda-java-serialization/RELEASE.CHANGELOG.md index be5423fec..2ce29d758 100644 --- a/aws-lambda-java-serialization/RELEASE.CHANGELOG.md +++ b/aws-lambda-java-serialization/RELEASE.CHANGELOG.md @@ -1,3 +1,37 @@ +### 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 + +### October 19, 2023 +`1.1.4`: +- Update org.json version to 20231013 +- Rollback relocation changes(1.1.3 version) + +### September 21, 2023 +`1.1.3`: +- Add support for event v4 lib + +### February 22, 2023 +`1.1.1`: +- Register `JodaModule` to JacksonFactory + +### February 17, 2023 +`1.1.0`: +- Update `jackson-databind` dependency from 2.13.4.1 to 2.14.2 +- Register `JavaTimeModule` and `Jdk8Module` modules to `jackson-databind` + +### February 09, 2023 +`1.0.2`: +- Updated `gson` dependency from 2.8.9 to 2.10.1 + +### November 21, 2022 +`1.0.1`: +- Updated `jackson-databind` dependency from 2.12.6.1 to 2.13.4.1 + ### December 01, 2020 `1.0.0`: - Initial release of AWS Lambda Java Serialization \ No newline at end of file diff --git a/aws-lambda-java-serialization/pom.xml b/aws-lambda-java-serialization/pom.xml index e3892d6c4..7fa472118 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.0.0 + 1.2.0 jar AWS Lambda Java Runtime Serialization @@ -32,6 +32,10 @@ 1.8 1.8 com.amazonaws.lambda.thirdparty + 2.15.4 + 2.10.1 + 20231013 + 7.3.2 @@ -39,30 +43,32 @@ com.fasterxml.jackson.core jackson-databind - 2.10.0 + ${jackson.version} - com.google.code.gson - gson - 2.8.5 + com.fasterxml.jackson.datatype + jackson-datatype-joda + ${jackson.version} - org.json - json - 20160810 + com.fasterxml.jackson.datatype + jackson-datatype-jsr310 + ${jackson.version} - - org.junit.jupiter - junit-jupiter-engine - 5.7.0 - test + com.fasterxml.jackson.datatype + jackson-datatype-jdk8 + ${jackson.version} - com.amazonaws - aws-lambda-java-events - 3.11.0 - test + com.google.code.gson + gson + ${gson.version} + + + org.json + json + ${json.version} @@ -79,7 +85,7 @@ org.owasp dependency-check-maven - 5.3.2 + ${owasp.version} validate @@ -163,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 @@ -190,7 +194,7 @@ org.apache.maven.plugins maven-shade-plugin - 3.2.1 + 3.6.1 package @@ -214,6 +218,16 @@ org.json ${relocation.prefix}.org.json + + org.joda.time + ${relocation.prefix}.org.joda.time + + + + com.amazonaws.lambda.unshade.thirdparty.org.joda.time + org.joda.time + 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 4cb4c431e..3b10b198e 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 @@ -8,7 +8,9 @@ import com.amazonaws.services.lambda.runtime.serialization.events.mixins.CodeCommitEventMixin; import com.amazonaws.services.lambda.runtime.serialization.events.mixins.ConnectEventMixin; import com.amazonaws.services.lambda.runtime.serialization.events.mixins.DynamodbEventMixin; +import com.amazonaws.services.lambda.runtime.serialization.events.mixins.DynamodbTimeWindowEventMixin; import com.amazonaws.services.lambda.runtime.serialization.events.mixins.KinesisEventMixin; +import com.amazonaws.services.lambda.runtime.serialization.events.mixins.KinesisTimeWindowEventMixin; import com.amazonaws.services.lambda.runtime.serialization.events.mixins.SNSEventMixin; import com.amazonaws.services.lambda.runtime.serialization.events.mixins.SQSEventMixin; import com.amazonaws.services.lambda.runtime.serialization.events.mixins.ScheduledEventMixin; @@ -63,8 +65,10 @@ public class LambdaEventSerializers { "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", @@ -114,6 +118,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", @@ -128,10 +133,14 @@ public class LambdaEventSerializers { DynamodbEventMixin.AttributeValueMixin.class), new SimpleEntry<>("com.amazonaws.services.lambda.runtime.events.models.dynamodb.AttributeValue", DynamodbEventMixin.AttributeValueMixin.class), + new SimpleEntry<>("com.amazonaws.services.lambda.runtime.events.DynamodbTimeWindowEvent", + DynamodbTimeWindowEventMixin.class), new SimpleEntry<>("com.amazonaws.services.lambda.runtime.events.KinesisEvent", KinesisEventMixin.class), new SimpleEntry<>("com.amazonaws.services.lambda.runtime.events.KinesisEvent$Record", KinesisEventMixin.RecordMixin.class), + new SimpleEntry<>("com.amazonaws.services.lambda.runtime.events.KinesisTimeWindowEvent", + KinesisTimeWindowEventMixin.class), new SimpleEntry<>("com.amazonaws.services.lambda.runtime.events.ScheduledEvent", ScheduledEventMixin.class), new SimpleEntry<>("com.amazonaws.services.lambda.runtime.events.SecretsManagerRotationEvent", @@ -150,7 +159,7 @@ public class LambdaEventSerializers { * If mixins are required for inner classes of an event, then those nested classes must be specified here. */ @SuppressWarnings("rawtypes") - private static final Map> NESTED_CLASS_MAP = Stream.of( + private static final Map> NESTED_CLASS_MAP = Stream.of( new SimpleEntry<>("com.amazonaws.services.lambda.runtime.events.CodeCommitEvent", Arrays.asList( new NestedClass("com.amazonaws.services.lambda.runtime.events.CodeCommitEvent$Record"))), @@ -162,6 +171,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( @@ -172,6 +182,23 @@ public class LambdaEventSerializers { "com.amazonaws.services.lambda.runtime.events.models.dynamodb.StreamRecord", "com.amazonaws.services.dynamodbv2.model.StreamRecord"), new NestedClass("com.amazonaws.services.lambda.runtime.events.DynamodbEvent$DynamodbStreamRecord"))), + new SimpleEntry<>("com.amazonaws.services.lambda.runtime.events.DynamodbEvent$DynamodbStreamRecord", + Arrays.asList( + new AlternateNestedClass( + "com.amazonaws.services.lambda.runtime.events.models.dynamodb.AttributeValue", + "com.amazonaws.services.dynamodbv2.model.AttributeValue"), + new AlternateNestedClass( + "com.amazonaws.services.lambda.runtime.events.models.dynamodb.StreamRecord", + "com.amazonaws.services.dynamodbv2.model.StreamRecord"))), + new SimpleEntry<>("com.amazonaws.services.lambda.runtime.events.DynamodbTimeWindowEvent", + Arrays.asList( + new AlternateNestedClass( + "com.amazonaws.services.lambda.runtime.events.models.dynamodb.AttributeValue", + "com.amazonaws.services.dynamodbv2.model.AttributeValue"), + new AlternateNestedClass( + "com.amazonaws.services.lambda.runtime.events.models.dynamodb.StreamRecord", + "com.amazonaws.services.dynamodbv2.model.StreamRecord"), + new NestedClass("com.amazonaws.services.lambda.runtime.events.DynamodbEvent$DynamodbStreamRecord"))), new SimpleEntry<>("com.amazonaws.services.lambda.runtime.events.KinesisEvent", Arrays.asList( new NestedClass("com.amazonaws.services.lambda.runtime.events.KinesisEvent$Record"))), @@ -189,7 +216,10 @@ 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 PropertyNamingStrategy.PascalCaseStrategy()), + new SimpleEntry<>("com.amazonaws.services.lambda.runtime.events.ConnectEvent$Queue", + new PropertyNamingStrategy.PascalCaseStrategy()) + ) .collect(Collectors.toMap(SimpleEntry::getKey, SimpleEntry::getValue)); /** @@ -219,7 +249,7 @@ public static PojoSerializer serializerFor(Class eventClass, ClassLoad } // 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()); + 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)) { 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/mixins/DynamodbTimeWindowEventMixin.java b/aws-lambda-java-serialization/src/main/java/com/amazonaws/services/lambda/runtime/serialization/events/mixins/DynamodbTimeWindowEventMixin.java new file mode 100644 index 000000000..93d87a32f --- /dev/null +++ b/aws-lambda-java-serialization/src/main/java/com/amazonaws/services/lambda/runtime/serialization/events/mixins/DynamodbTimeWindowEventMixin.java @@ -0,0 +1,14 @@ +/* + * Copyright 2022 Amazon.com, Inc. or its affiliates. All Rights Reserved. + */ + +package com.amazonaws.services.lambda.runtime.serialization.events.mixins; + +import com.fasterxml.jackson.annotation.JsonProperty; + +public abstract class DynamodbTimeWindowEventMixin extends DynamodbEventMixin { + + // needed because Jackson expects "eventSourceArn" instead of "eventSourceARN" + @JsonProperty("eventSourceARN") abstract String getEventSourceArn(); + @JsonProperty("eventSourceARN") abstract void setEventSourceArn(String eventSourceArn); +} diff --git a/aws-lambda-java-serialization/src/main/java/com/amazonaws/services/lambda/runtime/serialization/events/mixins/KinesisTimeWindowEventMixin.java b/aws-lambda-java-serialization/src/main/java/com/amazonaws/services/lambda/runtime/serialization/events/mixins/KinesisTimeWindowEventMixin.java new file mode 100644 index 000000000..374a23bc1 --- /dev/null +++ b/aws-lambda-java-serialization/src/main/java/com/amazonaws/services/lambda/runtime/serialization/events/mixins/KinesisTimeWindowEventMixin.java @@ -0,0 +1,14 @@ +/* + * Copyright 2022 Amazon.com, Inc. or its affiliates. All Rights Reserved. + */ + +package com.amazonaws.services.lambda.runtime.serialization.events.mixins; + +import com.fasterxml.jackson.annotation.JsonProperty; + +public abstract class KinesisTimeWindowEventMixin extends KinesisEventMixin { + + // needed because Jackson expects "eventSourceArn" instead of "eventSourceARN" + @JsonProperty("eventSourceARN") abstract String getEventSourceArn(); + @JsonProperty("eventSourceARN") abstract void setEventSourceArn(String eventSourceArn); +} diff --git a/aws-lambda-java-serialization/src/main/java/com/amazonaws/services/lambda/runtime/serialization/events/mixins/SecretsManagerRotationEventMixin.java b/aws-lambda-java-serialization/src/main/java/com/amazonaws/services/lambda/runtime/serialization/events/mixins/SecretsManagerRotationEventMixin.java index ab94be20e..1b862e8cb 100644 --- a/aws-lambda-java-serialization/src/main/java/com/amazonaws/services/lambda/runtime/serialization/events/mixins/SecretsManagerRotationEventMixin.java +++ b/aws-lambda-java-serialization/src/main/java/com/amazonaws/services/lambda/runtime/serialization/events/mixins/SecretsManagerRotationEventMixin.java @@ -21,4 +21,8 @@ public abstract class SecretsManagerRotationEventMixin { // needed because Jackson expects "clientRequestToken" instead of "ClientRequestToken" @JsonProperty("ClientRequestToken") abstract String getClientRequestToken(); @JsonProperty("ClientRequestToken") abstract void setClientRequestToken(String clientRequestToken); + + // needed because Jackson expects "rotationToken" instead of "RotationToken" + @JsonProperty("RotationToken") abstract String getRotationToken(); + @JsonProperty("RotationToken") abstract void setRotationToken(String rotationToken); } diff --git a/aws-lambda-java-serialization/src/main/java/com/amazonaws/services/lambda/runtime/serialization/events/modules/DateTimeModule.java b/aws-lambda-java-serialization/src/main/java/com/amazonaws/services/lambda/runtime/serialization/events/modules/DateTimeModule.java index 54c7713de..a02857e00 100644 --- a/aws-lambda-java-serialization/src/main/java/com/amazonaws/services/lambda/runtime/serialization/events/modules/DateTimeModule.java +++ b/aws-lambda-java-serialization/src/main/java/com/amazonaws/services/lambda/runtime/serialization/events/modules/DateTimeModule.java @@ -6,26 +6,25 @@ import com.fasterxml.jackson.core.JsonGenerator; import com.fasterxml.jackson.core.JsonParser; import com.fasterxml.jackson.core.JsonProcessingException; -import com.fasterxml.jackson.core.json.PackageVersion; import com.fasterxml.jackson.databind.DeserializationContext; import com.fasterxml.jackson.databind.JsonDeserializer; import com.fasterxml.jackson.databind.JsonSerializer; import com.fasterxml.jackson.databind.SerializerProvider; -import com.fasterxml.jackson.databind.module.SimpleModule; +import com.fasterxml.jackson.datatype.joda.JodaModule; import java.io.IOException; /** * Class that is used to load customer DateTime class */ -public class DateTimeModule extends SimpleModule { +public class DateTimeModule extends JodaModule { /** * creates a DateTimeModule using customer class loader to pull org.joda.time.DateTime */ public DateTimeModule(ClassLoader classLoader) { - super(PackageVersion.VERSION); - Class dateTimeClass = SerializeUtil.loadCustomerClass("org.joda.time.DateTime", classLoader); + // Workaround not to let maven shade plugin relocating string literals https://issues.apache.org/jira/browse/MSHADE-156 + Class dateTimeClass = SerializeUtil.loadCustomerClass("com.amazonaws.lambda.unshade.thirdparty.org.joda.time.DateTime", classLoader); this.addSerializer(dateTimeClass, getSerializer(dateTimeClass, classLoader)); this.addDeserializer(dateTimeClass, getDeserializer(dateTimeClass)); } diff --git a/aws-lambda-java-serialization/src/main/java/com/amazonaws/services/lambda/runtime/serialization/events/serializers/S3EventSerializer.java b/aws-lambda-java-serialization/src/main/java/com/amazonaws/services/lambda/runtime/serialization/events/serializers/S3EventSerializer.java index 6c6f65870..c833abcc1 100644 --- a/aws-lambda-java-serialization/src/main/java/com/amazonaws/services/lambda/runtime/serialization/events/serializers/S3EventSerializer.java +++ b/aws-lambda-java-serialization/src/main/java/com/amazonaws/services/lambda/runtime/serialization/events/serializers/S3EventSerializer.java @@ -220,7 +220,8 @@ private JSONObject serializeEventNotificationRecord(A eventNotificationRecor Class requestParametersClass = SerializeUtil.loadCustomerClass(baseClassName + "$RequestParametersEntity", classLoader); Class responseElementsClass = SerializeUtil.loadCustomerClass(baseClassName + "$ResponseElementsEntity", classLoader); Class userIdentityClass = SerializeUtil.loadCustomerClass(baseClassName + "$UserIdentityEntity", classLoader); - Class dateTimeClass = SerializeUtil.loadCustomerClass("org.joda.time.DateTime", classLoader); + // Workaround not to let maven shade plugin relocating string literals https://issues.apache.org/jira/browse/MSHADE-156 + Class dateTimeClass = SerializeUtil.loadCustomerClass("com.amazonaws.lambda.unshade.thirdparty.org.joda.time.DateTime", classLoader); // serialize object JSONObject jsonObject = new JSONObject(); Functions.R0 getAwsRegionMethod = diff --git a/aws-lambda-java-serialization/src/main/java/com/amazonaws/services/lambda/runtime/serialization/factories/JacksonFactory.java b/aws-lambda-java-serialization/src/main/java/com/amazonaws/services/lambda/runtime/serialization/factories/JacksonFactory.java index 7fccd2a56..660ca8f58 100644 --- a/aws-lambda-java-serialization/src/main/java/com/amazonaws/services/lambda/runtime/serialization/factories/JacksonFactory.java +++ b/aws-lambda-java-serialization/src/main/java/com/amazonaws/services/lambda/runtime/serialization/factories/JacksonFactory.java @@ -5,6 +5,8 @@ import com.amazonaws.services.lambda.runtime.serialization.PojoSerializer; import com.fasterxml.jackson.core.JsonFactory; import com.fasterxml.jackson.core.JsonParser; +import com.fasterxml.jackson.core.json.JsonReadFeature; +import com.fasterxml.jackson.core.json.JsonWriteFeature; import com.fasterxml.jackson.core.JsonGenerator; import com.fasterxml.jackson.annotation.JsonInclude.Include; import com.fasterxml.jackson.databind.DeserializationConfig; @@ -19,7 +21,10 @@ import com.fasterxml.jackson.databind.PropertyNamingStrategy; import com.fasterxml.jackson.databind.SerializationConfig; import com.fasterxml.jackson.databind.SerializationFeature; +import com.fasterxml.jackson.databind.json.JsonMapper; import com.fasterxml.jackson.databind.module.SimpleModule; +import com.fasterxml.jackson.datatype.jdk8.Jdk8Module; +import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule; import java.io.IOException; import java.io.OutputStream; @@ -50,24 +55,42 @@ public ObjectMapper getMapper() { private static ObjectMapper createObjectMapper() { - ObjectMapper mapper = new ObjectMapper(createJsonFactory()); + ObjectMapper mapper = JsonMapper.builder(createJsonFactory()) + .enable(MapperFeature.ALLOW_FINAL_FIELDS_AS_MUTATORS) // this is default as of 2.2.0 + .enable(MapperFeature.AUTO_DETECT_FIELDS) // this is default as of 2.0.0 + .enable(MapperFeature.AUTO_DETECT_GETTERS) // this is default as of 2.0.0 + .enable(MapperFeature.AUTO_DETECT_IS_GETTERS) // this is default as of 2.0.0 + .enable(MapperFeature.AUTO_DETECT_SETTERS) // this is default as of 2.0.0 + .enable(MapperFeature.CAN_OVERRIDE_ACCESS_MODIFIERS) // this is default as of 2.0.0 + .enable(MapperFeature.USE_STD_BEAN_NAMING) + .enable(MapperFeature.USE_ANNOTATIONS) // this is default as of 2.0.0 + .disable(MapperFeature.ACCEPT_CASE_INSENSITIVE_PROPERTIES) // this is default as of 2.5.0 + .disable(MapperFeature.AUTO_DETECT_CREATORS) + .disable(MapperFeature.INFER_PROPERTY_MUTATORS) + .disable(MapperFeature.SORT_PROPERTIES_ALPHABETICALLY) // this is default as of 2.0.0 + .disable(MapperFeature.USE_GETTERS_AS_SETTERS) + .disable(MapperFeature.USE_WRAPPER_NAME_AS_PROPERTY_NAME) // this is default as of 2.1.0 + .disable(MapperFeature.USE_STATIC_TYPING) // this is default as of 2.0.0 + .disable(MapperFeature.REQUIRE_SETTERS_FOR_GETTERS) // this is default as of 2.0.0 + .build(); + SerializationConfig scfg = mapper.getSerializationConfig(); scfg = scfg.withFeatures( - SerializationFeature.FAIL_ON_SELF_REFERENCES, - SerializationFeature.FAIL_ON_UNWRAPPED_TYPE_IDENTIFIERS, - SerializationFeature.WRAP_EXCEPTIONS - ); + SerializationFeature.FAIL_ON_SELF_REFERENCES, // this is default as of 2.4.0 + SerializationFeature.FAIL_ON_UNWRAPPED_TYPE_IDENTIFIERS, // this is default as of 2.4.0 + SerializationFeature.WRAP_EXCEPTIONS // this is default as of 2.0.0 + ); scfg = scfg.withoutFeatures( - SerializationFeature.CLOSE_CLOSEABLE, + SerializationFeature.CLOSE_CLOSEABLE, // this is default as of 2.0.0 SerializationFeature.EAGER_SERIALIZER_FETCH, SerializationFeature.FAIL_ON_EMPTY_BEANS, SerializationFeature.FLUSH_AFTER_WRITE_VALUE, - SerializationFeature.INDENT_OUTPUT, - SerializationFeature.ORDER_MAP_ENTRIES_BY_KEYS, - SerializationFeature.USE_EQUALITY_FOR_OBJECT_ID, - SerializationFeature.WRITE_CHAR_ARRAYS_AS_JSON_ARRAYS, - SerializationFeature.WRAP_ROOT_VALUE - ); + SerializationFeature.INDENT_OUTPUT, // this is default as of 2.5.0 + SerializationFeature.ORDER_MAP_ENTRIES_BY_KEYS, // this is default as of 2.0.0 + SerializationFeature.USE_EQUALITY_FOR_OBJECT_ID, // this is default as of 2.3.0 + SerializationFeature.WRITE_CHAR_ARRAYS_AS_JSON_ARRAYS, // this is default as of 2.0.0 + SerializationFeature.WRAP_ROOT_VALUE // this is default as of 2.2.0 + ); mapper.setConfig(scfg); DeserializationConfig dcfg = mapper.getDeserializationConfig(); @@ -75,96 +98,65 @@ private static ObjectMapper createObjectMapper() { DeserializationFeature.ACCEPT_EMPTY_ARRAY_AS_NULL_OBJECT, DeserializationFeature.ACCEPT_EMPTY_STRING_AS_NULL_OBJECT, DeserializationFeature.ACCEPT_SINGLE_VALUE_AS_ARRAY, - DeserializationFeature.FAIL_ON_INVALID_SUBTYPE, - DeserializationFeature.FAIL_ON_UNRESOLVED_OBJECT_IDS, + DeserializationFeature.FAIL_ON_INVALID_SUBTYPE, // this is default as of 2.2.0 + DeserializationFeature.FAIL_ON_UNRESOLVED_OBJECT_IDS, // this is default as of 2.5.0 DeserializationFeature.READ_UNKNOWN_ENUM_VALUES_AS_NULL, DeserializationFeature.UNWRAP_SINGLE_VALUE_ARRAYS, - DeserializationFeature.WRAP_EXCEPTIONS - ); + DeserializationFeature.WRAP_EXCEPTIONS // this is default as of 2.0.0 + ); dcfg = dcfg.withoutFeatures( - DeserializationFeature.FAIL_ON_IGNORED_PROPERTIES, - DeserializationFeature.FAIL_ON_NULL_FOR_PRIMITIVES, - DeserializationFeature.FAIL_ON_NUMBERS_FOR_ENUMS, - DeserializationFeature.FAIL_ON_READING_DUP_TREE_KEY, + DeserializationFeature.FAIL_ON_IGNORED_PROPERTIES, // this is default as of 2.3.0 + DeserializationFeature.FAIL_ON_NULL_FOR_PRIMITIVES, // this is default as of 2.0.0 + DeserializationFeature.FAIL_ON_NUMBERS_FOR_ENUMS, // this is default as of 2.0.0 + DeserializationFeature.FAIL_ON_READING_DUP_TREE_KEY, // this is default as of 2.3.0 DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES - ); + ); mapper.setConfig(dcfg); - mapper.setSerializationInclusion(Include.NON_NULL); //NON_EMPTY? - - mapper.enable(MapperFeature.ALLOW_FINAL_FIELDS_AS_MUTATORS); - mapper.enable(MapperFeature.AUTO_DETECT_FIELDS); - mapper.enable(MapperFeature.AUTO_DETECT_GETTERS); - mapper.enable(MapperFeature.AUTO_DETECT_IS_GETTERS); - mapper.enable(MapperFeature.AUTO_DETECT_SETTERS); - mapper.enable(MapperFeature.CAN_OVERRIDE_ACCESS_MODIFIERS); - mapper.enable(MapperFeature.USE_STD_BEAN_NAMING); - mapper.enable(MapperFeature.USE_ANNOTATIONS); - - mapper.disable(MapperFeature.ACCEPT_CASE_INSENSITIVE_PROPERTIES); - mapper.disable(MapperFeature.AUTO_DETECT_CREATORS); - mapper.disable(MapperFeature.INFER_PROPERTY_MUTATORS); - mapper.disable(MapperFeature.SORT_PROPERTIES_ALPHABETICALLY); - mapper.disable(MapperFeature.USE_GETTERS_AS_SETTERS); - mapper.disable(MapperFeature.USE_WRAPPER_NAME_AS_PROPERTY_NAME); - mapper.disable(MapperFeature.USE_STATIC_TYPING); - mapper.disable(MapperFeature.REQUIRE_SETTERS_FOR_GETTERS); - - SimpleModule module = new SimpleModule(); - module.addDeserializer(Void.class, new VoidDeserializer()); - mapper.registerModule(module); - - return mapper; - } + mapper.setSerializationInclusion(Include.NON_NULL); - public static final class VoidDeserializer extends JsonDeserializer { - - private final static Void VOID = createVoid(); - - private static Void createVoid() { - try { - Constructor constructor = Void.class.getDeclaredConstructor(); - constructor.setAccessible(true); - return constructor.newInstance(); - } catch(Exception e) { - return null; - } - } - - @Override - public Void deserialize(JsonParser parser, DeserializationContext ctx) { - return VOID; - } + mapper.registerModule(new JavaTimeModule()); + mapper.registerModule(new Jdk8Module()); + return mapper; } private static JsonFactory createJsonFactory() { - JsonFactory factory = new JsonFactory(); - //Json Parser enabled - factory.enable(JsonParser.Feature.ALLOW_NON_NUMERIC_NUMBERS); - factory.enable(JsonParser.Feature.ALLOW_NUMERIC_LEADING_ZEROS); - factory.enable(JsonParser.Feature.ALLOW_SINGLE_QUOTES); - factory.enable(JsonParser.Feature.ALLOW_BACKSLASH_ESCAPING_ANY_CHARACTER); - factory.enable(JsonParser.Feature.ALLOW_UNQUOTED_CONTROL_CHARS); - factory.enable(JsonParser.Feature.ALLOW_UNQUOTED_FIELD_NAMES); + JsonFactory factory = JsonFactory.builder() + //Json Read enabled + .enable(JsonReadFeature.ALLOW_NON_NUMERIC_NUMBERS) + .enable(JsonReadFeature.ALLOW_LEADING_ZEROS_FOR_NUMBERS) + .enable(JsonReadFeature.ALLOW_SINGLE_QUOTES) + .enable(JsonReadFeature.ALLOW_BACKSLASH_ESCAPING_ANY_CHARACTER) + .enable(JsonReadFeature.ALLOW_UNESCAPED_CONTROL_CHARS) + .enable(JsonReadFeature.ALLOW_UNQUOTED_FIELD_NAMES) + + //Json Read disabled + .disable(JsonReadFeature.ALLOW_JAVA_COMMENTS) // this is default as of 2.10.0 + .disable(JsonReadFeature.ALLOW_YAML_COMMENTS) // this is default as of 2.10.0 + + //Json Write enabled + .enable(JsonWriteFeature.QUOTE_FIELD_NAMES) // this is default as of 2.10.0 + .enable(JsonWriteFeature.WRITE_NAN_AS_STRINGS) // this is default as of 2.10.0 + + //Json Write disabled + .disable(JsonWriteFeature.ESCAPE_NON_ASCII) // this is default as of 2.10.0 + .disable(JsonWriteFeature.WRITE_NUMBERS_AS_STRINGS) // this is default as of 2.10.0 + .build(); //Json Parser disabled - factory.disable(JsonParser.Feature.ALLOW_COMMENTS); - factory.disable(JsonParser.Feature.ALLOW_YAML_COMMENTS); factory.disable(JsonParser.Feature.AUTO_CLOSE_SOURCE); factory.disable(JsonParser.Feature.STRICT_DUPLICATE_DETECTION); - //Json generator enabled + //Json Generator enabled factory.enable(JsonGenerator.Feature.IGNORE_UNKNOWN); - factory.enable(JsonGenerator.Feature.QUOTE_FIELD_NAMES); - factory.enable(JsonGenerator.Feature.QUOTE_NON_NUMERIC_NUMBERS); - //Json generator disabled + + //Json Generator disabled factory.disable(JsonGenerator.Feature.AUTO_CLOSE_JSON_CONTENT); factory.disable(JsonGenerator.Feature.AUTO_CLOSE_TARGET); - factory.disable(JsonGenerator.Feature.ESCAPE_NON_ASCII); factory.disable(JsonGenerator.Feature.FLUSH_PASSED_TO_STREAM); - factory.disable(JsonGenerator.Feature.STRICT_DUPLICATE_DETECTION); - factory.disable(JsonGenerator.Feature.WRITE_BIGDECIMAL_AS_PLAIN); - factory.disable(JsonGenerator.Feature.WRITE_NUMBERS_AS_STRINGS); + factory.disable(JsonGenerator.Feature.STRICT_DUPLICATE_DETECTION); // this is default as of 2.3.0 + factory.disable(JsonGenerator.Feature.WRITE_BIGDECIMAL_AS_PLAIN); // this is default as of 2.3.0 + return factory; } @@ -206,7 +198,7 @@ public void toJson(T value, OutputStream output) { private static final class TypeSerializer extends InternalSerializer { public TypeSerializer(ObjectMapper mapper, JavaType type) { - super(mapper.reader(type), mapper.writerFor(type)); + super(mapper.readerFor(type), mapper.writerFor(type)); } public TypeSerializer(ObjectMapper mapper, Type type) { @@ -216,7 +208,7 @@ public TypeSerializer(ObjectMapper mapper, Type type) { private static final class ClassSerializer extends InternalSerializer { public ClassSerializer(ObjectMapper mapper, Class clazz) { - super(mapper.reader(clazz), mapper.writerFor(clazz)); + super(mapper.readerFor(clazz), mapper.writerFor(clazz)); } } diff --git a/aws-lambda-java-serialization/src/main/java/com/amazonaws/services/lambda/runtime/serialization/util/SerializeUtil.java b/aws-lambda-java-serialization/src/main/java/com/amazonaws/services/lambda/runtime/serialization/util/SerializeUtil.java index bd4c7450e..f6acb528f 100644 --- a/aws-lambda-java-serialization/src/main/java/com/amazonaws/services/lambda/runtime/serialization/util/SerializeUtil.java +++ b/aws-lambda-java-serialization/src/main/java/com/amazonaws/services/lambda/runtime/serialization/util/SerializeUtil.java @@ -71,9 +71,10 @@ public static T deserializeDateTime(Class dateTimeClass, String dateTimeS */ @SuppressWarnings({"unchecked"}) public static String serializeDateTime(T dateTime, ClassLoader classLoader) { - Class dateTimeFormatterClass = loadCustomerClass("org.joda.time.format.DateTimeFormatter", classLoader); - Class dateTimeFormatClass = loadCustomerClass("org.joda.time.format.ISODateTimeFormat", classLoader); - Class readableInstantInterface = loadCustomerClass("org.joda.time.ReadableInstant", classLoader); + // Workaround not to let maven shade plugin relocating string literals https://issues.apache.org/jira/browse/MSHADE-156 + Class dateTimeFormatterClass = loadCustomerClass("com.amazonaws.lambda.unshade.thirdparty.org.joda.time.format.DateTimeFormatter", classLoader); + Class dateTimeFormatClass = loadCustomerClass("com.amazonaws.lambda.unshade.thirdparty.org.joda.time.format.ISODateTimeFormat", classLoader); + Class readableInstantInterface = loadCustomerClass("com.amazonaws.lambda.unshade.thirdparty.org.joda.time.ReadableInstant", classLoader); return serializeDateTimeHelper(dateTime, dateTimeFormatterClass, dateTimeFormatClass, readableInstantInterface); } diff --git a/aws-lambda-java-serialization/src/test/java/com/amazonaws/services/lambda/runtime/serialization/events/LambdaEventSerializersTest.java b/aws-lambda-java-serialization/src/test/java/com/amazonaws/services/lambda/runtime/serialization/events/LambdaEventSerializersTest.java deleted file mode 100644 index 151e71f56..000000000 --- a/aws-lambda-java-serialization/src/test/java/com/amazonaws/services/lambda/runtime/serialization/events/LambdaEventSerializersTest.java +++ /dev/null @@ -1,217 +0,0 @@ -/* Copyright 2020 Amazon.com, Inc. or its affiliates. All Rights Reserved. */ - -package com.amazonaws.services.lambda.runtime.serialization.events; - -import com.amazonaws.services.lambda.runtime.events.*; -import com.amazonaws.services.lambda.runtime.serialization.PojoSerializer; -import com.fasterxml.jackson.databind.ObjectMapper; -import org.junit.jupiter.api.Test; - -import java.io.ByteArrayOutputStream; -import java.io.IOException; -import java.nio.charset.StandardCharsets; -import java.nio.file.Files; -import java.nio.file.Path; -import java.nio.file.Paths; - -import static org.junit.jupiter.api.Assertions.assertEquals; - - -public class LambdaEventSerializersTest { - - private static final ObjectMapper OBJECT_MAPPER = new ObjectMapper(); - public static final ClassLoader SYSTEM_CLASS_LOADER = ClassLoader.getSystemClassLoader(); - - @Test - public void testAPIGatewayProxyRequestEvent() throws IOException { - String expected = readEvent("api_gateway_proxy_request_event.json"); - String actual = deserializeSerializeJsonToString(expected, APIGatewayProxyRequestEvent.class); - - assertJsonEqual(expected, actual); - } - - @Test - public void testAPIGatewayProxyResponseEvent() throws IOException { - String expected = readEvent("api_gateway_proxy_response_event.json"); - String actual = deserializeSerializeJsonToString(expected, APIGatewayProxyResponseEvent.class); - - assertJsonEqual(expected, actual); - } - - @Test - public void testCloudFrontEvent() throws IOException { - String expected = readEvent("cloud_front_event.json"); - String actual = deserializeSerializeJsonToString(expected, CloudFrontEvent.class); - - assertJsonEqual(expected, actual); - } - - @Test - public void testCloudWatchLogsEvent() throws IOException { - String expected = readEvent("cloud_watch_logs_event.json"); - String actual = deserializeSerializeJsonToString(expected, CloudWatchLogsEvent.class); - - assertJsonEqual(expected, actual); - } - - @Test - public void testCodeCommitEvent() throws IOException { - String expected = readEvent("code_commit_event.json"); - String actual = deserializeSerializeJsonToString(expected, CodeCommitEvent.class); - - assertJsonEqual(expected, actual); - } - - @Test - public void testCognitoEvent() throws IOException { - String expected = readEvent("cognito_event.json"); - String actual = deserializeSerializeJsonToString(expected, CognitoEvent.class); - - assertJsonEqual(expected, actual); - } - - @Test - public void testConfigEvent() throws IOException { - String expected = readEvent("config_event.json"); - String actual = deserializeSerializeJsonToString(expected, ConfigEvent.class); - - assertJsonEqual(expected, actual); - } - - @Test - public void testDynamodbEvent() throws IOException { - String expected = readEvent("dynamodb_event.json"); - String actual = deserializeSerializeJsonToString(expected, DynamodbEvent.class); - - assertJsonEqual(expected, actual); - } - - @Test - public void testIoTButtonEvent() throws IOException { - String expected = readEvent("iot_button_event.json"); - String actual = deserializeSerializeJsonToString(expected, IoTButtonEvent.class); - - assertJsonEqual(expected, actual); - } - - @Test - public void testKinesisAnalyticsFirehoseInputPreprocessingEvent() throws IOException { - String expected = readEvent("kinesis_analytics_firehose_input_preprocessing_event.json"); - String actual = deserializeSerializeJsonToString(expected, KinesisAnalyticsFirehoseInputPreprocessingEvent.class); - - assertJsonEqual(expected, actual); - } - - @Test - public void testKinesisAnalyticsInputPreprocessingResponse() throws IOException { - String expected = readEvent("kinesis_analytics_input_preprocessing_response_event.json"); - String actual = deserializeSerializeJsonToString(expected, KinesisAnalyticsInputPreprocessingResponse.class); - - assertJsonEqual(expected, actual); - } - - @Test - public void testKinesisAnalyticsOutputDeliveryEvent() throws IOException { - String expected = readEvent("kinesis_analytics_output_delivery_event.json"); - String actual = deserializeSerializeJsonToString(expected, KinesisAnalyticsOutputDeliveryEvent.class); - - assertJsonEqual(expected, actual); - } - - @Test - public void testKinesisAnalyticsOutputDeliveryResponse() throws IOException { - String expected = readEvent("kinesis_analytics_output_delivery_response_event.json"); - String actual = deserializeSerializeJsonToString(expected, KinesisAnalyticsOutputDeliveryResponse.class); - - assertJsonEqual(expected, actual); - } - - @Test - public void testKinesisAnalyticsStreamsInputPreprocessingEvent() throws IOException { - String expected = readEvent("kinesis_analytics_streams_input_preprocessing_event.json"); - String actual = deserializeSerializeJsonToString(expected, KinesisAnalyticsStreamsInputPreprocessingEvent.class); - - assertJsonEqual(expected, actual); - } - - @Test - public void testKinesisEvent() throws IOException { - String expected = readEvent("kinesis_event.json"); - String actual = deserializeSerializeJsonToString(expected, KinesisEvent.class); - - assertJsonEqual(expected, actual); - } - - @Test - public void testKinesisFirehoseEvent() throws IOException { - String expected = readEvent("kinesis_firehose_event.json"); - String actual = deserializeSerializeJsonToString(expected, KinesisFirehoseEvent.class); - - assertJsonEqual(expected, actual); - } - - @Test - public void testLexEvent() throws IOException { - String expected = readEvent("lex_event.json"); - String actual = deserializeSerializeJsonToString(expected, LexEvent.class); - - assertJsonEqual(expected, actual); - } - - @Test - public void testS3Event() throws IOException { - String expected = readEvent("s3_event.json"); - String actual = deserializeSerializeJsonToString(expected, S3Event.class); - - assertJsonEqual(expected, actual); - } - - @Test - public void testScheduledEvent() throws IOException { - String expected = readEvent("scheduled_event.json"); - String actual = deserializeSerializeJsonToString(expected, ScheduledEvent.class); - - assertJsonEqual(expected, actual); - } - - @Test - public void testSNSEvent() throws IOException { - String expected = readEvent("sns_event.json"); - String actual = deserializeSerializeJsonToString(expected, SNSEvent.class); - - assertJsonEqual(expected, actual); - } - - @Test - public void testSQSEvent() throws IOException { - String expected = readEvent("sqs_event.json"); - String actual = deserializeSerializeJsonToString(expected, SQSEvent.class); - - assertJsonEqual(expected, actual); - } - - private String readEvent(String filename) throws IOException { - Path filePath = Paths.get("src", "test", "resources", "event_models", filename); - byte[] bytes = Files.readAllBytes(filePath); - return bytesToString(bytes); - } - - private String deserializeSerializeJsonToString(String expected, Class modelClass) { - PojoSerializer serializer = LambdaEventSerializers.serializerFor(modelClass, SYSTEM_CLASS_LOADER); - - T event = serializer.fromJson(expected); - ByteArrayOutputStream baos = new ByteArrayOutputStream(); - - serializer.toJson(event, baos); - return bytesToString(baos.toByteArray()); - } - - private String bytesToString(byte[] bytes) { - return new String(bytes, StandardCharsets.UTF_8); - } - - private void assertJsonEqual(String expected, String actual) throws IOException { - assertEquals(OBJECT_MAPPER.readTree(expected), OBJECT_MAPPER.readTree(actual)); - } - -} diff --git a/aws-lambda-java-serialization/src/test/java/com/amazonaws/services/lambda/runtime/serialization/events/serializers/S3EventSerializerTest.java b/aws-lambda-java-serialization/src/test/java/com/amazonaws/services/lambda/runtime/serialization/events/serializers/S3EventSerializerTest.java deleted file mode 100644 index 8b2d2e875..000000000 --- a/aws-lambda-java-serialization/src/test/java/com/amazonaws/services/lambda/runtime/serialization/events/serializers/S3EventSerializerTest.java +++ /dev/null @@ -1,73 +0,0 @@ -/* Copyright 2020 Amazon.com, Inc. or its affiliates. All Rights Reserved. */ - -package com.amazonaws.services.lambda.runtime.serialization.events.serializers; - -import com.amazonaws.services.lambda.runtime.events.S3Event; -import com.amazonaws.services.lambda.runtime.events.models.s3.S3EventNotification; -import com.fasterxml.jackson.databind.ObjectMapper; -import org.junit.jupiter.api.Test; - -import java.io.ByteArrayOutputStream; -import java.io.IOException; -import java.nio.charset.StandardCharsets; -import java.nio.file.Files; -import java.nio.file.Path; -import java.nio.file.Paths; - -import static org.junit.jupiter.api.Assertions.assertEquals; - - -public class S3EventSerializerTest { - - private static final ObjectMapper OBJECT_MAPPER = new ObjectMapper(); - public static final ClassLoader SYSTEM_CLASS_LOADER = ClassLoader.getSystemClassLoader(); - - @Test - public void testSerDeS3Event() throws IOException { - S3EventSerializer s3EventSerializer = getS3EventSerializerWithClass(S3Event.class); - - String expected = readEvent("s3_event.json"); - String actual = deserializeSerializeJsonToString(s3EventSerializer, expected); - - assertJsonEqual(expected, actual); - } - - @Test - public void testSerDeS3EventNotification() throws IOException { - S3EventSerializer s3EventSerializer = getS3EventSerializerWithClass(S3EventNotification.class); - - String expected = readEvent("s3_event.json"); - String actual = deserializeSerializeJsonToString(s3EventSerializer, expected); - - assertJsonEqual(expected, actual); - } - - private S3EventSerializer getS3EventSerializerWithClass(Class modelClass) { - return new S3EventSerializer() - .withClass(modelClass) - .withClassLoader(SYSTEM_CLASS_LOADER); - } - - private String readEvent(String filename) throws IOException { - Path filePath = Paths.get("src", "test", "resources", "event_models", filename); - byte[] bytes = Files.readAllBytes(filePath); - return bytesToString(bytes); - } - - private String bytesToString(byte[] bytes) { - return new String(bytes, StandardCharsets.UTF_8); - } - - private void assertJsonEqual(String expected, String actual) throws IOException { - assertEquals(OBJECT_MAPPER.readTree(expected), OBJECT_MAPPER.readTree(actual)); - } - - private String deserializeSerializeJsonToString(S3EventSerializer s3EventSerializer, String expected) { - T event = s3EventSerializer.fromJson(expected); - ByteArrayOutputStream baos = new ByteArrayOutputStream(); - - s3EventSerializer.toJson(event, baos); - return bytesToString(baos.toByteArray()); - } - -} diff --git a/aws-lambda-java-serialization/src/test/resources/event_models/api_gateway_proxy_request_event.json b/aws-lambda-java-serialization/src/test/resources/event_models/api_gateway_proxy_request_event.json deleted file mode 100644 index 66c5d9535..000000000 --- a/aws-lambda-java-serialization/src/test/resources/event_models/api_gateway_proxy_request_event.json +++ /dev/null @@ -1,60 +0,0 @@ -{ - "version": "1.0", - "path": "/test/hello", - "headers": { - "Accept": "text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8", - "Accept-Encoding": "gzip, deflate, lzma, sdch, br", - "Accept-Language": "en-US,en;q=0.8", - "CloudFront-Forwarded-Proto": "https", - "CloudFront-Is-Desktop-Viewer": "true", - "CloudFront-Is-Mobile-Viewer": "false", - "CloudFront-Is-SmartTV-Viewer": "false", - "CloudFront-Is-Tablet-Viewer": "false", - "CloudFront-Viewer-Country": "US", - "Host": "wt6mne2s9k.execute-api.us-west-2.amazonaws.com", - "Upgrade-Insecure-Requests": "1", - "User-Agent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_11_6) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/52.0.2743.82 Safari/537.36 OPR/39.0.2256.48", - "Via": "1.1 fb7cca60f0ecd82ce07790c9c5eef16c.cloudfront.net (CloudFront)", - "X-Amz-Cf-Id": "nBsWBOrSHMgnaROZJK1wGCZ9PcRcSpq_oSXZNQwQ10OTZL4cimZo3g==", - "X-Forwarded-For": "192.168.100.1, 192.168.1.1", - "X-Forwarded-Port": "443", - "X-Forwarded-Proto": "https" - }, - "pathParameters": { - "proxy": "hello" - }, - "requestContext": { - "path": "/{proxy+}", - "accountId": "123456789012", - "resourceId": "nl9h80", - "stage": "test-invoke-stage", - "requestId": "test-invoke-request", - "identity": { - "cognitoIdentityPoolId": "", - "accountId": "123456789012", - "cognitoIdentityId": "", - "caller": "AIDAJTIRKKKER4HCKVJZG", - "apiKey": "test-invoke-api-key", - "sourceIp": "test-invoke-source-ip", - "accessKey": "ASIAI6ANUE2RZBMJDQ5A", - "cognitoAuthenticationType": "", - "cognitoAuthenticationProvider": "", - "userArn": "arn:aws:iam::123456789012:user/kdeding", - "userAgent": "Apache-HttpClient/4.5.x (Java/1.8.0_131)", - "user": "AIDAJTIRKKKER4HCKVJZG" - }, - "resourcePath": "/{proxy+}", - "httpMethod": "POST", - "apiId": "r275xc9bmd" - }, - "resource": "/{proxy+}", - "httpMethod": "GET", - "isBase64Encoded": false, - "body": "{ \"callerName\": \"John\" }", - "queryStringParameters": { - "name": "me" - }, - "stageVariables": { - "stageVarName": "stageVarValue" - } -} \ No newline at end of file diff --git a/aws-lambda-java-serialization/src/test/resources/event_models/api_gateway_proxy_response_event.json b/aws-lambda-java-serialization/src/test/resources/event_models/api_gateway_proxy_response_event.json deleted file mode 100644 index 1b18bcfde..000000000 --- a/aws-lambda-java-serialization/src/test/resources/event_models/api_gateway_proxy_response_event.json +++ /dev/null @@ -1,23 +0,0 @@ -{ - "statusCode": 200, - "headers": { - "Accept": "text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8", - "Accept-Encoding": "gzip, deflate, lzma, sdch, br", - "Accept-Language": "en-US,en;q=0.8", - "CloudFront-Forwarded-Proto": "https", - "CloudFront-Is-Desktop-Viewer": "true", - "CloudFront-Is-Mobile-Viewer": "false", - "CloudFront-Is-SmartTV-Viewer": "false", - "CloudFront-Is-Tablet-Viewer": "false", - "CloudFront-Viewer-Country": "US", - "Host": "wt6mne2s9k.execute-api.us-west-2.amazonaws.com", - "Upgrade-Insecure-Requests": "1", - "User-Agent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_11_6) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/52.0.2743.82 Safari/537.36 OPR/39.0.2256.48", - "Via": "1.1 fb7cca60f0ecd82ce07790c9c5eef16c.cloudfront.net (CloudFront)", - "X-Amz-Cf-Id": "nBsWBOrSHMgnaROZJK1wGCZ9PcRcSpq_oSXZNQwQ10OTZL4cimZo3g==", - "X-Forwarded-For": "192.168.100.1, 192.168.1.1", - "X-Forwarded-Port": "443", - "X-Forwarded-Proto": "https" - }, - "body": "Hello World" -} \ No newline at end of file diff --git a/aws-lambda-java-serialization/src/test/resources/event_models/cloud_front_event.json b/aws-lambda-java-serialization/src/test/resources/event_models/cloud_front_event.json deleted file mode 100644 index e91674807..000000000 --- a/aws-lambda-java-serialization/src/test/resources/event_models/cloud_front_event.json +++ /dev/null @@ -1,30 +0,0 @@ -{ - "Records": [ - { - "cf": { - "config": { - "distributionId": "EDFDVBD6EXAMPLE" - }, - "request": { - "clientIp": "2001:0db8:85a3:0:0:8a2e:0370:7334", - "method": "GET", - "uri": "/picture.jpg", - "headers": { - "host": [ - { - "key": "Host", - "value": "d111111abcdef8.cloudfront.net" - } - ], - "user-agent": [ - { - "key": "User-Agent", - "value": "curl/7.51.0" - } - ] - } - } - } - } - ] -} \ No newline at end of file diff --git a/aws-lambda-java-serialization/src/test/resources/event_models/cloud_watch_logs_event.json b/aws-lambda-java-serialization/src/test/resources/event_models/cloud_watch_logs_event.json deleted file mode 100644 index 2b455b9bc..000000000 --- a/aws-lambda-java-serialization/src/test/resources/event_models/cloud_watch_logs_event.json +++ /dev/null @@ -1,5 +0,0 @@ -{ - "awslogs": { - "data": "H4sIAAAAAAAAAHWPwQqCQBCGX0Xm7EFtK+smZBEUgXoLCdMhFtKV3akI8d0bLYmibvPPN3wz00CJxmQnTO41whwWQRIctmEcB6sQbFC3CjW3XW8kxpOpP+OC22d1Wml1qZkQGtoMsScxaczKN3plG8zlaHIta5KqWsozoTYw3/djzwhpLwivWFGHGpAFe7DL68JlBUk+l7KSN7tCOEJ4M3/qOI49vMHj+zCKdlFqLaU2ZHV2a4Ct/an0/ivdX8oYc1UVX860fQDQiMdxRQEAAA==" - } -} \ No newline at end of file diff --git a/aws-lambda-java-serialization/src/test/resources/event_models/code_commit_event.json b/aws-lambda-java-serialization/src/test/resources/event_models/code_commit_event.json deleted file mode 100644 index 4e111cdd1..000000000 --- a/aws-lambda-java-serialization/src/test/resources/event_models/code_commit_event.json +++ /dev/null @@ -1,27 +0,0 @@ -{ - "Records": [ - { - "eventId": "163179e8-a983-4e22-8423-38473a2589fd", - "eventVersion": "1.0", - "eventTime": "2017-07-17T19:38:56.745Z", - "eventTriggerName": "notifyme", - "eventPartNumber": 1, - "codecommit": { - "references": [ - { - "commit": "7a95e78397313b32dc3c1beda4f5c2676c0c1bea", - "ref": "refs/heads/master", - "created": true - } - ] - }, - "eventName": "ReferenceChanges", - "eventTriggerConfigId": "e6f61a39-7e60-47b0-b206-0dfbd2ca0e6e", - "eventSourceARN": "arn:aws:codecommit:us-west-2:303480592763:garbage", - "userIdentityARN": "arn:aws:sts::303480592763:assumed-role/Admin/adsuresh-Isengard", - "eventSource": "aws:codecommit", - "awsRegion": "us-west-2", - "eventTotalParts": 1 - } - ] -} \ No newline at end of file diff --git a/aws-lambda-java-serialization/src/test/resources/event_models/cognito_event.json b/aws-lambda-java-serialization/src/test/resources/event_models/cognito_event.json deleted file mode 100644 index 88cc60627..000000000 --- a/aws-lambda-java-serialization/src/test/resources/event_models/cognito_event.json +++ /dev/null @@ -1,20 +0,0 @@ -{ - "version": 2, - "eventType": "SyncTrigger", - "region": "us-east-1", - "identityPoolId": "identityPoolId", - "identityId": "identityId", - "datasetName": "datasetName", - "datasetRecords": { - "sampleKey1": { - "oldValue": "oldValue1", - "newValue": "newValue1", - "op": "replace" - }, - "sampleKey2": { - "oldValue": "oldValue2", - "newValue": "newValue2", - "op": "replace" - } - } -} diff --git a/aws-lambda-java-serialization/src/test/resources/event_models/config_event.json b/aws-lambda-java-serialization/src/test/resources/event_models/config_event.json deleted file mode 100644 index a5d158de4..000000000 --- a/aws-lambda-java-serialization/src/test/resources/event_models/config_event.json +++ /dev/null @@ -1,12 +0,0 @@ -{ - "invokingEvent": "{\"configurationItem\":{\"configurationItemCaptureTime\":\"2016-02-17T01:36:34.043Z\",\"awsAccountId\":\"000000000000\",\"configurationItemStatus\":\"OK\",\"resourceId\":\"i-00000000\",\"ARN\":\"arn:aws:ec2:us-east-1:000000000000:instance/i-00000000\",\"awsRegion\":\"us-east-1\",\"availabilityZone\":\"us-east-1a\",\"resourceType\":\"AWS::EC2::Instance\",\"tags\":{\"Foo\":\"Bar\"},\"relationships\":[{\"resourceId\":\"eipalloc-00000000\",\"resourceType\":\"AWS::EC2::EIP\",\"name\":\"Is attached to ElasticIp\"}],\"configuration\":{\"foo\":\"bar\"}},\"messageType\":\"ConfigurationItemChangeNotification\"}", - "ruleParameters": "{\"myParameterKey\":\"myParameterValue\"}", - "resultToken": "myResultToken", - "eventLeftScope": false, - "executionRoleArn": "arn:aws:iam::012345678912:role/config-role", - "configRuleArn": "arn:aws:config:us-east-1:012345678912:config-rule/config-rule-0123456", - "configRuleName": "change-triggered-config-rule", - "configRuleId": "config-rule-0123456", - "accountId": "012345678912", - "version": "1.0" -} \ No newline at end of file diff --git a/aws-lambda-java-serialization/src/test/resources/event_models/dynamodb_event.json b/aws-lambda-java-serialization/src/test/resources/event_models/dynamodb_event.json deleted file mode 100644 index 6a9c6ac4f..000000000 --- a/aws-lambda-java-serialization/src/test/resources/event_models/dynamodb_event.json +++ /dev/null @@ -1,166 +0,0 @@ -{ - "Records": [ - { - "eventID": "1", - "dynamodb": { - "Keys": { - "Id": { - "N": "101" - } - }, - "NewImage": { - "Message": { - "S": "New item!" - }, - "Id": { - "N": "101" - } - }, - "StreamViewType": "NEW_AND_OLD_IMAGES", - "SequenceNumber": "111", - "SizeBytes": 26 - }, - "awsRegion": "us-west-2", - "eventName": "INSERT", - "eventSourceARN": "arn:aws:dynamodb:us-west-2:account-id:table/ExampleTableWithStream/stream/2015-06-27T00:48:05.899", - "eventSource": "aws:dynamodb" - }, - { - "eventID": "2", - "dynamodb": { - "OldImage": { - "Message": { - "S": "New item!" - }, - "Id": { - "N": "101" - } - }, - "SequenceNumber": "222", - "Keys": { - "Id": { - "N": "101" - } - }, - "SizeBytes": 59, - "NewImage": { - "Message": { - "S": "This item has changed" - }, - "Id": { - "N": "101" - } - }, - "StreamViewType": "NEW_AND_OLD_IMAGES" - }, - "awsRegion": "us-west-2", - "eventName": "MODIFY", - "eventSourceARN": "arn:aws:dynamodb:us-west-2:account-id:table/ExampleTableWithStream/stream/2015-06-27T00:48:05.899", - "eventSource": "aws:dynamodb" - }, - { - "eventID": "3", - "dynamodb": { - "Keys": { - "Id": { - "N": "101" - } - }, - "SizeBytes": 38, - "SequenceNumber": "333", - "OldImage": { - "Message": { - "S": "This item has changed" - }, - "Id": { - "N": "101" - } - }, - "StreamViewType": "NEW_AND_OLD_IMAGES" - }, - "awsRegion": "us-west-2", - "eventName": "REMOVE", - "eventSourceARN": "arn:aws:dynamodb:us-west-2:account-id:table/ExampleTableWithStream/stream/2015-06-27T00:48:05.899", - "eventSource": "aws:dynamodb" - }, - { - "eventID": "77742f55658ca1effb9ae758c65c8a51", - "eventName": "INSERT", - "eventSource": "aws:dynamodb", - "awsRegion": "us-west-2", - "dynamodb": { - "ApproximateCreationDateTime": 1.44130524E9, - "Keys": { - "Id": { - "N": "147" - } - }, - "NewImage": { - "attr_M": { - "M": { - "attr_M1": { - "N": "1580756226" - }, - "attr_M2": { - "S": "1580756226" - } - } - }, - "attr_L": { - "L": [ - { - "N": "1580756226" - }, - { - "S": "1580756226" - } - ] - }, - "attr_BOOL": { - "BOOL": true - }, - "s3info": { - "S": "lambda-ddb-streams-int-test/a2b4c06b-2360-44ac-9bd7-2bab669246b8" - }, - "attr_NULL": { - "NULL": true - }, - "attr_NS": { - "NS": [ - "1580756227", - "1580756226" - ] - }, - "attr_SS": { - "SS": [ - "1580756226", - "1580756227" - ] - }, - "attr_BS": { - "BS": [ - "MTU4MDc1NjIyNg==", - "MTU4MDc1NjIyNw==" - ] - }, - "attr_S": { - "S": "1580756226" - }, - "Id": { - "N": "147" - }, - "attr_B": { - "B": "MTU4MDc1NjIyNg==" - }, - "attr_N": { - "N": "1580756226" - } - }, - "SequenceNumber": "2700000000000002229109", - "SizeBytes": 285, - "StreamViewType": "NEW_AND_OLD_IMAGES" - }, - "eventSourceARN": "arn:aws:dynamodb:us-west-2:059493405231:table/lambda-ddb-streams-int-test/stream/2015-06-26T18:21:25.123" - } - ] -} diff --git a/aws-lambda-java-serialization/src/test/resources/event_models/iot_button_event.json b/aws-lambda-java-serialization/src/test/resources/event_models/iot_button_event.json deleted file mode 100644 index 8988caf0e..000000000 --- a/aws-lambda-java-serialization/src/test/resources/event_models/iot_button_event.json +++ /dev/null @@ -1,5 +0,0 @@ -{ - "serialNumber": "ABCDEFG12345", - "clickType": "SINGLE", - "batteryVoltage": "2000 mV" -} \ No newline at end of file diff --git a/aws-lambda-java-serialization/src/test/resources/event_models/kinesis_analytics_firehose_input_preprocessing_event.json b/aws-lambda-java-serialization/src/test/resources/event_models/kinesis_analytics_firehose_input_preprocessing_event.json deleted file mode 100644 index 862f17f29..000000000 --- a/aws-lambda-java-serialization/src/test/resources/event_models/kinesis_analytics_firehose_input_preprocessing_event.json +++ /dev/null @@ -1,14 +0,0 @@ -{ - "invocationId":"00540a87-5050-496a-84e4-e7d92bbaf5e2", - "applicationArn":"arn:aws:kinesisanalytics:us-east-1:12345678911:application/lambda-test", - "streamArn":"arn:aws:firehose:us-east-1:AAAAAAAAAAAA:deliverystream/lambda-test", - "records":[ - { - "recordId":"49572672223665514422805246926656954630972486059535892482", - "data":"aGVsbG8gd29ybGQ=", - "kinesisFirehoseRecordMetadata":{ - "approximateArrivalTimestamp":1520280173 - } - } - ] - } \ No newline at end of file diff --git a/aws-lambda-java-serialization/src/test/resources/event_models/kinesis_analytics_input_preprocessing_response_event.json b/aws-lambda-java-serialization/src/test/resources/event_models/kinesis_analytics_input_preprocessing_response_event.json deleted file mode 100644 index 59b899d6b..000000000 --- a/aws-lambda-java-serialization/src/test/resources/event_models/kinesis_analytics_input_preprocessing_response_event.json +++ /dev/null @@ -1,10 +0,0 @@ -{ - "records": [ - { - "recordId": "49572672223665514422805246926656954630972486059535892482", - "result": "Ok", - "data": "SEVMTE8gV09STEQ=" - } - ] - } - \ No newline at end of file diff --git a/aws-lambda-java-serialization/src/test/resources/event_models/kinesis_analytics_output_delivery_event.json b/aws-lambda-java-serialization/src/test/resources/event_models/kinesis_analytics_output_delivery_event.json deleted file mode 100644 index d42688d73..000000000 --- a/aws-lambda-java-serialization/src/test/resources/event_models/kinesis_analytics_output_delivery_event.json +++ /dev/null @@ -1,10 +0,0 @@ -{ - "invocationId": "00540a87-5050-496a-84e4-e7d92bbaf5e2", - "applicationArn": "arn:aws:kinesisanalytics:us-east-1:12345678911:application/lambda-test", - "records": [ - { - "recordId": "49572672223665514422805246926656954630972486059535892482", - "data": "aGVsbG8gd29ybGQ=" - } - ] -} \ No newline at end of file diff --git a/aws-lambda-java-serialization/src/test/resources/event_models/kinesis_analytics_output_delivery_response_event.json b/aws-lambda-java-serialization/src/test/resources/event_models/kinesis_analytics_output_delivery_response_event.json deleted file mode 100644 index dba7b21d1..000000000 --- a/aws-lambda-java-serialization/src/test/resources/event_models/kinesis_analytics_output_delivery_response_event.json +++ /dev/null @@ -1,8 +0,0 @@ -{ - "records": [ - { - "recordId": "49572672223665514422805246926656954630972486059535892482", - "result": "Ok" - } - ] -} diff --git a/aws-lambda-java-serialization/src/test/resources/event_models/kinesis_analytics_streams_input_preprocessing_event.json b/aws-lambda-java-serialization/src/test/resources/event_models/kinesis_analytics_streams_input_preprocessing_event.json deleted file mode 100644 index f7ec383bd..000000000 --- a/aws-lambda-java-serialization/src/test/resources/event_models/kinesis_analytics_streams_input_preprocessing_event.json +++ /dev/null @@ -1,17 +0,0 @@ -{ - "invocationId": "00540a87-5050-496a-84e4-e7d92bbaf5e2", - "applicationArn": "arn:aws:kinesisanalytics:us-east-1:12345678911:application/lambda-test", - "streamArn": "arn:aws:kinesis:us-east-1:AAAAAAAAAAAA:stream/lambda-test", - "records": [ - { - "recordId": "49572672223665514422805246926656954630972486059535892482", - "data": "aGVsbG8gd29ybGQ=", - "kinesisStreamRecordMetadata":{ - "shardId" :"shardId-000000000003", - "partitionKey":"7400791606", - "sequenceNumber":"49572672223665514422805246926656954630972486059535892482", - "approximateArrivalTimestamp":1520280173 - } - } - ] -} \ No newline at end of file diff --git a/aws-lambda-java-serialization/src/test/resources/event_models/kinesis_event.json b/aws-lambda-java-serialization/src/test/resources/event_models/kinesis_event.json deleted file mode 100644 index 854794cfd..000000000 --- a/aws-lambda-java-serialization/src/test/resources/event_models/kinesis_event.json +++ /dev/null @@ -1,35 +0,0 @@ -{ - "Records": [ - { - "kinesis": { - "partitionKey": "partitionKey-3", - "kinesisSchemaVersion": "1.0", - "data": "SGVsbG8sIHRoaXMgaXMgYSB0ZXN0IDEyMy4=", - "sequenceNumber": "49545115243490985018280067714973144582180062593244200961" - }, - "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": "us-east-1" - }, - { - "kinesis": { - "partitionKey": "partitionKey-3", - "kinesisSchemaVersion": "1.0", - "data": "SGVsbG8sIHRoaXMgaXMgYSB0ZXN0IDEyMy4=", - "sequenceNumber": "49545115243490985018280067714973144582180062593244200961", - "approximateArrivalTimestamp": 1.455606024806E9 - }, - "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": "us-east-1" - } - ] -} diff --git a/aws-lambda-java-serialization/src/test/resources/event_models/kinesis_firehose_event.json b/aws-lambda-java-serialization/src/test/resources/event_models/kinesis_firehose_event.json deleted file mode 100644 index 3b1cd6c76..000000000 --- a/aws-lambda-java-serialization/src/test/resources/event_models/kinesis_firehose_event.json +++ /dev/null @@ -1,33 +0,0 @@ -{ - "invocationId": "invoked123", - "deliveryStreamArn": "aws:lambda:events", - "region": "us-west-2", - "records": [ - { - "data": "SGVsbG8gV29ybGQ=", - "recordId": "record1", - "approximateArrivalEpoch": 1507217624302, - "approximateArrivalTimestamp": 1507217624302, - "kinesisRecordMetadata": { - "shardId": "shardId-000000000000", - "partitionKey": "4d1ad2b9-24f8-4b9d-a088-76e9947c317a", - "approximateArrivalTimestamp": "1507217624302", - "sequenceNumber": "49546986683135544286507457936321625675700192471156785154", - "subsequenceNumber": "" - } - }, - { - "data": "SGVsbG8gV29ybGQ=", - "recordId": "record2", - "approximateArrivalEpoch": 1507217624302, - "approximateArrivalTimestamp": 1507217624302, - "kinesisRecordMetadata": { - "shardId": "shardId-000000000001", - "partitionKey": "4d1ad2b9-24f8-4b9d-a088-76e9947c318a", - "approximateArrivalTimestamp": "1507217624302", - "sequenceNumber": "49546986683135544286507457936321625675700192471156785155", - "subsequenceNumber": "" - } - } - ] -} \ No newline at end of file diff --git a/aws-lambda-java-serialization/src/test/resources/event_models/lex_event.json b/aws-lambda-java-serialization/src/test/resources/event_models/lex_event.json deleted file mode 100644 index 2961ac28c..000000000 --- a/aws-lambda-java-serialization/src/test/resources/event_models/lex_event.json +++ /dev/null @@ -1,24 +0,0 @@ -{ - "messageVersion": "1.0", - "invocationSource": "FulfillmentCodeHook or DialogCodeHook", - "userId": "user-id specified in the POST request to Amazon Lex.", - "sessionAttributes": { - "key1": "value1", - "key2": "value2" - }, - "bot": { - "name": "bot-name", - "alias": "bot-alias", - "version": "bot-version" - }, - "outputDialogMode": "Text or Voice, based on ContentType request header in runtime API request", - "currentIntent": { - "name": "intent-name", - "slots": { - "slot1": "value", - "slot2": "value", - "slot3": "value" - }, - "confirmationStatus": "None" - } -} \ No newline at end of file diff --git a/aws-lambda-java-serialization/src/test/resources/event_models/s3_event.json b/aws-lambda-java-serialization/src/test/resources/event_models/s3_event.json deleted file mode 100644 index 519bc1ce0..000000000 --- a/aws-lambda-java-serialization/src/test/resources/event_models/s3_event.json +++ /dev/null @@ -1,40 +0,0 @@ -{ - "Records": [ - { - "eventVersion": "2.0", - "eventSource": "aws:s3", - "awsRegion": "us-east-1", - "eventTime": "1970-01-01T00:00:00.000Z", - "eventName": "ObjectCreated:Put", - "userIdentity": { - "principalId": "EXAMPLE" - }, - "requestParameters": { - "sourceIPAddress": "127.0.0.1" - }, - "responseElements": { - "x-amz-request-id": "C3D13FE58DE4C810", - "x-amz-id-2": "FMyUVURIY8/IgAtTv8xRjskZQpcIZ9KG4V5Wp6S7S/JRWeUWerMUE5JgHvANOjpD" - }, - "s3": { - "s3SchemaVersion": "1.0", - "configurationId": "testConfigRule", - "bucket": { - "name": "sourcebucket", - "ownerIdentity": { - "principalId": "EXAMPLE" - }, - "arn": "arn:aws:s3:::mybucket" - }, - "object": { - "key": "Happy%20Face.jpg", - "size": 1024, - "urlDecodedKey": "Happy Face.jpg", - "versionId": "version", - "eTag": "d41d8cd98f00b204e9800998ecf8427e", - "sequencer": "Happy Sequencer" - } - } - } - ] -} diff --git a/aws-lambda-java-serialization/src/test/resources/event_models/scheduled_event.json b/aws-lambda-java-serialization/src/test/resources/event_models/scheduled_event.json deleted file mode 100644 index 3d803cc24..000000000 --- a/aws-lambda-java-serialization/src/test/resources/event_models/scheduled_event.json +++ /dev/null @@ -1,12 +0,0 @@ -{ - "account": "123456789012", - "region": "us-east-1", - "detail": {}, - "detail-type": "Scheduled Event", - "source": "aws.events", - "time": "1970-01-01T00:00:00.000Z", - "id": "cdc73f9d-aea9-11e3-9d5a-835b769c0d9c", - "resources": [ - "arn:aws:events:us-east-1:123456789012:rule/my-schedule" - ] -} \ No newline at end of file diff --git a/aws-lambda-java-serialization/src/test/resources/event_models/sns_event.json b/aws-lambda-java-serialization/src/test/resources/event_models/sns_event.json deleted file mode 100644 index ff6246355..000000000 --- a/aws-lambda-java-serialization/src/test/resources/event_models/sns_event.json +++ /dev/null @@ -1,31 +0,0 @@ -{ - "Records": [ - { - "EventVersion": "1.0", - "EventSubscriptionArn": "arn:aws:sns:EXAMPLE", - "EventSource": "aws:sns", - "Sns": { - "Signature": "EXAMPLE", - "MessageId": "95df01b4-ee98-5cb9-9903-4c221d41eb5e", - "Type": "Notification", - "TopicArn": "arn:aws:sns:EXAMPLE", - "MessageAttributes": { - "Test": { - "Type": "String", - "Value": "TestString" - }, - "TestBinary": { - "Type": "Binary", - "Value": "TestBinary" - } - }, - "SignatureVersion": "1", - "Timestamp": "2015-06-03T17:43:27.020Z", - "SigningCertUrl": "EXAMPLE", - "Message": "Hello from SNS!", - "UnsubscribeUrl": "EXAMPLE", - "Subject": "TestInvoke" - } - } - ] -} diff --git a/aws-lambda-java-serialization/src/test/resources/event_models/sqs_event.json b/aws-lambda-java-serialization/src/test/resources/event_models/sqs_event.json deleted file mode 100644 index aa69c3509..000000000 --- a/aws-lambda-java-serialization/src/test/resources/event_models/sqs_event.json +++ /dev/null @@ -1,40 +0,0 @@ -{ - "Records": [ - { - "messageId" : "MessageID_1", - "receiptHandle" : "MessageReceiptHandle", - "body" : "Message Body", - "md5OfBody" : "fce0ea8dd236ccb3ed9b37dae260836f", - "md5OfMessageAttributes" : "582c92c5c5b6ac403040a4f3ab3115c9", - "eventSourceARN": "arn:aws:sqs:us-west-2:123456789012:SQSQueue", - "eventSource": "aws:sqs", - "awsRegion": "us-west-2", - "attributes" : { - "ApproximateReceiveCount" : "2", - "SentTimestamp" : "1520621625029", - "SenderId" : "AROAIWPX5BD2BHG722MW4:sender", - "ApproximateFirstReceiveTimestamp" : "1520621634884" - }, - "messageAttributes" : { - "Attribute3" : { - "binaryValue" : "MTEwMA==", - "stringListValues" : ["abc", "123"], - "binaryListValues" : ["MA==", "MQ==", "MA=="], - "dataType" : "Binary" - }, - "Attribute2" : { - "stringValue" : "123", - "stringListValues" : [ ], - "binaryListValues" : ["MQ==", "MA=="], - "dataType" : "Number" - }, - "Attribute1" : { - "stringValue" : "AttributeValue1", - "stringListValues" : [ ], - "binaryListValues" : [ ], - "dataType" : "String" - } - } - } - ] -} diff --git a/aws-lambda-java-tests/pom.xml b/aws-lambda-java-tests/pom.xml index f62a54136..314669968 100644 --- a/aws-lambda-java-tests/pom.xml +++ b/aws-lambda-java-tests/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-tests - 1.1.1 + 1.1.2 jar AWS Lambda Java Tests @@ -32,19 +32,20 @@ 1.8 1.8 UTF-8 - 5.7.0 + 5.9.2 + 0.8.7 com.amazonaws aws-lambda-java-serialization - 1.0.0 + 1.1.6 com.amazonaws aws-lambda-java-events - 3.11.0 + 3.16.1 org.junit.jupiter @@ -64,13 +65,13 @@ org.apache.commons commons-lang3 - 3.11 + 3.18.0 org.assertj assertj-core - 3.18.1 + 3.27.7 test @@ -127,7 +128,7 @@ org.jacoco jacoco-maven-plugin - 0.8.6 + ${jacoco.maven.plugin.version} default-prepare-agent @@ -219,14 +220,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 @@ -239,7 +238,7 @@ org.apache.maven.plugins maven-compiler-plugin - 3.8.1 + 3.11.0 ${maven.compiler.source} ${maven.compiler.target} @@ -253,4 +252,4 @@ - + \ No newline at end of file diff --git a/aws-lambda-java-tests/src/main/java/com/amazonaws/services/lambda/runtime/tests/EventLoader.java b/aws-lambda-java-tests/src/main/java/com/amazonaws/services/lambda/runtime/tests/EventLoader.java index 68cd37d3d..0c5d66206 100644 --- a/aws-lambda-java-tests/src/main/java/com/amazonaws/services/lambda/runtime/tests/EventLoader.java +++ b/aws-lambda-java-tests/src/main/java/com/amazonaws/services/lambda/runtime/tests/EventLoader.java @@ -45,10 +45,18 @@ public static CloudFrontEvent loadCloudFrontEvent(String filename) { return loadEvent(filename, CloudFrontEvent.class); } + public static CloudWatchCompositeAlarmEvent loadCloudWatchCompositeAlarmEvent(String filename) { + return loadEvent(filename, CloudWatchCompositeAlarmEvent.class); + } + public static CloudWatchLogsEvent loadCloudWatchLogsEvent(String filename) { return loadEvent(filename, CloudWatchLogsEvent.class); } + public static CloudWatchMetricAlarmEvent loadCloudWatchMetricAlarmEvent(String filename) { + return loadEvent(filename, CloudWatchMetricAlarmEvent.class); + } + public static CodeCommitEvent loadCodeCommitEvent(String filename) { return loadEvent(filename, CodeCommitEvent.class); } @@ -65,6 +73,10 @@ public static DynamodbEvent loadDynamoDbEvent(String filename) { return loadEvent(filename, DynamodbEvent.class); } + public static DynamodbEvent.DynamodbStreamRecord loadDynamoDbStreamRecord(String filename) { + return loadEvent(filename, DynamodbEvent.DynamodbStreamRecord.class); + } + public static KafkaEvent loadKafkaEvent(String filename) { return loadEvent(filename, KafkaEvent.class); } @@ -85,10 +97,18 @@ public static LexEvent loadLexEvent(String filename) { return loadEvent(filename, LexEvent.class); } + public static MSKFirehoseEvent loadMSKFirehoseEvent(String filename) { + return loadEvent(filename, MSKFirehoseEvent.class); + } + public static S3Event loadS3Event(String filename) { return loadEvent(filename, S3Event.class); } + public static S3BatchEventV2 loadS3BatchEventV2(String filename) { + return loadEvent(filename, S3BatchEventV2.class); + } + public static SecretsManagerRotationEvent loadSecretsManagerRotationEvent(String filename) { return loadEvent(filename, SecretsManagerRotationEvent.class); } @@ -109,6 +129,10 @@ public static RabbitMQEvent loadRabbitMQEvent(String filename) { return loadEvent(filename, RabbitMQEvent.class); } + public static CognitoUserPoolPreTokenGenerationEventV2 loadCognitoUserPoolPreTokenGenerationEventV2(String filename) { + return loadEvent(filename, CognitoUserPoolPreTokenGenerationEventV2.class); + } + public static T loadEvent(String filename, Class targetClass) { if (!filename.endsWith("json")) { 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 8f8fe50a4..752b84e27 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 @@ -1,6 +1,38 @@ /* 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.APIGatewayCustomAuthorizerEvent; +import com.amazonaws.services.lambda.runtime.events.APIGatewayProxyRequestEvent; +import com.amazonaws.services.lambda.runtime.events.APIGatewayV2CustomAuthorizerEvent; +import com.amazonaws.services.lambda.runtime.events.APIGatewayV2HTTPEvent; +import com.amazonaws.services.lambda.runtime.events.ActiveMQEvent; +import com.amazonaws.services.lambda.runtime.events.ApplicationLoadBalancerRequestEvent; +import com.amazonaws.services.lambda.runtime.events.CloudFormationCustomResourceEvent; +import com.amazonaws.services.lambda.runtime.events.CloudFrontEvent; +import com.amazonaws.services.lambda.runtime.events.CloudWatchCompositeAlarmEvent; +import com.amazonaws.services.lambda.runtime.events.CloudWatchCompositeAlarmEvent.AlarmData; +import com.amazonaws.services.lambda.runtime.events.CloudWatchCompositeAlarmEvent.Configuration; +import com.amazonaws.services.lambda.runtime.events.CloudWatchCompositeAlarmEvent.PreviousState; +import com.amazonaws.services.lambda.runtime.events.CloudWatchCompositeAlarmEvent.State; +import com.amazonaws.services.lambda.runtime.events.CloudWatchLogsEvent; +import com.amazonaws.services.lambda.runtime.events.CloudWatchMetricAlarmEvent; +import com.amazonaws.services.lambda.runtime.events.CodeCommitEvent; +import com.amazonaws.services.lambda.runtime.events.CognitoUserPoolPreTokenGenerationEventV2; +import com.amazonaws.services.lambda.runtime.events.ConfigEvent; +import com.amazonaws.services.lambda.runtime.events.ConnectEvent; +import com.amazonaws.services.lambda.runtime.events.DynamodbEvent; +import com.amazonaws.services.lambda.runtime.events.KafkaEvent; +import com.amazonaws.services.lambda.runtime.events.KinesisEvent; +import com.amazonaws.services.lambda.runtime.events.KinesisFirehoseEvent; +import com.amazonaws.services.lambda.runtime.events.LambdaDestinationEvent; +import com.amazonaws.services.lambda.runtime.events.LexEvent; +import com.amazonaws.services.lambda.runtime.events.MSKFirehoseEvent; +import com.amazonaws.services.lambda.runtime.events.RabbitMQEvent; +import com.amazonaws.services.lambda.runtime.events.S3Event; +import com.amazonaws.services.lambda.runtime.events.SNSEvent; +import com.amazonaws.services.lambda.runtime.events.SQSEvent; +import com.amazonaws.services.lambda.runtime.events.ScheduledEvent; +import com.amazonaws.services.lambda.runtime.events.SecretsManagerRotationEvent; import com.amazonaws.services.lambda.runtime.events.models.dynamodb.AttributeValue; import com.amazonaws.services.lambda.runtime.events.models.dynamodb.Record; import com.amazonaws.services.lambda.runtime.events.models.dynamodb.StreamRecord; @@ -14,9 +46,6 @@ import static java.time.Instant.ofEpochSecond; import static org.assertj.core.api.Assertions.*; -import static org.assertj.core.api.Assertions.from; - -import com.amazonaws.services.lambda.runtime.events.*; public class EventLoaderTest { @@ -119,6 +148,19 @@ public void testLoadKinesisFirehoseEvent() { assertThat(event.getRecords().get(0).getData().array()).asString().isEqualTo("Hello, this is a test 123."); } + @Test + public void testLoadMSKFirehoseEvent() { + MSKFirehoseEvent event = EventLoader.loadMSKFirehoseEvent("msk_firehose_event.json"); + + assertThat(event).isNotNull(); + assertThat(event.getSourceMSKArn()).isEqualTo("arn:aws:kafka:EXAMPLE"); + assertThat(event.getDeliveryStreamArn()).isEqualTo("arn:aws:firehose:EXAMPLE"); + assertThat(event.getRecords()).hasSize(1); + 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}"); + } + @Test public void testLoadS3Event() { S3Event event = EventLoader.loadS3Event("s3_event.json"); @@ -160,27 +202,43 @@ public void testLoadSNSEvent() { @Test public void testLoadDynamoEvent() { - DynamodbEvent event = EventLoader.loadDynamoDbEvent("dynamo_event.json"); + DynamodbEvent event = EventLoader.loadDynamoDbEvent("ddb/dynamo_event.json"); assertThat(event).isNotNull(); assertThat(event.getRecords()).hasSize(3); + assertDynamoDbStreamRecord(event.getRecords().get(1)); + } - DynamodbEvent.DynamodbStreamRecord record = event.getRecords().get(0); + @Test + public void testLoadDynamoDbStreamRecord() { + assertDynamoDbStreamRecord(EventLoader.loadDynamoDbStreamRecord("ddb/dynamo_ddb_stream_record.json")); + } + + private static void assertDynamoDbStreamRecord(final DynamodbEvent.DynamodbStreamRecord record) { assertThat(record) + .isNotNull() .returns("arn:aws:dynamodb:eu-central-1:123456789012:table/ExampleTableWithStream/stream/2015-06-27T00:48:05.899", from(DynamodbEvent.DynamodbStreamRecord::getEventSourceARN)) - .returns("INSERT", from(Record::getEventName)); + .returns("MODIFY", from(Record::getEventName)); StreamRecord streamRecord = record.getDynamodb(); assertThat(streamRecord) - .returns("4421584500000000017450439091", StreamRecord::getSequenceNumber) - .returns(26L, StreamRecord::getSizeBytes) + .returns("4421584500000000017450439092", StreamRecord::getSequenceNumber) + .returns(59L, StreamRecord::getSizeBytes) .returns("NEW_AND_OLD_IMAGES", StreamRecord::getStreamViewType) - .returns(Date.from(ofEpochSecond(1428537600)), StreamRecord::getApproximateCreationDateTime); - - assertThat(streamRecord.getKeys()).contains(entry("Id", new AttributeValue().withN("101"))); - assertThat(streamRecord.getNewImage()).containsAnyOf( - entry("Message", new AttributeValue("New item!")), - entry("Id", new AttributeValue().withN("101")) - ); + .returns(Date.from(ofEpochSecond(1635734407).plusNanos(123456789)), StreamRecord::getApproximateCreationDateTime); + + assertThat(streamRecord.getKeys()) + .isNotNull() + .contains(entry("Id", new AttributeValue().withN("101"))); + assertThat(streamRecord.getNewImage()) + .isNotNull() + .containsAnyOf( + entry("Message", new AttributeValue("This item has changed")), + entry("Id", new AttributeValue().withN("101"))); + assertThat(streamRecord.getOldImage()) + .isNotNull() + .containsAnyOf( + entry("Message", new AttributeValue("New item!")), + entry("Id", new AttributeValue().withN("101"))); } @Test @@ -205,6 +263,15 @@ public void testLoadActiveMQEvent() { assertThat(event.getMessages().get(1).getMessageID()).isEqualTo("ID:b-8bcfa572-428a-4642-879d-eb284b418fc8-1.mq.us-west-2.amazonaws.com-37557-1234520418293-4:1:1:1:1"); } + @Test + public void testLoadActiveMQEventWithProperties() { + ActiveMQEvent event = EventLoader.loadActiveMQEvent("mq_event.json"); + assertThat(event).isNotNull(); + assertThat(event.getMessages()).hasSize(2); + assertThat(event.getMessages().get(0).getProperties().get("testKey")).isEqualTo("testValue"); + assertThat(event.getMessages().get(1).getProperties().get("testKey")).isEqualTo("testValue"); + } + @Test public void testLoadCodeCommitEvent() { CodeCommitEvent event = EventLoader.loadCodeCommitEvent("codecommit_event.json"); @@ -266,6 +333,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 @@ -302,7 +377,9 @@ public void testLoadSecretsManagerRotationEvent() { assertThat(event) .returns("123e4567-e89b-12d3-a456-426614174000", from(SecretsManagerRotationEvent::getClientRequestToken)) .returns("arn:aws:secretsmanager:eu-central-1:123456789012:secret:/powertools/secretparam-xBPaJ5", from(SecretsManagerRotationEvent::getSecretId)) - .returns("CreateSecret", from(SecretsManagerRotationEvent::getStep)); + .returns("CreateSecret", from(SecretsManagerRotationEvent::getStep)) + .returns("8a4cc1ac-82ea-47c7-bd9f-aeb370b1b6a6", from(SecretsManagerRotationEvent::getRotationToken)); +; } @Test @@ -338,4 +415,133 @@ public void testLoadRabbitMQEvent() { assertThat(header1.get("bytes")).contains(118, 97, 108, 117, 101, 49); assertThat((Integer) headers.get("numberInHeader")).isEqualTo(10); } + + @Test + public void testLoadCognitoUserPoolPreTokenGenerationEventV2() { + CognitoUserPoolPreTokenGenerationEventV2 event = EventLoader.loadCognitoUserPoolPreTokenGenerationEventV2("cognito_user_pool_pre_token_generation_event_v2.json"); + assertThat(event).isNotNull(); + assertThat(event) + .returns("2", from(CognitoUserPoolPreTokenGenerationEventV2::getVersion)) + .returns("us-east-1", from(CognitoUserPoolPreTokenGenerationEventV2::getRegion)) + .returns("TokenGeneration_Authentication", from(CognitoUserPoolPreTokenGenerationEventV2::getTriggerSource)); + + CognitoUserPoolPreTokenGenerationEventV2.Request request = event.getRequest(); + String[] requestScopes = request.getScopes(); + assertThat("aws.cognito.signin.user.admin").isEqualTo(requestScopes[0]); + + CognitoUserPoolPreTokenGenerationEventV2.Response response = event.getResponse(); + String[] groupsToOverride = response.getClaimsAndScopeOverrideDetails().getGroupOverrideDetails().getGroupsToOverride(); + String[] iamRolesToOverride = response.getClaimsAndScopeOverrideDetails().getGroupOverrideDetails().getIamRolesToOverride(); + String preferredRole = response.getClaimsAndScopeOverrideDetails().getGroupOverrideDetails().getPreferredRole(); + + assertThat("group-99").isEqualTo(groupsToOverride[0]); + assertThat("group-98").isEqualTo(groupsToOverride[1]); + assertThat("arn:aws:iam::123456789012:role/sns_caller99").isEqualTo(iamRolesToOverride[0]); + assertThat("arn:aws:iam::123456789012:role/sns_caller98").isEqualTo(iamRolesToOverride[1]); + assertThat("arn:aws:iam::123456789012:role/sns_caller_99").isEqualTo(preferredRole); + } + + @Test + public void testCloudWatchCompositeAlarmEvent() { + CloudWatchCompositeAlarmEvent event = EventLoader.loadCloudWatchCompositeAlarmEvent("cloudwatch_composite_alarm.json"); + assertThat(event).isNotNull(); + assertThat(event) + .returns("aws.cloudwatch", from(CloudWatchCompositeAlarmEvent::getSource)) + .returns("arn:aws:cloudwatch:us-east-1:111122223333:alarm:SuppressionDemo.Main", from(CloudWatchCompositeAlarmEvent::getAlarmArn)) + .returns("111122223333", from(CloudWatchCompositeAlarmEvent::getAccountId)) + .returns("2023-08-04T12:56:46.138+0000", from(CloudWatchCompositeAlarmEvent::getTime)) + .returns("us-east-1", from(CloudWatchCompositeAlarmEvent::getRegion)); + + AlarmData alarmData = event.getAlarmData(); + assertThat(alarmData).isNotNull(); + assertThat(alarmData) + .returns("CompositeDemo.Main", from(AlarmData::getAlarmName)); + + State state = alarmData.getState(); + assertThat(state).isNotNull(); + assertThat(state) + .returns("ALARM", from(State::getValue)) + .returns("arn:aws:cloudwatch:us-east-1:111122223333:alarm:CompositeDemo.FirstChild transitioned to ALARM at Friday 04 August, 2023 12:54:46 UTC", from(State::getReason)) + .returns("{\"triggeringAlarms\":[{\"arn\":\"arn:aws:cloudwatch:us-east-1:111122223333:alarm:CompositeDemo.FirstChild\",\"state\":{\"value\":\"ALARM\",\"timestamp\":\"2023-08-04T12:54:46.138+0000\"}}]}", from(State::getReasonData)) + .returns("2023-08-04T12:56:46.138+0000", from(State::getTimestamp)); + + PreviousState previousState = alarmData.getPreviousState(); + assertThat(previousState).isNotNull(); + assertThat(previousState) + .returns("ALARM", from(PreviousState::getValue)) + .returns("arn:aws:cloudwatch:us-east-1:111122223333:alarm:CompositeDemo.FirstChild transitioned to ALARM at Friday 04 August, 2023 12:54:46 UTC", from(PreviousState::getReason)) + .returns("{\"triggeringAlarms\":[{\"arn\":\"arn:aws:cloudwatch:us-east-1:111122223333:alarm:CompositeDemo.FirstChild\",\"state\":{\"value\":\"ALARM\",\"timestamp\":\"2023-08-04T12:54:46.138+0000\"}}]}", from(PreviousState::getReasonData)) + .returns("2023-08-04T12:54:46.138+0000", from(PreviousState::getTimestamp)) + .returns("WaitPeriod", from(PreviousState::getActionsSuppressedBy)) + .returns("Actions suppressed by WaitPeriod", from(PreviousState::getActionsSuppressedReason)); + + Configuration configuration = alarmData.getConfiguration(); + assertThat(configuration).isNotNull(); + assertThat(configuration) + .returns("ALARM(CompositeDemo.FirstChild) OR ALARM(CompositeDemo.SecondChild)", from(Configuration::getAlarmRule)) + .returns("CompositeDemo.ActionsSuppressor", from(Configuration::getActionsSuppressor)) + .returns(120, from(Configuration::getActionsSuppressorWaitPeriod)) + .returns(180, from(Configuration::getActionsSuppressorExtensionPeriod)); + } + + @Test + public void testCloudWatchMetricAlarmEvent() { + CloudWatchMetricAlarmEvent event = EventLoader.loadCloudWatchMetricAlarmEvent("cloudwatch_metric_alarm.json"); + assertThat(event).isNotNull(); + assertThat(event) + .returns("aws.cloudwatch", from(CloudWatchMetricAlarmEvent::getSource)) + .returns("arn:aws:cloudwatch:us-east-1:444455556666:alarm:lambda-demo-metric-alarm", from(CloudWatchMetricAlarmEvent::getAlarmArn)) + .returns("444455556666", from(CloudWatchMetricAlarmEvent::getAccountId)) + .returns("2023-08-04T12:36:15.490+0000", from(CloudWatchMetricAlarmEvent::getTime)) + .returns("us-east-1", from(CloudWatchMetricAlarmEvent::getRegion)); + + CloudWatchMetricAlarmEvent.AlarmData alarmData = event.getAlarmData(); + assertThat(alarmData).isNotNull(); + assertThat(alarmData) + .returns("lambda-demo-metric-alarm", from(CloudWatchMetricAlarmEvent.AlarmData::getAlarmName)); + + CloudWatchMetricAlarmEvent.State state = alarmData.getState(); + assertThat(state).isNotNull(); + assertThat(state) + .returns("ALARM", from(CloudWatchMetricAlarmEvent.State::getValue)) + .returns("test", from(CloudWatchMetricAlarmEvent.State::getReason)) + .returns("2023-08-04T12:36:15.490+0000", from(CloudWatchMetricAlarmEvent.State::getTimestamp)); + + CloudWatchMetricAlarmEvent.PreviousState previousState = alarmData.getPreviousState(); + assertThat(previousState).isNotNull(); + assertThat(previousState) + .returns("INSUFFICIENT_DATA", from(CloudWatchMetricAlarmEvent.PreviousState::getValue)) + .returns("Insufficient Data: 5 datapoints were unknown.", from(CloudWatchMetricAlarmEvent.PreviousState::getReason)) + .returns("{\"version\":\"1.0\",\"queryDate\":\"2023-08-04T12:31:29.591+0000\",\"statistic\":\"Average\",\"period\":60,\"recentDatapoints\":[],\"threshold\":5.0,\"evaluatedDatapoints\":[{\"timestamp\":\"2023-08-04T12:30:00.000+0000\"},{\"timestamp\":\"2023-08-04T12:29:00.000+0000\"},{\"timestamp\":\"2023-08-04T12:28:00.000+0000\"},{\"timestamp\":\"2023-08-04T12:27:00.000+0000\"},{\"timestamp\":\"2023-08-04T12:26:00.000+0000\"}]}", from(CloudWatchMetricAlarmEvent.PreviousState::getReasonData)) + .returns("2023-08-04T12:31:29.595+0000", from(CloudWatchMetricAlarmEvent.PreviousState::getTimestamp)); + + CloudWatchMetricAlarmEvent.Configuration configuration = alarmData.getConfiguration(); + assertThat(configuration).isNotNull(); + assertThat(configuration) + .returns("Metric Alarm to test Lambda actions", from(CloudWatchMetricAlarmEvent.Configuration::getDescription)); + + List metrics = configuration.getMetrics(); + assertThat(metrics).hasSize(1); + CloudWatchMetricAlarmEvent.Metric metric = metrics.get(0); + assertThat(metric) + .returns("1234e046-06f0-a3da-9534-EXAMPLEe4c", from(CloudWatchMetricAlarmEvent.Metric::getId)); + + CloudWatchMetricAlarmEvent.MetricStat metricStat = metric.getMetricStat(); + assertThat(metricStat).isNotNull(); + assertThat(metricStat) + .returns(60, from(CloudWatchMetricAlarmEvent.MetricStat::getPeriod)) + .returns("Average", from(CloudWatchMetricAlarmEvent.MetricStat::getStat)) + .returns("Percent", from(CloudWatchMetricAlarmEvent.MetricStat::getUnit)); + + CloudWatchMetricAlarmEvent.MetricDetail metricDetail = metricStat.getMetric(); + assertThat(metricDetail).isNotNull(); + assertThat(metricDetail) + .returns("AWS/Logs", from(CloudWatchMetricAlarmEvent.MetricDetail::getNamespace)) + .returns("CallCount", from(CloudWatchMetricAlarmEvent.MetricDetail::getName)); + + Map dimensions = metricDetail.getDimensions(); + assertThat(dimensions).isNotEmpty().hasSize(1); + assertThat(dimensions) + .contains(entry("InstanceId", "i-12345678")); + } } diff --git a/aws-lambda-java-tests/src/test/java/com/amazonaws/services/lambda/runtime/tests/S3BatchEventV2Test.java b/aws-lambda-java-tests/src/test/java/com/amazonaws/services/lambda/runtime/tests/S3BatchEventV2Test.java new file mode 100644 index 000000000..562af4355 --- /dev/null +++ b/aws-lambda-java-tests/src/test/java/com/amazonaws/services/lambda/runtime/tests/S3BatchEventV2Test.java @@ -0,0 +1,21 @@ +package com.amazonaws.services.lambda.runtime.tests; + +import com.amazonaws.services.lambda.runtime.events.S3BatchEventV2; +import org.junit.jupiter.api.Test; + +import static org.assertj.core.api.Assertions.*; + +public class S3BatchEventV2Test { + + @Test + public void testS3BatchEventV2() { + S3BatchEventV2 event = EventLoader.loadS3BatchEventV2("s3_batch_event_v2.json"); + assertThat(event).isNotNull(); + assertThat(event.getInvocationId()).isEqualTo("Jr3s8KZqYWRmaiBhc2RmdW9hZHNmZGpmaGFzbGtkaGZzatx7Ruy"); + assertThat(event.getJob()).isNotNull(); + assertThat(event.getJob().getUserArguments().get("MyDestinationBucket")).isEqualTo("destination-directory-bucket-name"); + assertThat(event.getTasks()).hasSize(1); + assertThat(event.getTasks().get(0).getS3Key()).isEqualTo("s3objectkey"); + assertThat(event.getTasks().get(0).getS3Bucket()).isEqualTo("source-directory-bucket-name"); + } +} diff --git a/aws-lambda-java-tests/src/test/resources/cloudwatch_composite_alarm.json b/aws-lambda-java-tests/src/test/resources/cloudwatch_composite_alarm.json new file mode 100644 index 000000000..353d470ae --- /dev/null +++ b/aws-lambda-java-tests/src/test/resources/cloudwatch_composite_alarm.json @@ -0,0 +1,30 @@ +{ + "source": "aws.cloudwatch", + "alarmArn": "arn:aws:cloudwatch:us-east-1:111122223333:alarm:SuppressionDemo.Main", + "accountId": "111122223333", + "time": "2023-08-04T12:56:46.138+0000", + "region": "us-east-1", + "alarmData": { + "alarmName": "CompositeDemo.Main", + "state": { + "value": "ALARM", + "reason": "arn:aws:cloudwatch:us-east-1:111122223333:alarm:CompositeDemo.FirstChild transitioned to ALARM at Friday 04 August, 2023 12:54:46 UTC", + "reasonData": "{\"triggeringAlarms\":[{\"arn\":\"arn:aws:cloudwatch:us-east-1:111122223333:alarm:CompositeDemo.FirstChild\",\"state\":{\"value\":\"ALARM\",\"timestamp\":\"2023-08-04T12:54:46.138+0000\"}}]}", + "timestamp": "2023-08-04T12:56:46.138+0000" + }, + "previousState": { + "value": "ALARM", + "reason": "arn:aws:cloudwatch:us-east-1:111122223333:alarm:CompositeDemo.FirstChild transitioned to ALARM at Friday 04 August, 2023 12:54:46 UTC", + "reasonData": "{\"triggeringAlarms\":[{\"arn\":\"arn:aws:cloudwatch:us-east-1:111122223333:alarm:CompositeDemo.FirstChild\",\"state\":{\"value\":\"ALARM\",\"timestamp\":\"2023-08-04T12:54:46.138+0000\"}}]}", + "timestamp": "2023-08-04T12:54:46.138+0000", + "actionsSuppressedBy": "WaitPeriod", + "actionsSuppressedReason": "Actions suppressed by WaitPeriod" + }, + "configuration": { + "alarmRule": "ALARM(CompositeDemo.FirstChild) OR ALARM(CompositeDemo.SecondChild)", + "actionsSuppressor": "CompositeDemo.ActionsSuppressor", + "actionsSuppressorWaitPeriod": 120, + "actionsSuppressorExtensionPeriod": 180 + } + } +} \ No newline at end of file diff --git a/aws-lambda-java-tests/src/test/resources/cloudwatch_metric_alarm.json b/aws-lambda-java-tests/src/test/resources/cloudwatch_metric_alarm.json new file mode 100644 index 000000000..61b4187b5 --- /dev/null +++ b/aws-lambda-java-tests/src/test/resources/cloudwatch_metric_alarm.json @@ -0,0 +1,42 @@ +{ + "source": "aws.cloudwatch", + "alarmArn": "arn:aws:cloudwatch:us-east-1:444455556666:alarm:lambda-demo-metric-alarm", + "accountId": "444455556666", + "time": "2023-08-04T12:36:15.490+0000", + "region": "us-east-1", + "alarmData": { + "alarmName": "lambda-demo-metric-alarm", + "state": { + "value": "ALARM", + "reason": "test", + "timestamp": "2023-08-04T12:36:15.490+0000" + }, + "previousState": { + "value": "INSUFFICIENT_DATA", + "reason": "Insufficient Data: 5 datapoints were unknown.", + "reasonData": "{\"version\":\"1.0\",\"queryDate\":\"2023-08-04T12:31:29.591+0000\",\"statistic\":\"Average\",\"period\":60,\"recentDatapoints\":[],\"threshold\":5.0,\"evaluatedDatapoints\":[{\"timestamp\":\"2023-08-04T12:30:00.000+0000\"},{\"timestamp\":\"2023-08-04T12:29:00.000+0000\"},{\"timestamp\":\"2023-08-04T12:28:00.000+0000\"},{\"timestamp\":\"2023-08-04T12:27:00.000+0000\"},{\"timestamp\":\"2023-08-04T12:26:00.000+0000\"}]}", + "timestamp": "2023-08-04T12:31:29.595+0000" + }, + "configuration": { + "description": "Metric Alarm to test Lambda actions", + "metrics": [ + { + "id": "1234e046-06f0-a3da-9534-EXAMPLEe4c", + "metricStat": { + "metric": { + "namespace": "AWS/Logs", + "name": "CallCount", + "dimensions": { + "InstanceId": "i-12345678" + } + }, + "period": 60, + "stat": "Average", + "unit": "Percent" + }, + "returnData": true + } + ] + } + } +} \ No newline at end of file diff --git a/aws-lambda-java-tests/src/test/resources/cognito_user_pool_pre_token_generation_event_v2.json b/aws-lambda-java-tests/src/test/resources/cognito_user_pool_pre_token_generation_event_v2.json new file mode 100644 index 000000000..eb46b8cb3 --- /dev/null +++ b/aws-lambda-java-tests/src/test/resources/cognito_user_pool_pre_token_generation_event_v2.json @@ -0,0 +1,39 @@ +{ + "version": "2", + "triggerSource": "TokenGeneration_Authentication", + "region": "us-east-1", + "userPoolId": "us-east-1_EXAMPLE", + "userName": "JaneDoe", + "callerContext": { + "awsSdkVersion": "aws-sdk-unknown-unknown", + "clientId": "1example23456789" + }, + "request": { + "userAttributes": { + "sub": "a1b2c3d4-5678-90ab-cdef-EXAMPLE11111", + "cognito:user_status": "CONFIRMED", + "email_verified": "true", + "phone_number_verified": "true", + "phone_number": "+12065551212", + "family_name": "Zoe", + "email": "Jane.Doe@example.com" + }, + "groupConfiguration": { + "groupsToOverride": ["group-1", "group-2", "group-3"], + "iamRolesToOverride": ["arn:aws:iam::123456789012:role/sns_caller1", "arn:aws:iam::123456789012:role/sns_caller2", "arn:aws:iam::123456789012:role/sns_caller3"], + "preferredRole": "arn:aws:iam::123456789012:role/sns_caller" + }, + "scopes": [ + "aws.cognito.signin.user.admin", "openid", "email", "phone" + ] + }, + "response": { + "claimsAndScopeOverrideDetails": { + "groupOverrideDetails": { + "groupsToOverride": ["group-99", "group-98"], + "iamRolesToOverride": ["arn:aws:iam::123456789012:role/sns_caller99", "arn:aws:iam::123456789012:role/sns_caller98"], + "preferredRole": "arn:aws:iam::123456789012:role/sns_caller_99" + } + } + } +} \ 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..b71bf6692 100644 --- a/aws-lambda-java-tests/src/test/resources/connect_event.json +++ b/aws-lambda-java-tests/src/test/resources/connect_event.json @@ -22,7 +22,10 @@ } }, "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_ddb_stream_record.json b/aws-lambda-java-tests/src/test/resources/ddb/dynamo_ddb_stream_record.json new file mode 100644 index 000000000..f5df23ff5 --- /dev/null +++ b/aws-lambda-java-tests/src/test/resources/ddb/dynamo_ddb_stream_record.json @@ -0,0 +1,35 @@ +{ + "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.635734407123456789E9, + "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" +} diff --git a/aws-lambda-java-tests/src/test/resources/dynamo_event.json b/aws-lambda-java-tests/src/test/resources/ddb/dynamo_event.json similarity index 97% rename from aws-lambda-java-tests/src/test/resources/dynamo_event.json rename to aws-lambda-java-tests/src/test/resources/ddb/dynamo_event.json index f28ce0e6e..2e43ba497 100644 --- a/aws-lambda-java-tests/src/test/resources/dynamo_event.json +++ b/aws-lambda-java-tests/src/test/resources/ddb/dynamo_event.json @@ -59,7 +59,7 @@ "N": "101" } }, - "ApproximateCreationDateTime": 1428537600, + "ApproximateCreationDateTime": 1.635734407123456789E9, "SequenceNumber": "4421584500000000017450439092", "SizeBytes": 59, "StreamViewType": "NEW_AND_OLD_IMAGES" 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 a9a798546..6505a22d4 100644 --- a/aws-lambda-java-tests/src/test/resources/mq_event.json +++ b/aws-lambda-java-tests/src/test/resources/mq_event.json @@ -13,7 +13,10 @@ }, "timestamp": 1598827811958, "brokerInTime": 1598827811958, - "brokerOutTime": 1598827811959 + "brokerOutTime": 1598827811959, + "properties": { + "testKey": "testValue" + } }, { "messageID": "ID:b-8bcfa572-428a-4642-879d-eb284b418fc8-1.mq.us-west-2.amazonaws.com-37557-1234520418293-4:1:1:1:1", @@ -26,7 +29,10 @@ }, "timestamp": 1598827811958, "brokerInTime": 1598827811958, - "brokerOutTime": 1598827811959 + "brokerOutTime": 1598827811959, + "properties": { + "testKey": "testValue" + } } ] } \ No newline at end of file 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 new file mode 100644 index 000000000..6b839912d --- /dev/null +++ b/aws-lambda-java-tests/src/test/resources/msk_firehose_event.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==" + } + ] +} diff --git a/aws-lambda-java-tests/src/test/resources/s3_batch_event_v2.json b/aws-lambda-java-tests/src/test/resources/s3_batch_event_v2.json new file mode 100644 index 000000000..4cdbacaaa --- /dev/null +++ b/aws-lambda-java-tests/src/test/resources/s3_batch_event_v2.json @@ -0,0 +1,21 @@ +{ + "invocationSchemaVersion": "2.0", + "invocationId": "Jr3s8KZqYWRmaiBhc2RmdW9hZHNmZGpmaGFzbGtkaGZzatx7Ruy", + "job": { + "id": "ry77cd60-61f6-4a2b-8a21-d07600c874gf", + "userArguments": { + "MyDestinationBucket": "destination-directory-bucket-name", + "MyDestinationBucketRegion": "us-east-1", + "MyDestinationPrefix": "copied/", + "MyDestinationObjectKeySuffix": "_new_suffix" + } + }, + "tasks": [ + { + "taskId": "y5R3a2lkZ29lc2hlurcS", + "s3Key": "s3objectkey", + "s3VersionId": null, + "s3Bucket": "source-directory-bucket-name" + } + ] +} diff --git a/aws-lambda-java-tests/src/test/resources/secrets_rotation_event.json b/aws-lambda-java-tests/src/test/resources/secrets_rotation_event.json index 38440fac9..e8d80b573 100644 --- a/aws-lambda-java-tests/src/test/resources/secrets_rotation_event.json +++ b/aws-lambda-java-tests/src/test/resources/secrets_rotation_event.json @@ -1,5 +1,6 @@ { "Step" : "CreateSecret", "SecretId" : "arn:aws:secretsmanager:eu-central-1:123456789012:secret:/powertools/secretparam-xBPaJ5", - "ClientRequestToken" : "123e4567-e89b-12d3-a456-426614174000" + "ClientRequestToken" : "123e4567-e89b-12d3-a456-426614174000", + "RotationToken": "8a4cc1ac-82ea-47c7-bd9f-aeb370b1b6a6" } \ No newline at end of file diff --git a/experimental/aws-lambda-java-profiler/.gitignore b/experimental/aws-lambda-java-profiler/.gitignore new file mode 100644 index 000000000..4c3fb86d5 --- /dev/null +++ b/experimental/aws-lambda-java-profiler/.gitignore @@ -0,0 +1,3 @@ +*.zip +/.idea/ +/target/ diff --git a/experimental/aws-lambda-java-profiler/.mvn/wrapper/maven-wrapper.properties b/experimental/aws-lambda-java-profiler/.mvn/wrapper/maven-wrapper.properties new file mode 100644 index 000000000..48a56c99a --- /dev/null +++ b/experimental/aws-lambda-java-profiler/.mvn/wrapper/maven-wrapper.properties @@ -0,0 +1,19 @@ +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, +# software distributed under the License is distributed on an +# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +# KIND, either express or implied. See the License for the +# specific language governing permissions and limitations +# under the License. +wrapperVersion=3.3.2 +distributionType=only-script +distributionUrl=https://repo.maven.apache.org/maven2/org/apache/maven/apache-maven/3.9.6/apache-maven-3.9.6-bin.zip diff --git a/experimental/aws-lambda-java-profiler/README.md b/experimental/aws-lambda-java-profiler/README.md new file mode 100644 index 000000000..c15c22791 --- /dev/null +++ b/experimental/aws-lambda-java-profiler/README.md @@ -0,0 +1,133 @@ +

+ AWS Lambda service icon +

+ +

AWS Lambda Profiler Extension for Java

+ +The Lambda profiler extension allows you to profile your Java functions invoke by invoke, with high fidelity, and no +code changes. It uses the [async-profiler](https://github.com/async-profiler/async-profiler) project to produce +profiling data and automatically uploads the data as HTML flame graphs to S3. + +

+ A flame graph of a Java Lambda function +

+ +## Current status +**This is an alpha release and not yet ready for production use.** We're especially interested in early feedback on usability, features, performance, and compatibility. Please send feedback by opening a [GitHub issue](https://github.com/aws/aws-lambda-java-libs/issues/new). + +The profiler has been tested with Lambda managed runtimes for Java 17 and Java 21. + +## How to use the Lambda Profiler + +To use the profiler you need to + +1. Build the extension in this repo +2. Deploy it as a Lambda Layer and attach the layer to your function +3. Create an S3 bucket for the results, or reuse an existing one +4. Give your function permission to write to the bucket +5. Configure the required environment variables. + +The above assumes you're using the ZIP deployment method with managed runtimes. If you deploy your functions as container images instead, you will need to include the profiler in your Dockerfile at `/opt/extensions/` rather than using a Lambda layer. + +### Quick Start + +The following [Quick Start](#quick-start) gives AWS CLI commands you can run to get started (MacOS/Linux). There are also [examples](examples) using infrastructure as code for you to refer to. + +1. Clone the repo + + ```bash + git clone https://github.com/aws/aws-lambda-java-libs + ``` + +2. Build the extension + + ```bash + cd aws-lambda-java-libs/experimental/aws-lambda-java-profiler/extension + ./build_layer.sh + ``` + +3. Run the `update-function.sh` script which will create a new S3 bucket, Lambda layer and all the configuration required. + + ```bash + cd .. + ./update-function.sh YOUR_FUNCTION_NAME + ``` + +4. Invoke your function and review the flame graph in S3 using your browser. + +### Configuration + +#### Required Environment Variables + +| Name | Value | +|-----------------------------------------|-----------------------------------------------------------------------------------------------| +| AWS_LAMBDA_PROFILER_RESULTS_BUCKET_NAME | Your unique bucket name | +| JAVA_TOOL_OPTIONS | -XX:+UnlockDiagnosticVMOptions -XX:+DebugNonSafepoints -javaagent:/opt/profiler-extension.jar | + +#### Optional Environment Variables + +| Name | Default Value | Options | +|------------------------------------------|-----------------------------------------------------------|--------------------------------| +| AWS_LAMBDA_PROFILER_START_COMMAND | start,event=wall,interval=1us | | +| AWS_LAMBDA_PROFILER_STOP_COMMAND | stop,file=%s,include=*AWSLambda.main,include=start_thread | file=%s is required | +| AWS_LAMBDA_PROFILER_DEBUG | false | true - to enable debug logging | +| AWS_LAMBDA_PROFILER_COMMUNICATION_PORT | 1234 | a valid port number | + +### How does it work? + +In `/src` is the code for a Java agent. It's entry point `AgentEntry.premain()` is executed as the runtime starts up. +The environment variable `JAVA_TOOL_OPTIONS` is used to specify which `.jar` file the agent is in. The `MANIFEST.MF` file is used to specify the pre-main class. + +When the agent is constructed, it starts the profiler and registers itself as a Lambda extension for `INVOKE` request. + +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. +- Verify that the environment variables are set correctly in your Lambda function configuration. +- Check CloudWatch logs for any error messages from the extension. +- The profiler extension uses dependencies such as `com.amazonaws:aws-lambda-java-core`, `com.amazonaws:aws-lambda-java-events` and `software.amazon.awssdk:s3`. If you're using the same dependencies in your Lambda function, make sure that the versions match those used by the extension as mismatched versions can lead to compatibility issues. + +## Contributing + +Contributions to improve the Java profiler extension are welcome. Please see [CONTRIBUTING.md](../../CONTRIBUTING.md) for more information on how to report bugs or submit pull requests. + +Issues or contributions to the [async-profiler](https://github.com/async-profiler/async-profiler) itself should be submitted to that project. + +### Security + +If you discover a potential security issue in this project we ask that you notify AWS Security via our [vulnerability reporting page](http://aws.amazon.com/security/vulnerability-reporting/). Please do **not** create a public GitHub issue. + +### Code of conduct + +This project has adopted the [Amazon Open Source Code of Conduct](https://aws.github.io/code-of-conduct). See [CODE_OF_CONDUCT.md](doc/CODE_OF_CONDUCT.md) for more details. + +## License + +This project is licensed under the [Apache 2.0](../../LICENSE) License. It uses the following projects: + +- [async-profiler](https://github.com/async-profiler/async-profiler) (Apache 2.0 license) +- [AWS SDK for Java 2.0](https://github.com/aws/aws-sdk-java-v2) (Apache 2.0 license) +- Other libraries in this repository (Apache 2.0 license) + diff --git a/experimental/aws-lambda-java-profiler/RELEASE.CHANGELOG.md b/experimental/aws-lambda-java-profiler/RELEASE.CHANGELOG.md new file mode 100644 index 000000000..f2f14ae48 --- /dev/null +++ b/experimental/aws-lambda-java-profiler/RELEASE.CHANGELOG.md @@ -0,0 +1,7 @@ +### March 31, 2025 +`0.1.1` [link to tag](https://github.com/aws/aws-lambda-java-libs/releases/tag/profiler-extension-0.1.1) +- fix: use PROFILER_STOP_COMMAND in Shutdown hooks ([#537](https://github.com/aws/aws-lambda-java-libs/pull/537)) + +### March 18, 2025 +`0.1.0` [link to tag](https://github.com/aws/aws-lambda-java-libs/releases/tag/profiler-extension-0.1.0) +- Initial release \ No newline at end of file diff --git a/experimental/aws-lambda-java-profiler/docs/Arch_AWS-Lambda_64.svg b/experimental/aws-lambda-java-profiler/docs/Arch_AWS-Lambda_64.svg new file mode 100644 index 000000000..496ef0e72 --- /dev/null +++ b/experimental/aws-lambda-java-profiler/docs/Arch_AWS-Lambda_64.svg @@ -0,0 +1,18 @@ + + + + Icon-Architecture/64/Arch_AWS-Lambda_64 + Created with Sketch. + + + + + + + + + + + + + \ No newline at end of file diff --git a/experimental/aws-lambda-java-profiler/docs/example-cold-start-flame-graph-small.png b/experimental/aws-lambda-java-profiler/docs/example-cold-start-flame-graph-small.png new file mode 100644 index 000000000..81ae8cba3 Binary files /dev/null and b/experimental/aws-lambda-java-profiler/docs/example-cold-start-flame-graph-small.png differ diff --git a/experimental/aws-lambda-java-profiler/docs/example-cold-start-flame-graph.png b/experimental/aws-lambda-java-profiler/docs/example-cold-start-flame-graph.png new file mode 100644 index 000000000..26d11c310 Binary files /dev/null and b/experimental/aws-lambda-java-profiler/docs/example-cold-start-flame-graph.png differ diff --git a/experimental/aws-lambda-java-profiler/examples/cdk/.gitignore b/experimental/aws-lambda-java-profiler/examples/cdk/.gitignore new file mode 100644 index 000000000..1db21f162 --- /dev/null +++ b/experimental/aws-lambda-java-profiler/examples/cdk/.gitignore @@ -0,0 +1,13 @@ +.classpath.txt +target +.classpath +.project +.idea +.settings +.vscode +*.iml + +# CDK asset staging directory +.cdk.staging +cdk.out + diff --git a/experimental/aws-lambda-java-profiler/examples/cdk/README.md b/experimental/aws-lambda-java-profiler/examples/cdk/README.md new file mode 100644 index 000000000..516ef71a2 --- /dev/null +++ b/experimental/aws-lambda-java-profiler/examples/cdk/README.md @@ -0,0 +1,18 @@ +# Welcome to your CDK Java project! + +This is a blank project for CDK development with Java. + +The `cdk.json` file tells the CDK Toolkit how to execute your app. + +It is a [Maven](https://maven.apache.org/) based project, so you can open this project with any Maven compatible Java IDE to build and run tests. + +## Useful commands + + * `mvn package` compile and run tests + * `cdk ls` list all stacks in the app + * `cdk synth` emits the synthesized CloudFormation template + * `cdk deploy` deploy this stack to your default AWS account/region + * `cdk diff` compare deployed stack with current state + * `cdk docs` open CDK documentation + +Enjoy! diff --git a/experimental/aws-lambda-java-profiler/examples/cdk/cdk.json b/experimental/aws-lambda-java-profiler/examples/cdk/cdk.json new file mode 100644 index 000000000..e94ff8512 --- /dev/null +++ b/experimental/aws-lambda-java-profiler/examples/cdk/cdk.json @@ -0,0 +1,68 @@ +{ + "app": "mvn -e -q compile exec:java", + "watch": { + "include": [ + "**" + ], + "exclude": [ + "README.md", + "cdk*.json", + "target", + "pom.xml", + "src/test" + ] + }, + "context": { + "@aws-cdk/aws-lambda:recognizeLayerVersion": true, + "@aws-cdk/core:checkSecretUsage": true, + "@aws-cdk/core:target-partitions": [ + "aws", + "aws-cn" + ], + "@aws-cdk-containers/ecs-service-extensions:enableDefaultLogDriver": true, + "@aws-cdk/aws-ec2:uniqueImdsv2TemplateName": true, + "@aws-cdk/aws-ecs:arnFormatIncludesClusterName": true, + "@aws-cdk/aws-iam:minimizePolicies": true, + "@aws-cdk/core:validateSnapshotRemovalPolicy": true, + "@aws-cdk/aws-codepipeline:crossAccountKeyAliasStackSafeResourceName": true, + "@aws-cdk/aws-s3:createDefaultLoggingPolicy": true, + "@aws-cdk/aws-sns-subscriptions:restrictSqsDescryption": true, + "@aws-cdk/aws-apigateway:disableCloudWatchRole": true, + "@aws-cdk/core:enablePartitionLiterals": true, + "@aws-cdk/aws-events:eventsTargetQueueSameAccount": true, + "@aws-cdk/aws-ecs:disableExplicitDeploymentControllerForCircuitBreaker": true, + "@aws-cdk/aws-iam:importedRoleStackSafeDefaultPolicyName": true, + "@aws-cdk/aws-s3:serverAccessLogsUseBucketPolicy": true, + "@aws-cdk/aws-route53-patters:useCertificate": true, + "@aws-cdk/customresources:installLatestAwsSdkDefault": false, + "@aws-cdk/aws-rds:databaseProxyUniqueResourceName": true, + "@aws-cdk/aws-codedeploy:removeAlarmsFromDeploymentGroup": true, + "@aws-cdk/aws-apigateway:authorizerChangeDeploymentLogicalId": true, + "@aws-cdk/aws-ec2:launchTemplateDefaultUserData": true, + "@aws-cdk/aws-secretsmanager:useAttachedSecretResourcePolicyForSecretTargetAttachments": true, + "@aws-cdk/aws-redshift:columnId": true, + "@aws-cdk/aws-stepfunctions-tasks:enableEmrServicePolicyV2": true, + "@aws-cdk/aws-ec2:restrictDefaultSecurityGroup": true, + "@aws-cdk/aws-apigateway:requestValidatorUniqueId": true, + "@aws-cdk/aws-kms:aliasNameRef": true, + "@aws-cdk/aws-autoscaling:generateLaunchTemplateInsteadOfLaunchConfig": true, + "@aws-cdk/core:includePrefixInUniqueNameGeneration": true, + "@aws-cdk/aws-efs:denyAnonymousAccess": true, + "@aws-cdk/aws-opensearchservice:enableOpensearchMultiAzWithStandby": true, + "@aws-cdk/aws-lambda-nodejs:useLatestRuntimeVersion": true, + "@aws-cdk/aws-efs:mountTargetOrderInsensitiveLogicalId": true, + "@aws-cdk/aws-rds:auroraClusterChangeScopeOfInstanceParameterGroupWithEachParameters": true, + "@aws-cdk/aws-appsync:useArnForSourceApiAssociationIdentifier": true, + "@aws-cdk/aws-rds:preventRenderingDeprecatedCredentials": true, + "@aws-cdk/aws-codepipeline-actions:useNewDefaultBranchForCodeCommitSource": true, + "@aws-cdk/aws-cloudwatch-actions:changeLambdaPermissionLogicalIdForLambdaAction": true, + "@aws-cdk/aws-codepipeline:crossAccountKeysDefaultValueToFalse": true, + "@aws-cdk/aws-codepipeline:defaultPipelineTypeToV2": true, + "@aws-cdk/aws-kms:reduceCrossAccountRegionPolicyScope": true, + "@aws-cdk/aws-eks:nodegroupNameAttribute": true, + "@aws-cdk/aws-ec2:ebsDefaultGp3Volume": true, + "@aws-cdk/aws-ecs:removeDefaultDeploymentAlarm": true, + "@aws-cdk/custom-resources:logApiResponseDataPropertyTrueDefault": false, + "@aws-cdk/aws-s3:keepNotificationInImportedBucket": false + } +} diff --git a/experimental/aws-lambda-java-profiler/examples/cdk/pom.xml b/experimental/aws-lambda-java-profiler/examples/cdk/pom.xml new file mode 100644 index 000000000..01bbf0d67 --- /dev/null +++ b/experimental/aws-lambda-java-profiler/examples/cdk/pom.xml @@ -0,0 +1,59 @@ + + + 4.0.0 + + com.myorg + example-cdk-profiler-layer + 0.1 + + + UTF-8 + 2.155.0 + [10.0.0,11.0.0) + 5.7.1 + + + + + + org.apache.maven.plugins + maven-compiler-plugin + 3.11.0 + + 17 + + + + + org.codehaus.mojo + exec-maven-plugin + 3.1.0 + + com.myorg.InfraApp + + + + + + + + software.amazon.awscdk + aws-cdk-lib + ${cdk.version} + + + + software.constructs + constructs + ${constructs.version} + + + + org.junit.jupiter + junit-jupiter + ${junit.version} + test + + + diff --git a/experimental/aws-lambda-java-profiler/examples/cdk/src/main/java/com/myorg/InfraApp.java b/experimental/aws-lambda-java-profiler/examples/cdk/src/main/java/com/myorg/InfraApp.java new file mode 100644 index 000000000..1232c1b8b --- /dev/null +++ b/experimental/aws-lambda-java-profiler/examples/cdk/src/main/java/com/myorg/InfraApp.java @@ -0,0 +1,42 @@ +package com.myorg; + +import software.amazon.awscdk.App; +import software.amazon.awscdk.Environment; +import software.amazon.awscdk.StackProps; + +import java.util.Arrays; + +public class InfraApp { + public static void main(final String[] args) { + App app = new App(); + + new InfraStack(app, "InfraStack", StackProps.builder() + // If you don't specify 'env', this stack will be environment-agnostic. + // Account/Region-dependent features and context lookups will not work, + // but a single synthesized template can be deployed anywhere. + + // Uncomment the next block to specialize this stack for the AWS Account + // and Region that are implied by the current CLI configuration. + /* + .env(Environment.builder() + .account(System.getenv("CDK_DEFAULT_ACCOUNT")) + .region(System.getenv("CDK_DEFAULT_REGION")) + .build()) + */ + + // Uncomment the next block if you know exactly what Account and Region you + // want to deploy the stack to. + /* + .env(Environment.builder() + .account("123456789012") + .region("us-east-1") + .build()) + */ + + // For more information, see https://docs.aws.amazon.com/cdk/latest/guide/environments.html + .build()); + + app.synth(); + } +} + diff --git a/experimental/aws-lambda-java-profiler/examples/cdk/src/main/java/com/myorg/InfraStack.java b/experimental/aws-lambda-java-profiler/examples/cdk/src/main/java/com/myorg/InfraStack.java new file mode 100644 index 000000000..79773e39e --- /dev/null +++ b/experimental/aws-lambda-java-profiler/examples/cdk/src/main/java/com/myorg/InfraStack.java @@ -0,0 +1,53 @@ +package com.myorg; + +import software.amazon.awscdk.Duration; +import software.amazon.awscdk.services.lambda.Code; +import software.amazon.awscdk.services.lambda.Function; +import software.amazon.awscdk.services.lambda.LayerVersion; +import software.amazon.awscdk.services.s3.Bucket; +import software.constructs.Construct; +import software.amazon.awscdk.Stack; +import software.amazon.awscdk.StackProps; + +import java.util.List; +import java.util.Map; +import java.util.UUID; + +import static software.amazon.awscdk.services.lambda.Architecture.*; +import static software.amazon.awscdk.services.lambda.Runtime.*; + +public class InfraStack extends Stack { + public InfraStack(final Construct scope, final String id) { + this(scope, id, null); + } + + public InfraStack(final Construct scope, final String id, final StackProps props) { + super(scope, id, props); + + var resultsBucketName = UUID.randomUUID().toString(); + var resultsBucket = Bucket.Builder.create(this, "profiler-results-bucket") + .bucketName(resultsBucketName) + .build(); + + var layerVersion = LayerVersion.Builder.create(this, "async-profiler-layer") + .compatibleArchitectures(List.of(ARM_64, X86_64)) + .compatibleRuntimes(List.of(JAVA_11, JAVA_17, JAVA_21)) + .code(Code.fromAsset("../../target/extension.zip")) + .build(); + + var environmentVariables = Map.of("JAVA_TOOL_OPTIONS", "-XX:+UnlockDiagnosticVMOptions -XX:+DebugNonSafepoints -javaagent:/opt/profiler.jar", + "AWS_LAMBDA_PROFILER_RESULTS_BUCKET_NAME", resultsBucketName); + + var function = Function.Builder.create(this, "example-profiler-function") + .runtime(JAVA_21) + .handler("helloworld.App") + .code(Code.fromAsset("../function/profiling-example/target/Helloworld-1.0.jar")) + .memorySize(2048) + .layers(List.of(layerVersion)) + .environment(environmentVariables) + .timeout(Duration.seconds(30)) + .build(); + + resultsBucket.grantPut(function); + } +} 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 new file mode 100644 index 000000000..ac1001009 --- /dev/null +++ b/experimental/aws-lambda-java-profiler/examples/function/profiling-example/pom.xml @@ -0,0 +1,63 @@ + + 4.0.0 + helloworld + HelloWorld + 1.0 + jar + A sample Hello World created for SAM CLI. + + 21 + 21 + + + + + com.amazonaws + aws-lambda-java-core + 1.2.2 + + + com.amazonaws + aws-lambda-java-events + 3.11.0 + + + com.hkupty.penna + penna-core + 0.8.0 + + + org.slf4j + slf4j-api + 2.0.13 + + + + junit + junit + 4.13.2 + test + + + + + + + org.apache.maven.plugins + maven-shade-plugin + 3.6.1 + + + + + package + + shade + + + + + + + diff --git a/experimental/aws-lambda-java-profiler/examples/function/profiling-example/src/main/java/helloworld/App.java b/experimental/aws-lambda-java-profiler/examples/function/profiling-example/src/main/java/helloworld/App.java new file mode 100644 index 000000000..c58f55a1f --- /dev/null +++ b/experimental/aws-lambda-java-profiler/examples/function/profiling-example/src/main/java/helloworld/App.java @@ -0,0 +1,53 @@ +package helloworld; + +import java.io.BufferedReader; +import java.io.IOException; +import java.io.InputStreamReader; +import java.net.URL; +import java.util.HashMap; +import java.util.Map; +import java.util.stream.Collectors; + +import com.amazonaws.services.lambda.runtime.Context; +import com.amazonaws.services.lambda.runtime.RequestHandler; +import com.amazonaws.services.lambda.runtime.events.APIGatewayProxyRequestEvent; +import com.amazonaws.services.lambda.runtime.events.APIGatewayProxyResponseEvent; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * Handler for requests to Lambda function. + */ +public class App implements RequestHandler { + + private static Logger logger = LoggerFactory.getLogger(App.class); + + public APIGatewayProxyResponseEvent handleRequest(final APIGatewayProxyRequestEvent input, final Context context) { + Map headers = new HashMap<>(); + headers.put("Content-Type", "application/json"); + headers.put("X-Custom-Header", "application/json"); + + APIGatewayProxyResponseEvent response = new APIGatewayProxyResponseEvent() + .withHeaders(headers); + try { + final String pageContents = this.getPageContents("https://checkip.amazonaws.com"); + String output = String.format("{ \"message\": \"hello world\", \"location\": \"%s\" }", pageContents); + logger.info(output); + + return response + .withStatusCode(200) + .withBody(output); + } catch (IOException e) { + return response + .withBody("{}") + .withStatusCode(500); + } + } + + private String getPageContents(String address) throws IOException{ + URL url = new URL(address); + try(BufferedReader br = new BufferedReader(new InputStreamReader(url.openStream()))) { + return br.lines().collect(Collectors.joining(System.lineSeparator())); + } + } +} diff --git a/experimental/aws-lambda-java-profiler/examples/function/profiling-example/src/test/java/helloworld/AppTest.java b/experimental/aws-lambda-java-profiler/examples/function/profiling-example/src/test/java/helloworld/AppTest.java new file mode 100644 index 000000000..240323bb7 --- /dev/null +++ b/experimental/aws-lambda-java-profiler/examples/function/profiling-example/src/test/java/helloworld/AppTest.java @@ -0,0 +1,22 @@ +package helloworld; + +import com.amazonaws.services.lambda.runtime.events.APIGatewayProxyResponseEvent; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertTrue; +import org.junit.Test; + +public class AppTest { + @Test + public void successfulResponse() { + App app = new App(); + APIGatewayProxyResponseEvent result = app.handleRequest(null, null); + assertEquals(200, result.getStatusCode().intValue()); + assertEquals("application/json", result.getHeaders().get("Content-Type")); + String content = result.getBody(); + assertNotNull(content); + assertTrue(content.contains("\"message\"")); + assertTrue(content.contains("\"hello world\"")); + assertTrue(content.contains("\"location\"")); + } +} diff --git a/experimental/aws-lambda-java-profiler/extension/build.gradle b/experimental/aws-lambda-java-profiler/extension/build.gradle new file mode 100644 index 000000000..387bb3528 --- /dev/null +++ b/experimental/aws-lambda-java-profiler/extension/build.gradle @@ -0,0 +1,34 @@ +plugins { + id 'java' + id "com.gradleup.shadow" version "8.3.3" +} + +repositories { + mavenCentral() +} + +sourceCompatibility = 11 +targetCompatibility = 11 + +dependencies { + implementation 'com.amazonaws:aws-lambda-java-core:1.2.3' + implementation 'com.amazonaws:aws-lambda-java-events:3.11.5' + implementation("tools.profiler:async-profiler:3.0") + implementation("software.amazon.awssdk:s3:2.31.2") { + exclude group: 'software.amazon.awssdk', module: 'netty-nio-client' + } +} + +jar { + manifest { + attributes 'Main-Class': 'com.amazonaws.services.lambda.extension.ExtensionMain' + attributes 'Premain-Class': 'com.amazonaws.services.lambda.extension.PreMain' + attributes 'Can-Redefine-Class': true + } +} + +shadowJar { + archiveFileName = "profiler-extension.jar" +} + +build.dependsOn jar diff --git a/experimental/aws-lambda-java-profiler/extension/build_layer.sh b/experimental/aws-lambda-java-profiler/extension/build_layer.sh new file mode 100755 index 000000000..cfb381cff --- /dev/null +++ b/experimental/aws-lambda-java-profiler/extension/build_layer.sh @@ -0,0 +1,13 @@ +# Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +# SPDX-License-Identifier: MIT-0 + +./gradlew :shadowJar + +chmod +x extensions/profiler-extension +archive="extension.zip" +if [ -f "$archive" ] ; then + rm "$archive" +fi + +zip "$archive" -j build/libs/profiler-extension.jar +zip "$archive" extensions/* \ No newline at end of file diff --git a/experimental/aws-lambda-java-profiler/extension/extensions/profiler-extension b/experimental/aws-lambda-java-profiler/extension/extensions/profiler-extension new file mode 100755 index 000000000..ef9a5e47c --- /dev/null +++ b/experimental/aws-lambda-java-profiler/extension/extensions/profiler-extension @@ -0,0 +1,6 @@ +#!/bin/bash +# Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +# SPDX-License-Identifier: MIT-0 + +set -euo pipefail +exec -- java -jar /opt/profiler-extension.jar \ No newline at end of file diff --git a/experimental/aws-lambda-java-profiler/extension/gradle/wrapper/gradle-wrapper.jar b/experimental/aws-lambda-java-profiler/extension/gradle/wrapper/gradle-wrapper.jar new file mode 100644 index 000000000..a4b76b953 Binary files /dev/null and b/experimental/aws-lambda-java-profiler/extension/gradle/wrapper/gradle-wrapper.jar differ diff --git a/experimental/aws-lambda-java-profiler/extension/gradle/wrapper/gradle-wrapper.properties b/experimental/aws-lambda-java-profiler/extension/gradle/wrapper/gradle-wrapper.properties new file mode 100644 index 000000000..df97d72b8 --- /dev/null +++ b/experimental/aws-lambda-java-profiler/extension/gradle/wrapper/gradle-wrapper.properties @@ -0,0 +1,7 @@ +distributionBase=GRADLE_USER_HOME +distributionPath=wrapper/dists +distributionUrl=https\://services.gradle.org/distributions/gradle-8.10.2-bin.zip +networkTimeout=10000 +validateDistributionUrl=true +zipStoreBase=GRADLE_USER_HOME +zipStorePath=wrapper/dists diff --git a/experimental/aws-lambda-java-profiler/extension/gradlew b/experimental/aws-lambda-java-profiler/extension/gradlew new file mode 100755 index 000000000..f5feea6d6 --- /dev/null +++ b/experimental/aws-lambda-java-profiler/extension/gradlew @@ -0,0 +1,252 @@ +#!/bin/sh + +# +# Copyright © 2015-2021 the original authors. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# https://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# +# SPDX-License-Identifier: Apache-2.0 +# + +############################################################################## +# +# Gradle start up script for POSIX generated by Gradle. +# +# Important for running: +# +# (1) You need a POSIX-compliant shell to run this script. If your /bin/sh is +# noncompliant, but you have some other compliant shell such as ksh or +# bash, then to run this script, type that shell name before the whole +# command line, like: +# +# ksh Gradle +# +# Busybox and similar reduced shells will NOT work, because this script +# requires all of these POSIX shell features: +# * functions; +# * expansions «$var», «${var}», «${var:-default}», «${var+SET}», +# «${var#prefix}», «${var%suffix}», and «$( cmd )»; +# * compound commands having a testable exit status, especially «case»; +# * various built-in commands including «command», «set», and «ulimit». +# +# Important for patching: +# +# (2) This script targets any POSIX shell, so it avoids extensions provided +# by Bash, Ksh, etc; in particular arrays are avoided. +# +# The "traditional" practice of packing multiple parameters into a +# space-separated string is a well documented source of bugs and security +# problems, so this is (mostly) avoided, by progressively accumulating +# options in "$@", and eventually passing that to Java. +# +# Where the inherited environment variables (DEFAULT_JVM_OPTS, JAVA_OPTS, +# and GRADLE_OPTS) rely on word-splitting, this is performed explicitly; +# see the in-line comments for details. +# +# There are tweaks for specific operating systems such as AIX, CygWin, +# Darwin, MinGW, and NonStop. +# +# (3) This script is generated from the Groovy template +# https://github.com/gradle/gradle/blob/HEAD/platforms/jvm/plugins-application/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt +# within the Gradle project. +# +# You can find Gradle at https://github.com/gradle/gradle/. +# +############################################################################## + +# Attempt to set APP_HOME + +# Resolve links: $0 may be a link +app_path=$0 + +# Need this for daisy-chained symlinks. +while + APP_HOME=${app_path%"${app_path##*/}"} # leaves a trailing /; empty if no leading path + [ -h "$app_path" ] +do + ls=$( ls -ld "$app_path" ) + link=${ls#*' -> '} + case $link in #( + /*) app_path=$link ;; #( + *) app_path=$APP_HOME$link ;; + esac +done + +# This is normally unused +# shellcheck disable=SC2034 +APP_BASE_NAME=${0##*/} +# Discard cd standard output in case $CDPATH is set (https://github.com/gradle/gradle/issues/25036) +APP_HOME=$( cd -P "${APP_HOME:-./}" > /dev/null && printf '%s +' "$PWD" ) || exit + +# Use the maximum available, or set MAX_FD != -1 to use that value. +MAX_FD=maximum + +warn () { + echo "$*" +} >&2 + +die () { + echo + echo "$*" + echo + exit 1 +} >&2 + +# OS specific support (must be 'true' or 'false'). +cygwin=false +msys=false +darwin=false +nonstop=false +case "$( uname )" in #( + CYGWIN* ) cygwin=true ;; #( + Darwin* ) darwin=true ;; #( + MSYS* | MINGW* ) msys=true ;; #( + NONSTOP* ) nonstop=true ;; +esac + +CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar + + +# Determine the Java command to use to start the JVM. +if [ -n "$JAVA_HOME" ] ; then + if [ -x "$JAVA_HOME/jre/sh/java" ] ; then + # IBM's JDK on AIX uses strange locations for the executables + JAVACMD=$JAVA_HOME/jre/sh/java + else + JAVACMD=$JAVA_HOME/bin/java + fi + if [ ! -x "$JAVACMD" ] ; then + die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." + fi +else + JAVACMD=java + if ! command -v java >/dev/null 2>&1 + then + die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." + fi +fi + +# Increase the maximum file descriptors if we can. +if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then + case $MAX_FD in #( + max*) + # In POSIX sh, ulimit -H is undefined. That's why the result is checked to see if it worked. + # shellcheck disable=SC2039,SC3045 + MAX_FD=$( ulimit -H -n ) || + warn "Could not query maximum file descriptor limit" + esac + case $MAX_FD in #( + '' | soft) :;; #( + *) + # In POSIX sh, ulimit -n is undefined. That's why the result is checked to see if it worked. + # shellcheck disable=SC2039,SC3045 + ulimit -n "$MAX_FD" || + warn "Could not set maximum file descriptor limit to $MAX_FD" + esac +fi + +# Collect all arguments for the java command, stacking in reverse order: +# * args from the command line +# * the main class name +# * -classpath +# * -D...appname settings +# * --module-path (only if needed) +# * DEFAULT_JVM_OPTS, JAVA_OPTS, and GRADLE_OPTS environment variables. + +# For Cygwin or MSYS, switch paths to Windows format before running java +if "$cygwin" || "$msys" ; then + APP_HOME=$( cygpath --path --mixed "$APP_HOME" ) + CLASSPATH=$( cygpath --path --mixed "$CLASSPATH" ) + + JAVACMD=$( cygpath --unix "$JAVACMD" ) + + # Now convert the arguments - kludge to limit ourselves to /bin/sh + for arg do + if + case $arg in #( + -*) false ;; # don't mess with options #( + /?*) t=${arg#/} t=/${t%%/*} # looks like a POSIX filepath + [ -e "$t" ] ;; #( + *) false ;; + esac + then + arg=$( cygpath --path --ignore --mixed "$arg" ) + fi + # Roll the args list around exactly as many times as the number of + # args, so each arg winds up back in the position where it started, but + # possibly modified. + # + # NB: a `for` loop captures its iteration list before it begins, so + # changing the positional parameters here affects neither the number of + # iterations, nor the values presented in `arg`. + shift # remove old arg + set -- "$@" "$arg" # push replacement arg + done +fi + + +# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' + +# Collect all arguments for the java command: +# * DEFAULT_JVM_OPTS, JAVA_OPTS, JAVA_OPTS, and optsEnvironmentVar are not allowed to contain shell fragments, +# and any embedded shellness will be escaped. +# * For example: A user cannot expect ${Hostname} to be expanded, as it is an environment variable and will be +# treated as '${Hostname}' itself on the command line. + +set -- \ + "-Dorg.gradle.appname=$APP_BASE_NAME" \ + -classpath "$CLASSPATH" \ + org.gradle.wrapper.GradleWrapperMain \ + "$@" + +# Stop when "xargs" is not available. +if ! command -v xargs >/dev/null 2>&1 +then + die "xargs is not available" +fi + +# Use "xargs" to parse quoted args. +# +# With -n1 it outputs one arg per line, with the quotes and backslashes removed. +# +# In Bash we could simply go: +# +# readarray ARGS < <( xargs -n1 <<<"$var" ) && +# set -- "${ARGS[@]}" "$@" +# +# but POSIX shell has neither arrays nor command substitution, so instead we +# post-process each arg (as a line of input to sed) to backslash-escape any +# character that might be a shell metacharacter, then use eval to reverse +# that process (while maintaining the separation between arguments), and wrap +# the whole thing up as a single "set" statement. +# +# This will of course break if any of these variables contains a newline or +# an unmatched quote. +# + +eval "set -- $( + printf '%s\n' "$DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS" | + xargs -n1 | + sed ' s~[^-[:alnum:]+,./:=@_]~\\&~g; ' | + tr '\n' ' ' + )" '"$@"' + +exec "$JAVACMD" "$@" diff --git a/experimental/aws-lambda-java-profiler/extension/gradlew.bat b/experimental/aws-lambda-java-profiler/extension/gradlew.bat new file mode 100644 index 000000000..9b42019c7 --- /dev/null +++ b/experimental/aws-lambda-java-profiler/extension/gradlew.bat @@ -0,0 +1,94 @@ +@rem +@rem Copyright 2015 the original author or authors. +@rem +@rem Licensed under the Apache License, Version 2.0 (the "License"); +@rem you may not use this file except in compliance with the License. +@rem You may obtain a copy of the License at +@rem +@rem https://www.apache.org/licenses/LICENSE-2.0 +@rem +@rem Unless required by applicable law or agreed to in writing, software +@rem distributed under the License is distributed on an "AS IS" BASIS, +@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +@rem See the License for the specific language governing permissions and +@rem limitations under the License. +@rem +@rem SPDX-License-Identifier: Apache-2.0 +@rem + +@if "%DEBUG%"=="" @echo off +@rem ########################################################################## +@rem +@rem Gradle startup script for Windows +@rem +@rem ########################################################################## + +@rem Set local scope for the variables with windows NT shell +if "%OS%"=="Windows_NT" setlocal + +set DIRNAME=%~dp0 +if "%DIRNAME%"=="" set DIRNAME=. +@rem This is normally unused +set APP_BASE_NAME=%~n0 +set APP_HOME=%DIRNAME% + +@rem Resolve any "." and ".." in APP_HOME to make it shorter. +for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi + +@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" + +@rem Find java.exe +if defined JAVA_HOME goto findJavaFromJavaHome + +set JAVA_EXE=java.exe +%JAVA_EXE% -version >NUL 2>&1 +if %ERRORLEVEL% equ 0 goto execute + +echo. 1>&2 +echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 1>&2 +echo. 1>&2 +echo Please set the JAVA_HOME variable in your environment to match the 1>&2 +echo location of your Java installation. 1>&2 + +goto fail + +:findJavaFromJavaHome +set JAVA_HOME=%JAVA_HOME:"=% +set JAVA_EXE=%JAVA_HOME%/bin/java.exe + +if exist "%JAVA_EXE%" goto execute + +echo. 1>&2 +echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 1>&2 +echo. 1>&2 +echo Please set the JAVA_HOME variable in your environment to match the 1>&2 +echo location of your Java installation. 1>&2 + +goto fail + +:execute +@rem Setup the command line + +set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar + + +@rem Execute Gradle +"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %* + +:end +@rem End local scope for the variables with windows NT shell +if %ERRORLEVEL% equ 0 goto mainEnd + +:fail +rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of +rem the _cmd.exe /c_ return code! +set EXIT_CODE=%ERRORLEVEL% +if %EXIT_CODE% equ 0 set EXIT_CODE=1 +if not ""=="%GRADLE_EXIT_CONSOLE%" exit %EXIT_CODE% +exit /b %EXIT_CODE% + +:mainEnd +if "%OS%"=="Windows_NT" endlocal + +:omega 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/ExtensionClient.java b/experimental/aws-lambda-java-profiler/extension/src/main/java/com/amazonaws/services/lambda/extension/ExtensionClient.java new file mode 100644 index 000000000..60c13a811 --- /dev/null +++ b/experimental/aws-lambda-java-profiler/extension/src/main/java/com/amazonaws/services/lambda/extension/ExtensionClient.java @@ -0,0 +1,73 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: MIT-0 +package com.amazonaws.services.lambda.extension; + +import java.net.URI; +import java.net.http.HttpClient; +import java.net.http.HttpRequest; +import java.net.http.HttpResponse; +import java.util.Optional; + +/** + * Utility class that takes care of registration of extension, fetching the next event, initializing + * and exiting with error + */ +public class ExtensionClient { + private static final String EXTENSION_NAME = "profiler-extension"; + private static final String BASEURL = String + .format("http://%s/2020-01-01/extension", System.getenv("AWS_LAMBDA_RUNTIME_API")); + private static final String BODY = "{" + + " \"events\": [" + + " \"INVOKE\"," + + " \"SHUTDOWN\"" + + " ]" + + " }"; + private static final String LAMBDA_EXTENSION_IDENTIFIER = "Lambda-Extension-Identifier"; + private static final HttpClient client = HttpClient.newBuilder().build(); + + public static String registerExtension() { + final String registerUrl = String.format("%s/register", BASEURL); + HttpRequest request = HttpRequest.newBuilder() + .POST(HttpRequest.BodyPublishers.ofString(BODY)) + .header("Content-Type", "application/json") + .header("Lambda-Extension-Name", EXTENSION_NAME) + .uri(URI.create(registerUrl)) + .build(); + try { + HttpResponse response = client.send(request, HttpResponse.BodyHandlers.ofString()); + // Get extension ID from the response headers + Optional lambdaExtensionHeader = response.headers().firstValue("lambda-extension-identifier"); + if (lambdaExtensionHeader.isPresent()) { + return lambdaExtensionHeader.get(); + } + } + catch (Exception e) { + Logger.error("could not register the extension"); + e.printStackTrace(); + } + throw new RuntimeException("Error while registering extension"); + } + + public static String getNext(final String extensionId) { + try { + final String nextEventUrl = String.format("%s/event/next", BASEURL); + HttpRequest request = HttpRequest.newBuilder() + .GET() + .header(LAMBDA_EXTENSION_IDENTIFIER, extensionId) + .uri(URI.create(nextEventUrl)) + .build(); + HttpResponse response = client.send(request, HttpResponse.BodyHandlers.ofString()); + if (response.statusCode() == 200) { + return response.body(); + } else { + Logger.error("invalid status code returned while processing event = " + response.statusCode()); + } + } + catch (Exception e) { + Logger.error("could not get /next event"); + e.printStackTrace(); + } + + return null; + } +} diff --git a/experimental/aws-lambda-java-profiler/extension/src/main/java/com/amazonaws/services/lambda/extension/ExtensionMain.java b/experimental/aws-lambda-java-profiler/extension/src/main/java/com/amazonaws/services/lambda/extension/ExtensionMain.java new file mode 100644 index 000000000..18115a9fd --- /dev/null +++ b/experimental/aws-lambda-java-profiler/extension/src/main/java/com/amazonaws/services/lambda/extension/ExtensionMain.java @@ -0,0 +1,136 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: MIT-0 +package com.amazonaws.services.lambda.extension; + +import java.net.http.HttpClient; +import java.net.http.HttpRequest; +import java.net.http.HttpResponse; +import java.net.URI; +import java.util.UUID; + +public class ExtensionMain { + + private static final HttpClient client = HttpClient.newBuilder().build(); + private static String previousFileSuffix = null; + private static boolean coldstart = true; + private static final String REQUEST_ID = "requestId"; + private static final String EVENT_TYPE = "eventType"; + private static final String INTERNAL_COMMUNICATION_PORT = System.getenv().getOrDefault("AWS_LAMBDA_PROFILER_COMMUNICATION_PORT", "1234"); + public static final String HEADER_NAME = "X-FileName"; + + private static S3Manager s3Manager; + + public static void main(String[] args) { + final String extension = ExtensionClient.registerExtension(); + Logger.debug("Extension registration complete, extensionID: " + extension); + s3Manager = new S3Manager(); + while (true) { + try { + String response = ExtensionClient.getNext(extension); + if (response != null && !response.isEmpty()) { + final String eventType = extractInfo(EVENT_TYPE, response); + Logger.debug("eventType = " + eventType); + if (eventType != null) { + switch (eventType) { + case "INVOKE": + handleInvoke(response); + break; + case "SHUTDOWN": + handleShutDown(); + break; + default: + Logger.error("invalid event type received " + eventType); + } + } + } + } catch (Exception e) { + Logger.error("error while processing extension -" + e.getMessage()); + e.printStackTrace(); + } + } + } + + private static void handleShutDown() { + Logger.debug("handling SHUTDOWN event, flushing the last profile"); + try { + // no need to stop the profiler as it has been stopped by the shutdown hook + s3Manager.upload(previousFileSuffix, true); + } catch (Exception e) { + Logger.error("could not upload the file"); + throw e; + } + System.exit(0); + } + + public static void handleInvoke(String payload) { + final String requestId = extractInfo(REQUEST_ID, payload); + final String randomSuffix = UUID.randomUUID().toString().substring(0,5); + Logger.debug("handling INVOKE event, requestID = " + requestId); + if (!coldstart) { + try { + stopProfiler(previousFileSuffix); + s3Manager.upload(previousFileSuffix, false); + startProfiler(); + } catch (Exception e) { + Logger.error("could not start the profiler"); + throw e; + } + } + coldstart = false; + previousFileSuffix = extractInfo(REQUEST_ID, payload) + "-" + randomSuffix; + } + + private static String extractInfo(String info, String jsonString) { + String prefix = "\"" + info + "\":\""; + String suffix = "\""; + + int startIndex = jsonString.indexOf(prefix); + if (startIndex == -1) { + return null; // requestId not found + } + + startIndex += prefix.length(); + int endIndex = jsonString.indexOf(suffix, startIndex); + + if (endIndex == -1) { + return null; // Malformed JSON + } + + return jsonString.substring(startIndex, endIndex); + } + + private static void startProfiler() { + try { + String url = String.format("http://localhost:%s/profiler/start", INTERNAL_COMMUNICATION_PORT); + HttpRequest request = HttpRequest.newBuilder() + .GET() + .uri(URI.create(url)) + .build(); + HttpResponse response = client.send(request, HttpResponse.BodyHandlers.ofString()); + if (response.statusCode() == 200) { + Logger.debug("profiler successfully started"); + } + } catch(Exception e) { + Logger.error("could not start the profiler"); + e.printStackTrace(); + } + } + + private static void stopProfiler(String fileNameSuffix) { + try { + String url = String.format("http://localhost:%s/profiler/stop", INTERNAL_COMMUNICATION_PORT); + HttpRequest request = HttpRequest.newBuilder() + .GET() + .setHeader(HEADER_NAME, fileNameSuffix) + .uri(URI.create(url)) + .build(); + HttpResponse response = client.send(request, HttpResponse.BodyHandlers.ofString()); + if (response.statusCode() == 200) { + Logger.debug("profiler successfully stopped"); + } + } catch(Exception e) { + Logger.error("could not stop the profiler"); + e.printStackTrace(); + } + } +} diff --git a/experimental/aws-lambda-java-profiler/extension/src/main/java/com/amazonaws/services/lambda/extension/Logger.java b/experimental/aws-lambda-java-profiler/extension/src/main/java/com/amazonaws/services/lambda/extension/Logger.java new file mode 100644 index 000000000..e064da101 --- /dev/null +++ b/experimental/aws-lambda-java-profiler/extension/src/main/java/com/amazonaws/services/lambda/extension/Logger.java @@ -0,0 +1,25 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: MIT-0 +package com.amazonaws.services.lambda.extension; + +public class Logger { + + private static final boolean IS_DEBUG_ENABLED = initializeDebugFlag(); + private static final String PREFIX = "[PROFILER] "; + + private static boolean initializeDebugFlag() { + String envValue = System.getenv("AWS_LAMBDA_PROFILER_DEBUG"); + return "true".equalsIgnoreCase(envValue) || "1".equals(envValue); + } + + public static void debug(String message) { + if(IS_DEBUG_ENABLED) { + System.out.println(PREFIX + message); + } + } + + public static void error(String message) { + System.out.println(PREFIX + message); + } + +} \ No newline at end of file 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 new file mode 100644 index 000000000..2a84eb641 --- /dev/null +++ b/experimental/aws-lambda-java-profiler/extension/src/main/java/com/amazonaws/services/lambda/extension/PreMain.java @@ -0,0 +1,131 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// 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 static com.amazonaws.services.lambda.extension.Constants.PROFILER_START_COMMAND; +import static com.amazonaws.services.lambda.extension.Constants.PROFILER_STOP_COMMAND; + +public class PreMain { + + + 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")) { + 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 + ); + server.createContext("/profiler/start", new StartProfiler()); + server.createContext("/profiler/stop", new StopProfiler()); + server.setExecutor(null); // Use the default executor + server.start(); + } catch (Exception e) { + e.printStackTrace(); + } + } + } + + private static boolean createFileIfNotExist(String filePath) { + File file = new File(filePath); + try { + return file.createNewFile(); + } catch (IOException e) { + System.out.println(e); + return false; + } + } + + 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); + stopProfiler(fileName); + String response = "ok"; + exchange.sendResponseHeaders(200, response.length()); + try (OutputStream os = exchange.getResponseBody()) { + os.write(response.getBytes(StandardCharsets.UTF_8)); + } + } + } + + public static class StartProfiler implements HttpHandler { + + @Override + public void handle(HttpExchange exchange) throws IOException { + Logger.debug("hit /profiler/start"); + startProfiler(); + String response = "ok"; + exchange.sendResponseHeaders(200, response.length()); + try (OutputStream os = exchange.getResponseBody()) { + os.write(response.getBytes(StandardCharsets.UTF_8)); + } + } + } + + public static void stopProfiler(String fileNameSuffix) { + try { + 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(); + } + } + + public static void startProfiler() { + try { + Logger.debug( + "starting the profiler with command = " + PROFILER_START_COMMAND + ); + AsyncProfiler.getInstance().execute(PROFILER_START_COMMAND); + } catch (IOException e) { + throw new RuntimeException(e); + } + } + + public static void registerShutdownHook() { + Logger.debug("registering shutdown hook wit command = " + PROFILER_STOP_COMMAND); + Thread shutdownHook = new Thread( + new ShutdownHook(PROFILER_STOP_COMMAND) + ); + Runtime.getRuntime().addShutdownHook(shutdownHook); + } +} 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 new file mode 100644 index 000000000..0e31a2421 --- /dev/null +++ b/experimental/aws-lambda-java-profiler/extension/src/main/java/com/amazonaws/services/lambda/extension/S3Manager.java @@ -0,0 +1,66 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: MIT-0 +package com.amazonaws.services.lambda.extension; + +import java.io.File; +import java.time.format.DateTimeFormatter; +import java.time.LocalDate; + +import software.amazon.awssdk.core.sync.RequestBody; +import software.amazon.awssdk.services.s3.S3Client; +import software.amazon.awssdk.services.s3.model.PutObjectRequest; +import software.amazon.awssdk.services.s3.model.PutObjectResponse; + +public class S3Manager { + + private static final String RESULTS_BUCKET = "AWS_LAMBDA_PROFILER_RESULTS_BUCKET_NAME"; + private static final String FUNCTION_NAME = System.getenv().getOrDefault("AWS_LAMBDA_FUNCTION_NAME", "function"); + private S3Client s3Client; + private String bucketName; + + public S3Manager() { + final String bucketName = System.getenv(RESULTS_BUCKET); + Logger.debug("creating S3Manager with bucketName = " + bucketName); + if (null == bucketName || bucketName.isEmpty()) { + throw new IllegalArgumentException("please set the bucket name using AWS_LAMBDA_PROFILER_RESULTS_BUCKET_NAME environment variable"); + } + this.s3Client = S3Client.builder().build(); + this.bucketName = bucketName; + Logger.debug("S3Manager successfully created"); + } + + public void upload(String fileName, boolean isShutDownEvent) { + try { + final String suffix = isShutDownEvent ? "shutdown" : fileName; + final String key = buildKey(FUNCTION_NAME, fileName); + Logger.debug("uploading profile to key = " + key); + PutObjectRequest putObjectRequest = PutObjectRequest.builder() + .bucket(bucketName) + .key(key) + .build(); + File file = new File(String.format(Constants.getFilePathFromEnv(), suffix)); + if (file.exists()) { + Logger.debug("file size is " + file.length()); + RequestBody requestBody = RequestBody.fromFile(file); + PutObjectResponse response = s3Client.putObject(putObjectRequest, requestBody); + Logger.debug("profile uploaded successfully. ETag: " + response.eTag()); + if(file.delete()) { + Logger.debug("file deleted"); + } + } else { + throw new IllegalArgumentException("could not find the profile to upload"); + } + } catch (Exception e) { + Logger.error("could not upload the profile"); + e.printStackTrace(); + } + } + + private String buildKey(String functionName, String fileName) { + final LocalDate currentDate = LocalDate.now(); + final DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy/MM/dd"); + final String formattedDate = currentDate.format(formatter); + return String.format("%s/%s/%s", formattedDate, functionName, fileName); + } + +} \ No newline at end of file diff --git a/experimental/aws-lambda-java-profiler/extension/src/main/java/com/amazonaws/services/lambda/extension/ShutdownHook.java b/experimental/aws-lambda-java-profiler/extension/src/main/java/com/amazonaws/services/lambda/extension/ShutdownHook.java new file mode 100644 index 000000000..a36584bc1 --- /dev/null +++ b/experimental/aws-lambda-java-profiler/extension/src/main/java/com/amazonaws/services/lambda/extension/ShutdownHook.java @@ -0,0 +1,26 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: MIT-0 +package com.amazonaws.services.lambda.extension; + +import one.profiler.AsyncProfiler; + +public class ShutdownHook implements Runnable { + + private String stopCommand; + + public ShutdownHook(String stopCommand) { + this.stopCommand = stopCommand; + } + + @Override + public void run() { + Logger.debug("running ShutdownHook"); + try { + final String fileName = "/tmp/profiling-data-shutdown.html"; + Logger.debug("stopping the profiler"); + AsyncProfiler.getInstance().execute(String.format(this.stopCommand, fileName)); + } catch (Exception e) { + Logger.error("could not stop the profiler"); + } + } +} \ No newline at end of file diff --git a/experimental/aws-lambda-java-profiler/integration_tests/cleanup.sh b/experimental/aws-lambda-java-profiler/integration_tests/cleanup.sh new file mode 100755 index 000000000..d58142a04 --- /dev/null +++ b/experimental/aws-lambda-java-profiler/integration_tests/cleanup.sh @@ -0,0 +1,45 @@ +#!/bin/bash + +# Set variables +LAYER_ARN=$(cat /tmp/layer_arn) +FUNCTION_NAME="aws-lambda-java-profiler-function-${GITHUB_RUN_ID}" +ROLE_NAME="aws-lambda-java-profiler-role-${GITHUB_RUN_ID}" + +# Function to check if a command was successful +check_success() { + if [ $? -eq 0 ]; then + echo "Success: $1" + else + echo "Error: Failed to $1" + exit 1 + fi +} + +# Delete Lambda Layer +echo "Deleting Lambda Layer..." +aws lambda delete-layer-version --layer-name $(echo $LAYER_ARN | cut -d: -f7) --version-number $(echo $LAYER_ARN | cut -d: -f8) +check_success "delete Lambda Layer" + +# Delete Lambda Function +echo "Deleting Lambda Function..." +aws lambda delete-function --function-name $FUNCTION_NAME +check_success "delete Lambda Function" + +# Delete IAM Role +echo "Deleting IAM Role..." +# First, detach all policies from the role +for policy in $(aws iam list-attached-role-policies --role-name $ROLE_NAME --query 'AttachedPolicies[*].PolicyArn' --output text); do + aws iam detach-role-policy --role-name $ROLE_NAME --policy-arn $policy + check_success "detach policy $policy from role $ROLE_NAME" +done + +# Remove s3 inline policy +aws iam delete-role-policy --role-name $ROLE_NAME --policy-name "s3PutObject" +check_success "deleted inline policy" + + +# Then delete the role +aws iam delete-role --role-name $ROLE_NAME +check_success "delete IAM Role" + +echo "All deletions completed successfully." \ No newline at end of file diff --git a/experimental/aws-lambda-java-profiler/integration_tests/create_bucket.sh b/experimental/aws-lambda-java-profiler/integration_tests/create_bucket.sh new file mode 100755 index 000000000..0ba50b732 --- /dev/null +++ b/experimental/aws-lambda-java-profiler/integration_tests/create_bucket.sh @@ -0,0 +1,14 @@ +#!/bin/bash + +PROFILER_RESULTS_BUCKET_NAME="aws-lambda-java-profiler-bucket-${GITHUB_RUN_ID}" + +# Create the S3 bucket +aws s3 mb s3://"$PROFILER_RESULTS_BUCKET_NAME" + +# Check if the bucket was created successfully +if [ $? -eq 0 ]; then + echo "Bucket '$PROFILER_RESULTS_BUCKET_NAME' created successfully." +else + echo "Error: Failed to create bucket '$PROFILER_RESULTS_BUCKET_NAME'." + exit 1 +fi \ No newline at end of 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 new file mode 100755 index 000000000..12ba1cb2b --- /dev/null +++ b/experimental/aws-lambda-java-profiler/integration_tests/create_function.sh @@ -0,0 +1,85 @@ +#!/bin/bash + +# 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" +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 +gradle :buildZip +cd ../.. + +# Create IAM role for Lambda +ROLE_ARN=$(aws iam create-role \ + --role-name $ROLE_NAME \ + --assume-role-policy-document '{"Version": "2012-10-17","Statement": [{ "Effect": "Allow", "Principal": {"Service": "lambda.amazonaws.com"}, "Action": "sts:AssumeRole"}]}' \ + --query 'Role.Arn' \ + --output text) + +# Attach basic Lambda execution policy to the role +aws iam attach-role-policy \ + --role-name $ROLE_NAME \ + --policy-arn arn:aws:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole + +# Attach s3:PutObject policy to the role so we can upload profiles +POLICY_DOCUMENT=$(cat < $new_filename" + else + echo "No change: $filename" + fi + fi +done + +echo "All files processed." \ No newline at end of file diff --git a/experimental/aws-lambda-java-profiler/integration_tests/helloworld/build.gradle b/experimental/aws-lambda-java-profiler/integration_tests/helloworld/build.gradle new file mode 100644 index 000000000..79ffa030a --- /dev/null +++ b/experimental/aws-lambda-java-profiler/integration_tests/helloworld/build.gradle @@ -0,0 +1,32 @@ +plugins { + id 'java' +} + +repositories { + mavenCentral() +} + +java { + sourceCompatibility = JavaVersion.VERSION_21 + targetCompatibility = JavaVersion.VERSION_21 +} + +dependencies { + implementation ( + 'com.amazonaws:aws-lambda-java-core:1.2.3', + 'com.amazonaws:aws-lambda-java-events:3.11.0', + 'org.slf4j:slf4j-api:2.0.13' + ) +} + +task buildZip(type: Zip) { + archiveBaseName = "code" + from compileJava + from processResources + into('lib') { + from configurations.runtimeClasspath + } +} + + +build.dependsOn buildZip \ No newline at end of file diff --git a/experimental/aws-lambda-java-profiler/integration_tests/helloworld/src/main/java/helloworld/Handler.java b/experimental/aws-lambda-java-profiler/integration_tests/helloworld/src/main/java/helloworld/Handler.java new file mode 100644 index 000000000..a29cae18e --- /dev/null +++ b/experimental/aws-lambda-java-profiler/integration_tests/helloworld/src/main/java/helloworld/Handler.java @@ -0,0 +1,53 @@ +package helloworld; + +import java.io.BufferedReader; +import java.io.IOException; +import java.io.InputStreamReader; +import java.net.URL; +import java.util.HashMap; +import java.util.Map; + +import java.util.stream.Collectors; +import java.util.ArrayList; +import java.util.List; +import com.amazonaws.services.lambda.runtime.events.APIGatewayProxyResponseEvent; +import com.amazonaws.services.lambda.runtime.Context; + +import com.amazonaws.services.lambda.runtime.RequestHandler; +import com.amazonaws.services.lambda.runtime.events.APIGatewayProxyRequestEvent; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +public class Handler implements RequestHandler { + + public APIGatewayProxyResponseEvent handleRequest(final APIGatewayProxyRequestEvent input, final Context context) { + long start = System.currentTimeMillis(); + List result = slowRecursiveFunction(0, 5); + long end = System.currentTimeMillis(); + long duration = end - start; + + System.out.println("Function execution time: " + duration + " ms"); + System.out.println("Result size: " + result.size()); + System.out.println("First few elements: " + result.subList(0, Math.min(10, result.size()))); + + return new APIGatewayProxyResponseEvent() + .withStatusCode(200) + .withBody("ok"); + + } + + private static List slowRecursiveFunction(int n, int depth) { + List result = new ArrayList<>(); + if (depth == 0) { + return result; + } + long startTime = System.currentTimeMillis(); + while (System.currentTimeMillis() - startTime < 100) { + // nothing to do here + } + result.add(n); + result.addAll(slowRecursiveFunction(n + 2, depth - 1)); + return result; + } +} diff --git a/experimental/aws-lambda-java-profiler/integration_tests/helloworld/src/main/resources/wrapper.sh b/experimental/aws-lambda-java-profiler/integration_tests/helloworld/src/main/resources/wrapper.sh new file mode 100644 index 000000000..b54b77673 --- /dev/null +++ b/experimental/aws-lambda-java-profiler/integration_tests/helloworld/src/main/resources/wrapper.sh @@ -0,0 +1,10 @@ +#!/bin/bash + +# the path to the interpreter and all of the originally intended arguments +args=("$@") + +# the extra options to pass to the interpreter +echo "${args[@]}" + +# start the runtime with the extra options +exec "${args[@]}" \ 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 new file mode 100755 index 000000000..39b0dd885 --- /dev/null +++ b/experimental/aws-lambda-java-profiler/integration_tests/invoke_function.sh @@ -0,0 +1,74 @@ +#!/bin/bash + +# Set variables +FUNCTION_NAME="aws-lambda-java-profiler-function-${GITHUB_RUN_ID}" +PAYLOAD='{"key": "value"}' + +echo "Invoking Lambda function: $FUNCTION_NAME" + +# Invoke the Lambda function synchronously and capture the response +RESPONSE=$(aws lambda invoke \ + --function-name "$FUNCTION_NAME" \ + --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 + +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 + + +# Invoke it a second time for warm start +echo "Invoking Lambda function: $FUNCTION_NAME" + +# Invoke the Lambda function synchronously and capture the response +RESPONSE=$(aws lambda invoke \ + --function-name "$FUNCTION_NAME" \ + --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 + +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/experimental/aws-lambda-java-profiler/integration_tests/publish_layer.sh b/experimental/aws-lambda-java-profiler/integration_tests/publish_layer.sh new file mode 100755 index 000000000..879944e8e --- /dev/null +++ b/experimental/aws-lambda-java-profiler/integration_tests/publish_layer.sh @@ -0,0 +1,42 @@ +#!/bin/bash + +# Set variables +LAYER_NAME="aws-lambda-java-profiler-test" +DESCRIPTION="AWS Lambda Java Profiler Test Layer" +ZIP_FILE="./extension/extension.zip" +RUNTIME="java21" +ARCHITECTURE="x86_64" + +# Check if AWS CLI is installed +if ! command -v aws &> /dev/null; then + echo "AWS CLI is not installed. Please install it first." + exit 1 +fi + +# Check if the ZIP file exists +if [ ! -f "$ZIP_FILE" ]; then + echo "ZIP file $ZIP_FILE not found. Please make sure it exists." + exit 1 +fi + +# Publish the layer +echo "Publishing layer $LAYER_NAME..." +RESPONSE=$(aws lambda publish-layer-version \ + --layer-name "$LAYER_NAME" \ + --description "$DESCRIPTION" \ + --zip-file "fileb://$ZIP_FILE" \ + --compatible-runtimes "$RUNTIME" \ + --compatible-architectures "$ARCHITECTURE") + +# Check if the layer was published successfully +if [ $? -eq 0 ]; then + LAYER_VERSION=$(echo $RESPONSE | jq -r '.Version') + LAYER_ARN=$(echo $RESPONSE | jq -r '.LayerVersionArn') + echo "Layer published successfully!" + echo "Layer Version: $LAYER_VERSION" + echo "Layer ARN: $LAYER_ARN" + echo $LAYER_ARN > /tmp/layer_arn +else + echo "Failed to publish layer. Please check your AWS credentials and permissions." + exit 1 +fi \ No newline at end of file diff --git a/experimental/aws-lambda-java-profiler/update-function.sh b/experimental/aws-lambda-java-profiler/update-function.sh new file mode 100755 index 000000000..e849246a6 --- /dev/null +++ b/experimental/aws-lambda-java-profiler/update-function.sh @@ -0,0 +1,93 @@ +#!/bin/bash + +# Check if a function name was provided +if [ $# -eq 0 ]; then + echo "Please provide a function name as an argument." + echo "Usage: $0 " + exit 1 +fi + +FUNCTION_NAME="$1" + +# Generate a random lowercase S3 bucket name +RANDOM_SUFFIX=$(uuidgen | tr '[:upper:]' '[:lower:]' | cut -d'-' -f1) +BUCKET_NAME="my-bucket-${RANDOM_SUFFIX}" +echo "Generated bucket name: $BUCKET_NAME" + +# Create the S3 bucket with the random name +aws s3 mb "s3://$BUCKET_NAME" + +# Create a Lambda layer +aws lambda publish-layer-version \ + --layer-name profiler-layer \ + --description "Profiler Layer" \ + --license-info "MIT" \ + --zip-file fileb://extension/extension.zip \ + --compatible-runtimes java11 java17 java21 \ + --compatible-architectures "arm64" "x86_64" + +# Assign the layer to the function +aws lambda update-function-configuration \ + --function-name "$FUNCTION_NAME" \ + --layers $(aws lambda list-layer-versions --layer-name profiler-layer --query 'LayerVersions[0].LayerVersionArn' --output text) + +# Wait for the function to be updated +aws lambda wait function-updated \ + --function-name "$FUNCTION_NAME" + +# Get existing environment variables (handle null case) +EXISTING_VARS=$(aws lambda get-function-configuration --function-name "$FUNCTION_NAME" --query "Environment.Variables" --output json 2>/dev/null) +if [[ -z "$EXISTING_VARS" || "$EXISTING_VARS" == "null" ]]; then + EXISTING_VARS="{}" +fi + +# Define new environment variables in JSON format +NEW_VARS=$(jq -n --arg bucket "$BUCKET_NAME" \ + --arg java_opts "-XX:+UnlockDiagnosticVMOptions -XX:+DebugNonSafepoints -javaagent:/opt/profiler-extension.jar" \ + '{AWS_LAMBDA_PROFILER_RESULTS_BUCKET_NAME: $bucket, JAVA_TOOL_OPTIONS: $java_opts}') + +# Merge existing and new variables (compact JSON output) +UPDATED_VARS=$(echo "$EXISTING_VARS" | jq -c --argjson new_vars "$NEW_VARS" '. + $new_vars') + +# Convert JSON to "Key=Value" format for AWS CLI +ENV_VARS_FORMATTED=$(echo "$UPDATED_VARS" | jq -r 'to_entries | map("\(.key)=\(.value)") | join(",")') + +# Update Lambda function with correct format +aws lambda update-function-configuration \ + --function-name "$FUNCTION_NAME" \ + --environment "Variables={$ENV_VARS_FORMATTED}" + +# Update the function's permissions to write to the S3 bucket +# Get the function's execution role +ROLE_NAME=$(aws lambda get-function --function-name "$FUNCTION_NAME" --query 'Configuration.Role' --output text | awk -F'/' '{print $NF}') + +# Create a policy document +cat << EOF > s3-write-policy.json +{ + "Version": "2012-10-17", + "Statement": [ + { + "Effect": "Allow", + "Action": [ + "s3:PutObject" + ], + "Resource": [ + "arn:aws:s3:::$BUCKET_NAME", + "arn:aws:s3:::$BUCKET_NAME/*" + ] + } + ] +} +EOF + +# Attach the policy to the role +aws iam put-role-policy \ + --role-name "$ROLE_NAME" \ + --policy-name S3WriteAccess \ + --policy-document file://s3-write-policy.json + +echo "Setup completed for function $FUNCTION_NAME with S3 bucket $BUCKET_NAME" +echo "S3 write permissions added to the function's execution role" + +# Clean up temporary files +rm s3-write-policy.json diff --git a/samples/custom-serialization/.gitignore b/samples/custom-serialization/.gitignore new file mode 100644 index 000000000..2b448259f --- /dev/null +++ b/samples/custom-serialization/.gitignore @@ -0,0 +1,7 @@ +**/target/ +**/HelloWorld.iml +**/samconfig.toml +**/dependency-reduced-pom.xml +**/.aws-sam +**/.gradle +**/bin diff --git a/samples/custom-serialization/README.md b/samples/custom-serialization/README.md new file mode 100644 index 000000000..d9e751471 --- /dev/null +++ b/samples/custom-serialization/README.md @@ -0,0 +1,5 @@ +The Lambda Java managed runtimes support custom serialization for JSON events. +https://docs.aws.amazon.com/lambda/latest/dg/java-custom-serialization.html + +## Sample projects +In this repository you will find a number of sample projects from AWS to help you get started with the custom serialization feature. diff --git a/samples/custom-serialization/fastJson/HelloWorldFunction/pom.xml b/samples/custom-serialization/fastJson/HelloWorldFunction/pom.xml new file mode 100644 index 000000000..2a963ca21 --- /dev/null +++ b/samples/custom-serialization/fastJson/HelloWorldFunction/pom.xml @@ -0,0 +1,52 @@ + + 4.0.0 + helloworld + HelloWorld + 1.0 + jar + A sample Hello World created for SAM CLI. + + 21 + 21 + + + + + com.amazonaws + aws-lambda-java-core + 1.2.3 + + + com.amazonaws + aws-lambda-java-events + 3.16.0 + + + + com.alibaba.fastjson2 + fastjson2 + 2.0.33 + + + + + + + org.apache.maven.plugins + maven-shade-plugin + 3.6.1 + + + + + package + + shade + + + + + + + diff --git a/samples/custom-serialization/fastJson/HelloWorldFunction/src/main/java/com/example/vehicles/serialization/FastJsonSerializer.java b/samples/custom-serialization/fastJson/HelloWorldFunction/src/main/java/com/example/vehicles/serialization/FastJsonSerializer.java new file mode 100644 index 000000000..44709e768 --- /dev/null +++ b/samples/custom-serialization/fastJson/HelloWorldFunction/src/main/java/com/example/vehicles/serialization/FastJsonSerializer.java @@ -0,0 +1,50 @@ +/* +Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +SPDX-License-Identifier: Apache-2.0 +*/ + +package com.example.vehicles.serialization; + +import com.alibaba.fastjson2.JSON; +import com.alibaba.fastjson2.JSONException; +import com.amazonaws.services.lambda.runtime.CustomPojoSerializer; +import java.io.InputStream; +import java.io.OutputStream; +import java.lang.reflect.Type; + +public class FastJsonSerializer implements CustomPojoSerializer { + /** + * ServiceLoader class requires that the single exposed provider type has a default constructor + * to easily instantiate the service providers that it finds + */ + public FastJsonSerializer() { + } + + @Override + public T fromJson(InputStream input, Type type) { + try { + return JSON.parseObject(input, type); + } catch (JSONException e) { + throw (e); + } + } + + @Override + public T fromJson(String input, Type type) { + try { + return JSON.parseObject(input, type); + } catch (JSONException e) { + throw (e); + } + } + + @Override + public void toJson(T value, OutputStream output, Type type) { + try { + JSON.writeTo(output, value); + } catch (JSONException e) { + throw (e); + } + } + +} diff --git a/samples/custom-serialization/fastJson/HelloWorldFunction/src/main/java/helloworld/App.java b/samples/custom-serialization/fastJson/HelloWorldFunction/src/main/java/helloworld/App.java new file mode 100644 index 000000000..02ba6048f --- /dev/null +++ b/samples/custom-serialization/fastJson/HelloWorldFunction/src/main/java/helloworld/App.java @@ -0,0 +1,23 @@ +/* +Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +SPDX-License-Identifier: Apache-2.0 +*/ + +package helloworld; + +import com.amazonaws.services.lambda.runtime.Context; +import com.amazonaws.services.lambda.runtime.RequestHandler; +import com.amazonaws.services.lambda.runtime.events.APIGatewayProxyResponseEvent; + +/** + * Handler for requests to Lambda function. + */ +public class App implements RequestHandler { + + public APIGatewayProxyResponseEvent handleRequest(Vehicle vehicle, Context context) { + System.out.println("input: " + vehicle); + + return new APIGatewayProxyResponseEvent().withStatusCode(200); + } + +} diff --git a/samples/custom-serialization/fastJson/HelloWorldFunction/src/main/java/helloworld/Vehicle.java b/samples/custom-serialization/fastJson/HelloWorldFunction/src/main/java/helloworld/Vehicle.java new file mode 100644 index 000000000..2d34ee6eb --- /dev/null +++ b/samples/custom-serialization/fastJson/HelloWorldFunction/src/main/java/helloworld/Vehicle.java @@ -0,0 +1,49 @@ +/* +Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +SPDX-License-Identifier: Apache-2.0 +*/ + +package helloworld; + +import com.alibaba.fastjson2.annotation.JSONField; + +public class Vehicle { + + @JSONField(name = "vehicle-type") + private String vehicleType; + + @JSONField(name = "vehicleID") + private String vehicleId; + + public Vehicle() { + } + + public Vehicle(String vehicleType, String vehicleId) { + this.vehicleType = vehicleType; + this.vehicleId = vehicleId; + } + + public String getVehicleType() { + return vehicleType; + } + + public void setVehicleType(String vehicleType) { + this.vehicleType = vehicleType; + } + + public String getVehicleId() { + return vehicleId; + } + + public void setVehicleId(String vehicleId) { + this.vehicleId = vehicleId; + } + + @Override + public String toString() { + return "Vehicle{" + + "vehicleType='" + vehicleType + '\'' + + ", vehicleId='" + vehicleId + '\'' + + '}'; + } +} diff --git a/samples/custom-serialization/fastJson/HelloWorldFunction/src/main/resources/META-INF/services/com.amazonaws.services.lambda.runtime.CustomPojoSerializer b/samples/custom-serialization/fastJson/HelloWorldFunction/src/main/resources/META-INF/services/com.amazonaws.services.lambda.runtime.CustomPojoSerializer new file mode 100644 index 000000000..58c85a7a4 --- /dev/null +++ b/samples/custom-serialization/fastJson/HelloWorldFunction/src/main/resources/META-INF/services/com.amazonaws.services.lambda.runtime.CustomPojoSerializer @@ -0,0 +1 @@ +com.example.vehicles.serialization.FastJsonSerializer \ No newline at end of file diff --git a/samples/custom-serialization/fastJson/README.md b/samples/custom-serialization/fastJson/README.md new file mode 100644 index 000000000..3f6a2f3a2 --- /dev/null +++ b/samples/custom-serialization/fastJson/README.md @@ -0,0 +1,7 @@ +Build and test commands + +```bash +sam build +sam local invoke -e events/event.json +``` + diff --git a/samples/custom-serialization/fastJson/events/event.json b/samples/custom-serialization/fastJson/events/event.json new file mode 100644 index 000000000..5d882dba3 --- /dev/null +++ b/samples/custom-serialization/fastJson/events/event.json @@ -0,0 +1,4 @@ +{ + "vehicle-type": "car", + "vehicleID": 123 +} \ No newline at end of file diff --git a/samples/custom-serialization/fastJson/template.yaml b/samples/custom-serialization/fastJson/template.yaml new file mode 100644 index 000000000..016239cf5 --- /dev/null +++ b/samples/custom-serialization/fastJson/template.yaml @@ -0,0 +1,43 @@ +AWSTemplateFormatVersion: '2010-09-09' +Transform: AWS::Serverless-2016-10-31 +Description: > + fastJson + + Sample SAM Template for fastJson + +# More info about Globals: https://github.com/awslabs/serverless-application-model/blob/master/docs/globals.rst +Globals: + Function: + Timeout: 20 + MemorySize: 512 + +Resources: + HelloWorldFunction: + Type: AWS::Serverless::Function # More info about Function Resource: https://github.com/awslabs/serverless-application-model/blob/master/versions/2016-10-31.md#awsserverlessfunction + Properties: + CodeUri: HelloWorldFunction + Handler: helloworld.App::handleRequest + Runtime: java21 + Architectures: + - x86_64 + MemorySize: 512 + Events: + HelloWorld: + Type: Api # More info about API Event Source: https://github.com/awslabs/serverless-application-model/blob/master/versions/2016-10-31.md#api + Properties: + Path: /hello + Method: get + +Outputs: + # ServerlessRestApi is an implicit API created out of Events key under Serverless::Function + # Find out more about other implicit resources you can reference within SAM + # https://github.com/awslabs/serverless-application-model/blob/master/docs/internals/generated_resources.rst#api + HelloWorldApi: + Description: "API Gateway endpoint URL for Prod stage for Hello World function" + Value: !Sub "https://${ServerlessRestApi}.execute-api.${AWS::Region}.amazonaws.com/Prod/hello/" + HelloWorldFunction: + Description: "Hello World Lambda Function ARN" + Value: !GetAtt HelloWorldFunction.Arn + HelloWorldFunctionIamRole: + Description: "Implicit IAM Role created for Hello World function" + Value: !GetAtt HelloWorldFunctionRole.Arn diff --git a/samples/custom-serialization/gson/HelloWorldFunction/pom.xml b/samples/custom-serialization/gson/HelloWorldFunction/pom.xml new file mode 100644 index 000000000..47d04926a --- /dev/null +++ b/samples/custom-serialization/gson/HelloWorldFunction/pom.xml @@ -0,0 +1,51 @@ + + 4.0.0 + helloworld + HelloWorld + 1.0 + jar + A sample Hello World created for SAM CLI. + + 21 + 21 + + + + + com.amazonaws + aws-lambda-java-core + 1.2.3 + + + com.amazonaws + aws-lambda-java-events + 3.16.0 + + + com.google.code.gson + gson + 2.11.0 + + + + + + + org.apache.maven.plugins + maven-shade-plugin + 3.6.1 + + + + + package + + shade + + + + + + + diff --git a/samples/custom-serialization/gson/HelloWorldFunction/src/main/java/com/example/vehicles/serialization/GsonSerializer.java b/samples/custom-serialization/gson/HelloWorldFunction/src/main/java/com/example/vehicles/serialization/GsonSerializer.java new file mode 100644 index 000000000..5d2597657 --- /dev/null +++ b/samples/custom-serialization/gson/HelloWorldFunction/src/main/java/com/example/vehicles/serialization/GsonSerializer.java @@ -0,0 +1,60 @@ +/* +Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +SPDX-License-Identifier: Apache-2.0 +*/ + +package com.example.vehicles.serialization; + +import com.amazonaws.services.lambda.runtime.CustomPojoSerializer; +import com.google.gson.Gson; +import com.google.gson.GsonBuilder; +import com.google.gson.stream.JsonReader; +import java.io.BufferedWriter; +import java.io.IOException; +import java.io.InputStream; +import java.io.InputStreamReader; +import java.io.OutputStream; +import java.io.OutputStreamWriter; +import java.io.PrintWriter; +import java.io.StringReader; +import java.io.UncheckedIOException; +import java.lang.reflect.Type; +import java.nio.charset.Charset; +import java.nio.charset.StandardCharsets; + +public class GsonSerializer implements CustomPojoSerializer { + private static final Charset utf8 = StandardCharsets.UTF_8; + private static Gson gson; + + public GsonSerializer() { + gson = new GsonBuilder() + .disableHtmlEscaping() + .serializeSpecialFloatingPointValues() + .create(); + } + + @Override + public T fromJson(InputStream input, Type type) { + try (JsonReader reader = new JsonReader(new InputStreamReader(input, utf8))) { + return gson.fromJson(reader, type); + } catch (IOException e) { + throw new UncheckedIOException(e); + } + } + + @Override + public T fromJson(String input, Type type) { + try (JsonReader reader = new JsonReader(new StringReader(input))) { + return gson.fromJson(reader, type); + } catch (IOException e) { + throw new UncheckedIOException(e); + } + } + + @Override + public void toJson(T value, OutputStream output, Type type) { + try (PrintWriter writer = new PrintWriter(new BufferedWriter(new OutputStreamWriter(output, utf8)))) { + writer.write(gson.toJson(value)); + } + } +} diff --git a/samples/custom-serialization/gson/HelloWorldFunction/src/main/java/helloworld/App.java b/samples/custom-serialization/gson/HelloWorldFunction/src/main/java/helloworld/App.java new file mode 100644 index 000000000..02ba6048f --- /dev/null +++ b/samples/custom-serialization/gson/HelloWorldFunction/src/main/java/helloworld/App.java @@ -0,0 +1,23 @@ +/* +Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +SPDX-License-Identifier: Apache-2.0 +*/ + +package helloworld; + +import com.amazonaws.services.lambda.runtime.Context; +import com.amazonaws.services.lambda.runtime.RequestHandler; +import com.amazonaws.services.lambda.runtime.events.APIGatewayProxyResponseEvent; + +/** + * Handler for requests to Lambda function. + */ +public class App implements RequestHandler { + + public APIGatewayProxyResponseEvent handleRequest(Vehicle vehicle, Context context) { + System.out.println("input: " + vehicle); + + return new APIGatewayProxyResponseEvent().withStatusCode(200); + } + +} diff --git a/samples/custom-serialization/gson/HelloWorldFunction/src/main/java/helloworld/Vehicle.java b/samples/custom-serialization/gson/HelloWorldFunction/src/main/java/helloworld/Vehicle.java new file mode 100644 index 000000000..ffce611b2 --- /dev/null +++ b/samples/custom-serialization/gson/HelloWorldFunction/src/main/java/helloworld/Vehicle.java @@ -0,0 +1,49 @@ +/* +Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +SPDX-License-Identifier: Apache-2.0 +*/ + +package helloworld; + +import com.google.gson.annotations.SerializedName; + +public class Vehicle { + + @SerializedName("vehicle-type") + private String vehicleType; + + @SerializedName("vehicleID") + private String vehicleId; + + public Vehicle() { + } + + public Vehicle(String vehicleType, String vehicleId) { + this.vehicleType = vehicleType; + this.vehicleId = vehicleId; + } + + public String getVehicleType() { + return vehicleType; + } + + public void setVehicleType(String vehicleType) { + this.vehicleType = vehicleType; + } + + public String getVehicleId() { + return vehicleId; + } + + public void setVehicleId(String vehicleId) { + this.vehicleId = vehicleId; + } + + @Override + public String toString() { + return "Vehicle{" + + "vehicleType='" + vehicleType + '\'' + + ", vehicleId='" + vehicleId + '\'' + + '}'; + } +} diff --git a/samples/custom-serialization/gson/HelloWorldFunction/src/main/resources/META-INF/services/com.amazonaws.services.lambda.runtime.CustomPojoSerializer b/samples/custom-serialization/gson/HelloWorldFunction/src/main/resources/META-INF/services/com.amazonaws.services.lambda.runtime.CustomPojoSerializer new file mode 100644 index 000000000..0a4e281c0 --- /dev/null +++ b/samples/custom-serialization/gson/HelloWorldFunction/src/main/resources/META-INF/services/com.amazonaws.services.lambda.runtime.CustomPojoSerializer @@ -0,0 +1 @@ +com.example.vehicles.serialization.GsonSerializer \ No newline at end of file diff --git a/samples/custom-serialization/gson/README.md b/samples/custom-serialization/gson/README.md new file mode 100644 index 000000000..924c0cfd8 --- /dev/null +++ b/samples/custom-serialization/gson/README.md @@ -0,0 +1,6 @@ +Build and test commands + +```bash +sam build +sam local invoke -e events/event.json +``` \ No newline at end of file diff --git a/samples/custom-serialization/gson/events/event.json b/samples/custom-serialization/gson/events/event.json new file mode 100644 index 000000000..5d882dba3 --- /dev/null +++ b/samples/custom-serialization/gson/events/event.json @@ -0,0 +1,4 @@ +{ + "vehicle-type": "car", + "vehicleID": 123 +} \ No newline at end of file diff --git a/samples/custom-serialization/gson/template.yaml b/samples/custom-serialization/gson/template.yaml new file mode 100644 index 000000000..baf3b075e --- /dev/null +++ b/samples/custom-serialization/gson/template.yaml @@ -0,0 +1,43 @@ +AWSTemplateFormatVersion: '2010-09-09' +Transform: AWS::Serverless-2016-10-31 +Description: > + gson + + Sample SAM Template for gson + +# More info about Globals: https://github.com/awslabs/serverless-application-model/blob/master/docs/globals.rst +Globals: + Function: + Timeout: 20 + MemorySize: 512 + +Resources: + HelloWorldFunction: + Type: AWS::Serverless::Function # More info about Function Resource: https://github.com/awslabs/serverless-application-model/blob/master/versions/2016-10-31.md#awsserverlessfunction + Properties: + CodeUri: HelloWorldFunction + Handler: helloworld.App::handleRequest + Runtime: java21 + Architectures: + - x86_64 + MemorySize: 512 + Events: + HelloWorld: + Type: Api # More info about API Event Source: https://github.com/awslabs/serverless-application-model/blob/master/versions/2016-10-31.md#api + Properties: + Path: /hello + Method: get + +Outputs: + # ServerlessRestApi is an implicit API created out of Events key under Serverless::Function + # Find out more about other implicit resources you can reference within SAM + # https://github.com/awslabs/serverless-application-model/blob/master/docs/internals/generated_resources.rst#api + HelloWorldApi: + Description: "API Gateway endpoint URL for Prod stage for Hello World function" + Value: !Sub "https://${ServerlessRestApi}.execute-api.${AWS::Region}.amazonaws.com/Prod/hello/" + HelloWorldFunction: + Description: "Hello World Lambda Function ARN" + Value: !GetAtt HelloWorldFunction.Arn + HelloWorldFunctionIamRole: + Description: "Implicit IAM Role created for Hello World function" + Value: !GetAtt HelloWorldFunctionRole.Arn diff --git a/samples/custom-serialization/jackson-jr/HelloWorldFunction/build.gradle b/samples/custom-serialization/jackson-jr/HelloWorldFunction/build.gradle new file mode 100644 index 000000000..480abfded --- /dev/null +++ b/samples/custom-serialization/jackson-jr/HelloWorldFunction/build.gradle @@ -0,0 +1,20 @@ +plugins { + id 'java' +} + +repositories { + mavenCentral() +} + +dependencies { + implementation 'com.amazonaws:aws-lambda-java-core:1.2.3' + implementation 'com.amazonaws:aws-lambda-java-events:3.14.0' + implementation 'com.fasterxml.jackson.jr:jackson-jr-objects:2.15.2' + implementation 'com.fasterxml.jackson.jr:jackson-jr-annotation-support:2.15.2' + implementation 'com.fasterxml.jackson.core:jackson-annotations:2.15.2' +} + +java { + sourceCompatibility = JavaVersion.VERSION_21 + targetCompatibility = JavaVersion.VERSION_21 +} diff --git a/samples/custom-serialization/jackson-jr/HelloWorldFunction/src/main/java/com/example/vehicles/serialization/JacksonJRSerializer.java b/samples/custom-serialization/jackson-jr/HelloWorldFunction/src/main/java/com/example/vehicles/serialization/JacksonJRSerializer.java new file mode 100644 index 000000000..1ae1661b1 --- /dev/null +++ b/samples/custom-serialization/jackson-jr/HelloWorldFunction/src/main/java/com/example/vehicles/serialization/JacksonJRSerializer.java @@ -0,0 +1,88 @@ +/* +Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +SPDX-License-Identifier: Apache-2.0 +*/ + +package com.example.vehicles.serialization; + +import com.amazonaws.services.lambda.runtime.CustomPojoSerializer; +import com.fasterxml.jackson.core.JsonFactory; +import com.fasterxml.jackson.core.json.JsonWriteFeature; +import com.fasterxml.jackson.jr.annotationsupport.JacksonAnnotationExtension; +import com.fasterxml.jackson.jr.ob.JSON; +import com.fasterxml.jackson.jr.ob.JSON.Feature; +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.io.UncheckedIOException; +import java.lang.reflect.Type; + +public class JacksonJRSerializer implements CustomPojoSerializer { + + private static final JSON globalJson = createJson(); + + private static final JacksonJRSerializer instance = new JacksonJRSerializer(globalJson); + + private final JSON json; + + private JacksonJRSerializer(JSON json) { + this.json = json; + } + + /** + * ServiceLoader class requires that the single exposed provider type has a default constructor + * to easily instantiate the service providers that it finds + */ + public JacksonJRSerializer() { + this.json = globalJson; + } + + public static JacksonJRSerializer getInstance() { + return instance; + } + + public JSON getJson() { + return json; + } + + private static JSON createJson() { + JSON json = JSON.builder(createJsonFactory()) + .register(JacksonAnnotationExtension.std) + .build(); + + json.with(Feature.FLUSH_AFTER_WRITE_VALUE, false); + + return json; + } + + private static JsonFactory createJsonFactory() { + return JsonFactory.builder().build(); + } + + @Override + public T fromJson(InputStream input, Type type) { + try { + return json.beanFrom((Class) type, input); + } catch (IOException e) { + throw new UncheckedIOException(e); + } + } + + @Override + public T fromJson(String input, Type type) { + try { + return json.beanFrom((Class) type, input); + } catch (IOException e) { + throw new UncheckedIOException(e); + } + } + + @Override + public void toJson(T value, OutputStream output, Type type) { + try { + json.write(value, output); + } catch (IOException e) { + throw new UncheckedIOException(e); + } + } +} diff --git a/samples/custom-serialization/jackson-jr/HelloWorldFunction/src/main/java/helloworld/App.java b/samples/custom-serialization/jackson-jr/HelloWorldFunction/src/main/java/helloworld/App.java new file mode 100644 index 000000000..02ba6048f --- /dev/null +++ b/samples/custom-serialization/jackson-jr/HelloWorldFunction/src/main/java/helloworld/App.java @@ -0,0 +1,23 @@ +/* +Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +SPDX-License-Identifier: Apache-2.0 +*/ + +package helloworld; + +import com.amazonaws.services.lambda.runtime.Context; +import com.amazonaws.services.lambda.runtime.RequestHandler; +import com.amazonaws.services.lambda.runtime.events.APIGatewayProxyResponseEvent; + +/** + * Handler for requests to Lambda function. + */ +public class App implements RequestHandler { + + public APIGatewayProxyResponseEvent handleRequest(Vehicle vehicle, Context context) { + System.out.println("input: " + vehicle); + + return new APIGatewayProxyResponseEvent().withStatusCode(200); + } + +} diff --git a/samples/custom-serialization/jackson-jr/HelloWorldFunction/src/main/java/helloworld/Vehicle.java b/samples/custom-serialization/jackson-jr/HelloWorldFunction/src/main/java/helloworld/Vehicle.java new file mode 100644 index 000000000..f32c503b3 --- /dev/null +++ b/samples/custom-serialization/jackson-jr/HelloWorldFunction/src/main/java/helloworld/Vehicle.java @@ -0,0 +1,49 @@ +/* +Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +SPDX-License-Identifier: Apache-2.0 +*/ + +package helloworld; + +import com.fasterxml.jackson.annotation.JsonProperty; + +public class Vehicle { + + @JsonProperty("vehicle-type") + private String vehicleType; + + @JsonProperty("vehicleID") + private String vehicleId; + + public Vehicle() { + } + + public Vehicle(String vehicleType, String vehicleId) { + this.vehicleType = vehicleType; + this.vehicleId = vehicleId; + } + + public String getVehicleType() { + return vehicleType; + } + + public void setVehicleType(String vehicleType) { + this.vehicleType = vehicleType; + } + + public String getVehicleId() { + return vehicleId; + } + + public void setVehicleId(String vehicleId) { + this.vehicleId = vehicleId; + } + + @Override + public String toString() { + return "Vehicle{" + + "vehicleType='" + vehicleType + '\'' + + ", vehicleId='" + vehicleId + '\'' + + '}'; + } +} diff --git a/samples/custom-serialization/jackson-jr/HelloWorldFunction/src/main/resources/META-INF/services/com.amazonaws.services.lambda.runtime.CustomPojoSerializer b/samples/custom-serialization/jackson-jr/HelloWorldFunction/src/main/resources/META-INF/services/com.amazonaws.services.lambda.runtime.CustomPojoSerializer new file mode 100644 index 000000000..a54949b07 --- /dev/null +++ b/samples/custom-serialization/jackson-jr/HelloWorldFunction/src/main/resources/META-INF/services/com.amazonaws.services.lambda.runtime.CustomPojoSerializer @@ -0,0 +1 @@ +com.example.vehicles.serialization.JacksonJRSerializer \ No newline at end of file diff --git a/samples/custom-serialization/jackson-jr/README.md b/samples/custom-serialization/jackson-jr/README.md new file mode 100644 index 000000000..3f6a2f3a2 --- /dev/null +++ b/samples/custom-serialization/jackson-jr/README.md @@ -0,0 +1,7 @@ +Build and test commands + +```bash +sam build +sam local invoke -e events/event.json +``` + diff --git a/samples/custom-serialization/jackson-jr/events/event.json b/samples/custom-serialization/jackson-jr/events/event.json new file mode 100644 index 000000000..5d882dba3 --- /dev/null +++ b/samples/custom-serialization/jackson-jr/events/event.json @@ -0,0 +1,4 @@ +{ + "vehicle-type": "car", + "vehicleID": 123 +} \ No newline at end of file diff --git a/samples/custom-serialization/jackson-jr/template.yaml b/samples/custom-serialization/jackson-jr/template.yaml new file mode 100644 index 000000000..e3cf91dfc --- /dev/null +++ b/samples/custom-serialization/jackson-jr/template.yaml @@ -0,0 +1,43 @@ +AWSTemplateFormatVersion: '2010-09-09' +Transform: AWS::Serverless-2016-10-31 +Description: > + jackson-jr + + Sample SAM Template for jackson-jr + +# More info about Globals: https://github.com/awslabs/serverless-application-model/blob/master/docs/globals.rst +Globals: + Function: + Timeout: 20 + MemorySize: 512 + +Resources: + HelloWorldFunction: + Type: AWS::Serverless::Function # More info about Function Resource: https://github.com/awslabs/serverless-application-model/blob/master/versions/2016-10-31.md#awsserverlessfunction + Properties: + CodeUri: HelloWorldFunction + Handler: helloworld.App::handleRequest + Runtime: java21 + Architectures: + - x86_64 + MemorySize: 512 + Events: + HelloWorld: + Type: Api # More info about API Event Source: https://github.com/awslabs/serverless-application-model/blob/master/versions/2016-10-31.md#api + Properties: + Path: /hello + Method: get + +Outputs: + # ServerlessRestApi is an implicit API created out of Events key under Serverless::Function + # Find out more about other implicit resources you can reference within SAM + # https://github.com/awslabs/serverless-application-model/blob/master/docs/internals/generated_resources.rst#api + HelloWorldApi: + Description: "API Gateway endpoint URL for Prod stage for Hello World function" + Value: !Sub "https://${ServerlessRestApi}.execute-api.${AWS::Region}.amazonaws.com/Prod/hello/" + HelloWorldFunction: + Description: "Hello World Lambda Function ARN" + Value: !GetAtt HelloWorldFunction.Arn + HelloWorldFunctionIamRole: + Description: "Implicit IAM Role created for Hello World function" + Value: !GetAtt HelloWorldFunctionRole.Arn diff --git a/samples/custom-serialization/moshi/HelloWorldFunction/pom.xml b/samples/custom-serialization/moshi/HelloWorldFunction/pom.xml new file mode 100644 index 000000000..60277f10b --- /dev/null +++ b/samples/custom-serialization/moshi/HelloWorldFunction/pom.xml @@ -0,0 +1,52 @@ + + 4.0.0 + helloworld + HelloWorld + 1.0 + jar + A sample Hello World created for SAM CLI. + + 21 + 21 + + + + + com.amazonaws + aws-lambda-java-core + 1.2.3 + + + com.amazonaws + aws-lambda-java-events + 3.16.0 + + + + com.squareup.moshi + moshi + 1.15.1 + + + + + + + org.apache.maven.plugins + maven-shade-plugin + 3.6.1 + + + + + package + + shade + + + + + + + diff --git a/samples/custom-serialization/moshi/HelloWorldFunction/src/main/java/com/example/vehicles/serialization/MoshiSerializer.java b/samples/custom-serialization/moshi/HelloWorldFunction/src/main/java/com/example/vehicles/serialization/MoshiSerializer.java new file mode 100644 index 000000000..1254b1eec --- /dev/null +++ b/samples/custom-serialization/moshi/HelloWorldFunction/src/main/java/com/example/vehicles/serialization/MoshiSerializer.java @@ -0,0 +1,74 @@ +/* +Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +SPDX-License-Identifier: Apache-2.0 +*/ + +package com.example.vehicles.serialization; + +import com.amazonaws.services.lambda.runtime.CustomPojoSerializer; +import com.squareup.moshi.JsonAdapter; +import com.squareup.moshi.Moshi; +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.io.UncheckedIOException; +import java.lang.reflect.Type; +import okio.BufferedSink; +import okio.Okio; + +public class MoshiSerializer implements CustomPojoSerializer { + + private static final Moshi globalMoshi = createMoshi(); + + private final Moshi moshi; + + /** + * ServiceLoader class requires that the single exposed provider type has a + * default constructor + * to easily instantiate the service providers that it finds + */ + public MoshiSerializer() { + this.moshi = globalMoshi; + } + + private static Moshi createMoshi() { + return new Moshi.Builder().build(); + } + + @Override + public T fromJson(InputStream input, Type type) { + JsonAdapter jsonAdapter = moshi.adapter(type); + try { + return jsonAdapter.fromJson(Okio.buffer(Okio.source(input))); + } catch (IOException e) { + throw new UncheckedIOException(e); + } + } + + @Override + public T fromJson(String input, Type type) { + JsonAdapter jsonAdapter = moshi.adapter(type); + try { + return jsonAdapter.fromJson(input); + } catch (IOException e) { + throw new UncheckedIOException(e); + } + } + + @Override + public void toJson(T value, OutputStream output, Type type) { + JsonAdapter jsonAdapter = moshi.adapter(type); + BufferedSink out = Okio.buffer(Okio.sink(output)); + try { + jsonAdapter.toJson(out, value); + } catch (IOException e) { + e.printStackTrace(); + } finally { + try { + out.flush(); + } catch (IOException e) { + e.printStackTrace(); + } + } + } +} diff --git a/samples/custom-serialization/moshi/HelloWorldFunction/src/main/java/helloworld/App.java b/samples/custom-serialization/moshi/HelloWorldFunction/src/main/java/helloworld/App.java new file mode 100644 index 000000000..02ba6048f --- /dev/null +++ b/samples/custom-serialization/moshi/HelloWorldFunction/src/main/java/helloworld/App.java @@ -0,0 +1,23 @@ +/* +Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +SPDX-License-Identifier: Apache-2.0 +*/ + +package helloworld; + +import com.amazonaws.services.lambda.runtime.Context; +import com.amazonaws.services.lambda.runtime.RequestHandler; +import com.amazonaws.services.lambda.runtime.events.APIGatewayProxyResponseEvent; + +/** + * Handler for requests to Lambda function. + */ +public class App implements RequestHandler { + + public APIGatewayProxyResponseEvent handleRequest(Vehicle vehicle, Context context) { + System.out.println("input: " + vehicle); + + return new APIGatewayProxyResponseEvent().withStatusCode(200); + } + +} diff --git a/samples/custom-serialization/moshi/HelloWorldFunction/src/main/java/helloworld/Vehicle.java b/samples/custom-serialization/moshi/HelloWorldFunction/src/main/java/helloworld/Vehicle.java new file mode 100644 index 000000000..0087ee2cf --- /dev/null +++ b/samples/custom-serialization/moshi/HelloWorldFunction/src/main/java/helloworld/Vehicle.java @@ -0,0 +1,49 @@ +/* +Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +SPDX-License-Identifier: Apache-2.0 +*/ + +package helloworld; + +import com.squareup.moshi.Json; + +public class Vehicle { + + @Json(name = "vehicle-type") + private String vehicleType; + + @Json(name = "vehicleID") + private String vehicleId; + + public Vehicle() { + } + + public Vehicle(String vehicleType, String vehicleId) { + this.vehicleType = vehicleType; + this.vehicleId = vehicleId; + } + + public String getVehicleType() { + return vehicleType; + } + + public void setVehicleType(String vehicleType) { + this.vehicleType = vehicleType; + } + + public String getVehicleId() { + return vehicleId; + } + + public void setVehicleId(String vehicleId) { + this.vehicleId = vehicleId; + } + + @Override + public String toString() { + return "Vehicle{" + + "vehicleType='" + vehicleType + '\'' + + ", vehicleId='" + vehicleId + '\'' + + '}'; + } +} diff --git a/samples/custom-serialization/moshi/HelloWorldFunction/src/main/resources/META-INF/services/com.amazonaws.services.lambda.runtime.CustomPojoSerializer b/samples/custom-serialization/moshi/HelloWorldFunction/src/main/resources/META-INF/services/com.amazonaws.services.lambda.runtime.CustomPojoSerializer new file mode 100644 index 000000000..8f07647e8 --- /dev/null +++ b/samples/custom-serialization/moshi/HelloWorldFunction/src/main/resources/META-INF/services/com.amazonaws.services.lambda.runtime.CustomPojoSerializer @@ -0,0 +1 @@ +com.example.vehicles.serialization.MoshiSerializer \ No newline at end of file diff --git a/samples/custom-serialization/moshi/README.md b/samples/custom-serialization/moshi/README.md new file mode 100644 index 000000000..3f6a2f3a2 --- /dev/null +++ b/samples/custom-serialization/moshi/README.md @@ -0,0 +1,7 @@ +Build and test commands + +```bash +sam build +sam local invoke -e events/event.json +``` + diff --git a/samples/custom-serialization/moshi/events/event.json b/samples/custom-serialization/moshi/events/event.json new file mode 100644 index 000000000..5d882dba3 --- /dev/null +++ b/samples/custom-serialization/moshi/events/event.json @@ -0,0 +1,4 @@ +{ + "vehicle-type": "car", + "vehicleID": 123 +} \ No newline at end of file diff --git a/samples/custom-serialization/moshi/template.yaml b/samples/custom-serialization/moshi/template.yaml new file mode 100644 index 000000000..8d2b95365 --- /dev/null +++ b/samples/custom-serialization/moshi/template.yaml @@ -0,0 +1,43 @@ +AWSTemplateFormatVersion: '2010-09-09' +Transform: AWS::Serverless-2016-10-31 +Description: > + moshi + + Sample SAM Template for moshi + +# More info about Globals: https://github.com/awslabs/serverless-application-model/blob/master/docs/globals.rst +Globals: + Function: + Timeout: 20 + MemorySize: 512 + +Resources: + HelloWorldFunction: + Type: AWS::Serverless::Function # More info about Function Resource: https://github.com/awslabs/serverless-application-model/blob/master/versions/2016-10-31.md#awsserverlessfunction + Properties: + CodeUri: HelloWorldFunction + Handler: helloworld.App::handleRequest + Runtime: java21 + Architectures: + - x86_64 + MemorySize: 512 + Events: + HelloWorld: + Type: Api # More info about API Event Source: https://github.com/awslabs/serverless-application-model/blob/master/versions/2016-10-31.md#api + Properties: + Path: /hello + Method: get + +Outputs: + # ServerlessRestApi is an implicit API created out of Events key under Serverless::Function + # Find out more about other implicit resources you can reference within SAM + # https://github.com/awslabs/serverless-application-model/blob/master/docs/internals/generated_resources.rst#api + HelloWorldApi: + Description: "API Gateway endpoint URL for Prod stage for Hello World function" + Value: !Sub "https://${ServerlessRestApi}.execute-api.${AWS::Region}.amazonaws.com/Prod/hello/" + HelloWorldFunction: + Description: "Hello World Lambda Function ARN" + Value: !GetAtt HelloWorldFunction.Arn + HelloWorldFunctionIamRole: + Description: "Implicit IAM Role created for Hello World function" + Value: !GetAtt HelloWorldFunctionRole.Arn diff --git a/samples/custom-serialization/request-stream-handler/HelloWorldFunction/pom.xml b/samples/custom-serialization/request-stream-handler/HelloWorldFunction/pom.xml new file mode 100644 index 000000000..15e16439d --- /dev/null +++ b/samples/custom-serialization/request-stream-handler/HelloWorldFunction/pom.xml @@ -0,0 +1,51 @@ + + 4.0.0 + helloworld + HelloWorld + 1.0 + jar + A sample Hello World created for SAM CLI. + + 17 + 17 + + + + + com.amazonaws + aws-lambda-java-core + 1.2.3 + + + com.amazonaws + aws-lambda-java-events + 3.16.0 + + + com.google.code.gson + gson + 2.10.1 + + + + + + + org.apache.maven.plugins + maven-shade-plugin + 3.6.1 + + + + + package + + shade + + + + + + + diff --git a/samples/custom-serialization/request-stream-handler/HelloWorldFunction/src/main/java/helloworld/App.java b/samples/custom-serialization/request-stream-handler/HelloWorldFunction/src/main/java/helloworld/App.java new file mode 100644 index 000000000..645fe8f5e --- /dev/null +++ b/samples/custom-serialization/request-stream-handler/HelloWorldFunction/src/main/java/helloworld/App.java @@ -0,0 +1,46 @@ +/* +Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +SPDX-License-Identifier: Apache-2.0 +*/ + +package helloworld; + +import com.amazonaws.services.lambda.runtime.Context; +import com.amazonaws.services.lambda.runtime.RequestStreamHandler; +import com.amazonaws.services.lambda.runtime.events.APIGatewayProxyResponseEvent; +import com.google.gson.Gson; +import com.google.gson.GsonBuilder; +import com.google.gson.JsonSyntaxException; +import java.io.BufferedReader; +import java.io.BufferedWriter; +import java.io.IOException; +import java.io.InputStream; +import java.io.InputStreamReader; +import java.io.OutputStream; +import java.io.OutputStreamWriter; +import java.io.PrintWriter; +import java.nio.charset.Charset; +import java.nio.charset.StandardCharsets; + +/** + * Handler for requests to Lambda function. + */ + +public class App implements RequestStreamHandler { + private static final Charset usAscii = StandardCharsets.US_ASCII; + private final Gson gson = new GsonBuilder().setPrettyPrinting().create(); + + @Override + public void handleRequest(InputStream inputStream, OutputStream outputStream, Context context) throws IOException { + try ( + BufferedReader reader = new BufferedReader(new InputStreamReader(inputStream, usAscii)); + PrintWriter writer = new PrintWriter(new BufferedWriter(new OutputStreamWriter(outputStream, usAscii))) + ) { + Vehicle vehicle = gson.fromJson(reader, Vehicle.class); System.out.println("input: " + vehicle); + APIGatewayProxyResponseEvent responseEvent = new APIGatewayProxyResponseEvent().withStatusCode(200); + writer.write(gson.toJson(responseEvent)); + } catch (IllegalStateException | JsonSyntaxException exception) { + exception.printStackTrace(); + } + } +} diff --git a/samples/custom-serialization/request-stream-handler/HelloWorldFunction/src/main/java/helloworld/Vehicle.java b/samples/custom-serialization/request-stream-handler/HelloWorldFunction/src/main/java/helloworld/Vehicle.java new file mode 100644 index 000000000..ffce611b2 --- /dev/null +++ b/samples/custom-serialization/request-stream-handler/HelloWorldFunction/src/main/java/helloworld/Vehicle.java @@ -0,0 +1,49 @@ +/* +Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +SPDX-License-Identifier: Apache-2.0 +*/ + +package helloworld; + +import com.google.gson.annotations.SerializedName; + +public class Vehicle { + + @SerializedName("vehicle-type") + private String vehicleType; + + @SerializedName("vehicleID") + private String vehicleId; + + public Vehicle() { + } + + public Vehicle(String vehicleType, String vehicleId) { + this.vehicleType = vehicleType; + this.vehicleId = vehicleId; + } + + public String getVehicleType() { + return vehicleType; + } + + public void setVehicleType(String vehicleType) { + this.vehicleType = vehicleType; + } + + public String getVehicleId() { + return vehicleId; + } + + public void setVehicleId(String vehicleId) { + this.vehicleId = vehicleId; + } + + @Override + public String toString() { + return "Vehicle{" + + "vehicleType='" + vehicleType + '\'' + + ", vehicleId='" + vehicleId + '\'' + + '}'; + } +} diff --git a/samples/custom-serialization/request-stream-handler/README.md b/samples/custom-serialization/request-stream-handler/README.md new file mode 100644 index 000000000..924c0cfd8 --- /dev/null +++ b/samples/custom-serialization/request-stream-handler/README.md @@ -0,0 +1,6 @@ +Build and test commands + +```bash +sam build +sam local invoke -e events/event.json +``` \ No newline at end of file diff --git a/samples/custom-serialization/request-stream-handler/events/event.json b/samples/custom-serialization/request-stream-handler/events/event.json new file mode 100644 index 000000000..5d882dba3 --- /dev/null +++ b/samples/custom-serialization/request-stream-handler/events/event.json @@ -0,0 +1,4 @@ +{ + "vehicle-type": "car", + "vehicleID": 123 +} \ No newline at end of file diff --git a/samples/custom-serialization/request-stream-handler/template.yaml b/samples/custom-serialization/request-stream-handler/template.yaml new file mode 100644 index 000000000..b1ba37890 --- /dev/null +++ b/samples/custom-serialization/request-stream-handler/template.yaml @@ -0,0 +1,43 @@ +AWSTemplateFormatVersion: '2010-09-09' +Transform: AWS::Serverless-2016-10-31 +Description: > + request-stream-handler + + Sample SAM Template for request-stream-handler + +# More info about Globals: https://github.com/awslabs/serverless-application-model/blob/master/docs/globals.rst +Globals: + Function: + Timeout: 20 + MemorySize: 512 + +Resources: + HelloWorldFunction: + Type: AWS::Serverless::Function # More info about Function Resource: https://github.com/awslabs/serverless-application-model/blob/master/versions/2016-10-31.md#awsserverlessfunction + Properties: + CodeUri: HelloWorldFunction + Handler: helloworld.App::handleRequest + Runtime: java21 + Architectures: + - x86_64 + MemorySize: 512 + Events: + HelloWorld: + Type: Api # More info about API Event Source: https://github.com/awslabs/serverless-application-model/blob/master/versions/2016-10-31.md#api + Properties: + Path: /hello + Method: get + +Outputs: + # ServerlessRestApi is an implicit API created out of Events key under Serverless::Function + # Find out more about other implicit resources you can reference within SAM + # https://github.com/awslabs/serverless-application-model/blob/master/docs/internals/generated_resources.rst#api + HelloWorldApi: + Description: "API Gateway endpoint URL for Prod stage for Hello World function" + Value: !Sub "https://${ServerlessRestApi}.execute-api.${AWS::Region}.amazonaws.com/Prod/hello/" + HelloWorldFunction: + Description: "Hello World Lambda Function ARN" + Value: !GetAtt HelloWorldFunction.Arn + HelloWorldFunctionIamRole: + Description: "Implicit IAM Role created for Hello World function" + Value: !GetAtt HelloWorldFunctionRole.Arn diff --git a/samples/kinesis-firehose-event-handler/pom.xml b/samples/kinesis-firehose-event-handler/pom.xml index b4a949ba0..fbd93b64f 100644 --- a/samples/kinesis-firehose-event-handler/pom.xml +++ b/samples/kinesis-firehose-event-handler/pom.xml @@ -41,18 +41,18 @@ com.amazonaws aws-lambda-java-core - 1.2.1 + 1.2.3 com.amazonaws aws-lambda-java-events - 3.11.0 + 3.16.0 org.junit.jupiter junit-jupiter - RELEASE + 5.9.2 test diff --git a/samples/msk-firehose-event-handler/src/main/java/example/MSKFirehoseEventHandler.java b/samples/msk-firehose-event-handler/src/main/java/example/MSKFirehoseEventHandler.java new file mode 100644 index 000000000..f5e513496 --- /dev/null +++ b/samples/msk-firehose-event-handler/src/main/java/example/MSKFirehoseEventHandler.java @@ -0,0 +1,39 @@ +/* +Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +SPDX-License-Identifier: Apache-2.0 +*/ + +package example; + +import com.amazonaws.services.lambda.runtime.Context; +import com.amazonaws.services.lambda.runtime.RequestHandler; +import com.amazonaws.services.lambda.runtime.events.MSKFirehoseResponse; +import com.amazonaws.services.lambda.runtime.events.MSKFirehoseEvent; +import org.json.JSONObject; + +import java.nio.ByteBuffer; +import java.util.ArrayList; +import java.util.List; + +/** + * A sample MSKFirehoseEvent handler + * For more information see the developer guide - ... + */ +public class MSKFirehoseEventHandler implements RequestHandler { + + @Override + public MSKFirehoseResponse handleRequest(MSKFirehoseEvent MSKFirehoseEvent, Context context) { + List records = new ArrayList<>(); + + for (MSKFirehoseEvent.Record record : MSKFirehoseEvent.getRecords()) { + String recordData = new String(record.getKafkaRecordValue().array()); + // Your business logic + JSONObject jsonObject = new JSONObject(recordData); + records.add(new MSKFirehoseResponse.Record(record.getRecordId(), MSKFirehoseResponse.Result.Ok, encode(jsonObject.toString()))); + } + return new MSKFirehoseResponse(records); + } + private ByteBuffer encode(String content) { + return ByteBuffer.wrap(content.getBytes()); + } +} diff --git a/samples/msk-firehose-event-handler/src/test/java/example/MSKFirehoseEventHandlerTest.java b/samples/msk-firehose-event-handler/src/test/java/example/MSKFirehoseEventHandlerTest.java new file mode 100644 index 000000000..77223e516 --- /dev/null +++ b/samples/msk-firehose-event-handler/src/test/java/example/MSKFirehoseEventHandlerTest.java @@ -0,0 +1,32 @@ +/* +Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +SPDX-License-Identifier: Apache-2.0 +*/ + +package example; + +import com.amazonaws.services.lambda.runtime.Context; +import com.amazonaws.services.lambda.runtime.tests.annotations.Event; +import com.amazonaws.services.lambda.runtime.events.MSKFirehoseEvent; +import com.amazonaws.services.lambda.runtime.events.MSKFirehoseResponse; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.params.ParameterizedTest; + +import static java.nio.charset.StandardCharsets.UTF_8; + +public class MSKFirehoseEventHandlerTest { + + private Context context; // intentionally null as it's not used in the test + + @ParameterizedTest + @Event(value = "event.json", type = MSKFirehoseEvent.class) + public void testEventHandler(MSKFirehoseEvent event) { + MSKFirehoseEventHandler Sample = new MSKFirehoseEventHandler(); + MSKFirehoseResponse response = Sample.handleRequest(event, context); + + String expectedString = "{\"Name\":\"Hello World\"}"; + MSKFirehoseResponse.Record firstRecord = response.getRecords().get(0); + Assertions.assertEquals(expectedString, UTF_8.decode(firstRecord.getKafkaRecordValue()).toString()); + Assertions.assertEquals(MSKFirehoseResponse.Result.Ok, firstRecord.getResult()); + } +} diff --git a/samples/msk-firehose-event-handler/src/test/resources/event.json b/samples/msk-firehose-event-handler/src/test/resources/event.json new file mode 100644 index 000000000..91c4b4203 --- /dev/null +++ b/samples/msk-firehose-event-handler/src/test/resources/event.json @@ -0,0 +1,18 @@ +{ + "invocationId": "12345621-4787-0000-a418-36e56Example", + "sourceMSKArn": "", + "deliveryStreamArn": "", + "region": "us-east-1", + "records": [ + { + "recordId": "00000000000000000000000000000000000000000000000000000000000000", + "approximateArrivalTimestamp": 1716369573887, + "mskRecordMetadata": { + "offset": "0", + "partitionId": "1", + "approximateArrivalTimestamp": 1716369573887 + }, + "kafkaRecordValue": "eyJOYW1lIjoiSGVsbG8gV29ybGQifQ==" + } + ] +}