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 0b553bbc3..b1bed919f 100644 --- a/.github/workflows/aws-lambda-java-core.yml +++ b/.github/workflows/aws-lambda-java-core.yml @@ -7,11 +7,15 @@ on: push: 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,9 +23,9 @@ jobs: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v5 - name: Set up JDK 1.8 - uses: actions/setup-java@v3 + uses: actions/setup-java@v4 with: java-version: 8 distribution: corretto @@ -38,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 679639d34..1f1f08870 100644 --- a/.github/workflows/aws-lambda-java-events-sdk-transformer.yml +++ b/.github/workflows/aws-lambda-java-events-sdk-transformer.yml @@ -7,11 +7,15 @@ on: push: 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,9 +23,9 @@ jobs: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v5 - name: Set up JDK 1.8 - uses: actions/setup-java@v3 + uses: actions/setup-java@v4 with: java-version: 8 distribution: corretto diff --git a/.github/workflows/aws-lambda-java-events.yml b/.github/workflows/aws-lambda-java-events.yml index bdd01eb7f..2d101018d 100644 --- a/.github/workflows/aws-lambda-java-events.yml +++ b/.github/workflows/aws-lambda-java-events.yml @@ -7,11 +7,15 @@ on: push: 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,9 +23,9 @@ jobs: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v5 - name: Set up JDK 1.8 - uses: actions/setup-java@v3 + uses: actions/setup-java@v4 with: java-version: 8 distribution: corretto diff --git a/.github/workflows/aws-lambda-java-log4j2.yml b/.github/workflows/aws-lambda-java-log4j2.yml index 427c7536b..e9f6a56c1 100644 --- a/.github/workflows/aws-lambda-java-log4j2.yml +++ b/.github/workflows/aws-lambda-java-log4j2.yml @@ -7,11 +7,15 @@ on: push: 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,9 +23,9 @@ jobs: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v5 - name: Set up JDK 1.8 - uses: actions/setup-java@v3 + uses: actions/setup-java@v4 with: java-version: 8 distribution: corretto 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-serialization.yml b/.github/workflows/aws-lambda-java-serialization.yml index 9d71cbabf..13b7e08b0 100644 --- a/.github/workflows/aws-lambda-java-serialization.yml +++ b/.github/workflows/aws-lambda-java-serialization.yml @@ -7,11 +7,15 @@ on: push: 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,9 +23,9 @@ jobs: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v5 - name: Set up JDK 1.8 - uses: actions/setup-java@v3 + uses: actions/setup-java@v4 with: java-version: 8 distribution: corretto diff --git a/.github/workflows/aws-lambda-java-tests.yml b/.github/workflows/aws-lambda-java-tests.yml index fc587e8e4..720c52c11 100644 --- a/.github/workflows/aws-lambda-java-tests.yml +++ b/.github/workflows/aws-lambda-java-tests.yml @@ -7,11 +7,15 @@ on: push: 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,9 +23,9 @@ jobs: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v5 - name: Set up JDK 1.8 - uses: actions/setup-java@v3 + uses: actions/setup-java@v4 with: java-version: 8 distribution: corretto diff --git a/.github/workflows/repo-sync.yml b/.github/workflows/repo-sync.yml index 91054f9ef..2d97bc868 100644 --- a/.github/workflows/repo-sync.yml +++ b/.github/workflows/repo-sync.yml @@ -3,8 +3,16 @@ 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 @@ -12,7 +20,7 @@ jobs: env: IS_CONFIGURED: ${{ secrets.SOURCE_REPO != '' }} steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v5 if: ${{ env.IS_CONFIGURED == 'true' }} - uses: repo-sync/github-sync@v2 name: Sync repo to branch diff --git a/.github/workflows/runtime-interface-client_merge_to_main.yml b/.github/workflows/runtime-interface-client_merge_to_main.yml index 1783f1cde..3560207f3 100644 --- a/.github/workflows/runtime-interface-client_merge_to_main.yml +++ b/.github/workflows/runtime-interface-client_merge_to_main.yml @@ -15,6 +15,8 @@ on: branches: [ main ] paths: - 'aws-lambda-java-runtime-interface-client/**' + - '.github/workflows/runtime-interface-client_*.yml' + workflow_dispatch: jobs: @@ -26,31 +28,40 @@ jobs: contents: read steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v5 - name: Set up JDK 1.8 - uses: actions/setup-java@v3 + uses: actions/setup-java@v4 with: java-version: 8 distribution: corretto - name: Set up QEMU - uses: docker/setup-qemu-action@v2 + uses: docker/setup-qemu-action@v3 - name: Set up Docker Buildx - uses: docker/setup-buildx-action@v2 + 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 - uses: aws-actions/configure-aws-credentials@v1 + 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 }} @@ -58,6 +69,9 @@ jobs: 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 @@ -68,7 +82,14 @@ jobs: 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 index 7a7f602e9..dcad4fa0a 100644 --- a/.github/workflows/runtime-interface-client_pr.yml +++ b/.github/workflows/runtime-interface-client_pr.yml @@ -7,53 +7,83 @@ on: pull_request: branches: [ '*' ] paths: - - 'aws-lambda-java-runtime-interface-client/**' + - '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@v3 + - uses: actions/checkout@v5 - name: Set up JDK 1.8 - uses: actions/setup-java@v3 + 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@v3 + - uses: actions/checkout@v5 - name: Set up JDK 1.8 - uses: actions/setup-java@v3 + uses: actions/setup-java@v4 with: java-version: 8 distribution: corretto - name: Set up QEMU - uses: docker/setup-qemu-action@v2 + uses: docker/setup-qemu-action@v3 - name: Set up Docker Buildx - uses: docker/setup-buildx-action@v2 + 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@v3 + 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 2171ae785..aebb708a7 100644 --- a/.github/workflows/samples.yml +++ b/.github/workflows/samples.yml @@ -7,21 +7,23 @@ on: push: 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@v3 + - uses: actions/checkout@v5 - name: Set up JDK 1.8 - uses: actions/setup-java@v3 + uses: actions/setup-java@v4 with: java-version: 8 distribution: corretto @@ -29,6 +31,9 @@ jobs: # 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 @@ -36,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 2f2f0af47..1adf36493 100644 --- a/.gitignore +++ b/.gitignore @@ -24,4 +24,15 @@ dependency-reduced-pom.xml .project # OSX -.DS_Store \ No newline at end of file +.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 27af4eafc..b6c67b9e8 100644 --- a/README.md +++ b/README.md @@ -10,6 +10,8 @@ For information on how to optimize your functions watch the re:Invent talk [Opti ## 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. @@ -21,7 +23,7 @@ Example request handler public class Handler implements RequestHandler, String>{ @Override public String handleRequest(Map event, Context context) { - + } } ``` @@ -32,7 +34,7 @@ Example request stream handler public class HandlerStream implements RequestStreamHandler { @Override public void handleRequest(InputStream inputStream, OutputStream outputStream, Context context) throws IOException { - + } } ``` @@ -41,12 +43,14 @@ public class HandlerStream implements RequestStreamHandler { com.amazonaws aws-lambda-java-core - 1.2.3 + 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. @@ -71,12 +75,14 @@ public class SqsHandler implements RequestHandler { com.amazonaws aws-lambda-java-events - 3.11.3 + 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) @@ -100,8 +106,10 @@ public void testInjectSQSEvent(SQSEvent event) { ## aws-lambda-java-events-sdk-transformer +[![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) + 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. +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. - [Release Notes](aws-lambda-java-events-sdk-transformer/RELEASE.CHANGELOG.md) @@ -116,6 +124,8 @@ See the [documentation](aws-lambda-java-events-sdk-transformer/README.md) for mo ## Java Lambda Log4J2 support - aws-lambda-java-log4j2 +[![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) + 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. @@ -125,11 +135,24 @@ See the [README](aws-lambda-java-log4j2/README.md) or the [official documentatio com.amazonaws aws-lambda-java-log4j2 - 1.5.1 + 1.6.0 ``` +## Lambda Profiler Extension for Java - aws-lambda-java-profiler + +

+ A flame graph of a Java Lambda function +

+ +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. + ## 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. @@ -140,12 +163,14 @@ The purpose of this package is to allow developers to deploy their applications com.amazonaws aws-lambda-java-runtime-interface-client - 2.4.0 + 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. - [Release Notes](aws-lambda-java-serialization/RELEASE.CHANGELOG.md) @@ -154,7 +179,7 @@ This package defines the Lambda serialization logic using in the `aws-lambda-jav com.amazonaws aws-lambda-java-serialization - 1.0.1 + 1.1.5 ``` diff --git a/aws-lambda-java-core/RELEASE.CHANGELOG.md b/aws-lambda-java-core/RELEASE.CHANGELOG.md index ebd0566ff..aebc8ecd9 100644 --- a/aws-lambda-java-core/RELEASE.CHANGELOG.md +++ b/aws-lambda-java-core/RELEASE.CHANGELOG.md @@ -1,3 +1,11 @@ +### 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 diff --git a/aws-lambda-java-core/pom.xml b/aws-lambda-java-core/pom.xml index 0dd848a96..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.3 + 1.4.0 jar AWS Lambda Java Core Library @@ -36,13 +36,6 @@ 1.8 - - - sonatype-nexus-staging - https://oss.sonatype.org/service/local/staging/deploy/maven2/ - - - dev @@ -115,14 +108,12 @@ - org.sonatype.plugins - nexus-staging-maven-plugin - 1.6.3 + org.sonatype.central + central-publishing-maven-plugin + 0.8.0 true - sonatype-nexus-staging - https://aws.oss.sonatype.org/ - false + central diff --git a/aws-lambda-java-core/src/main/java/com/amazonaws/services/lambda/runtime/Context.java b/aws-lambda-java-core/src/main/java/com/amazonaws/services/lambda/runtime/Context.java index 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-events-sdk-transformer/pom.xml b/aws-lambda-java-events-sdk-transformer/pom.xml index 6a2b1735c..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.1.0 + 3.1.1 jar AWS Lambda Java Events SDK Transformer Library @@ -63,7 +63,7 @@ com.amazonaws aws-lambda-java-events - 3.11.2 + 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/README.md b/aws-lambda-java-events/README.md index 02c070826..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` @@ -69,7 +74,7 @@ com.amazonaws aws-lambda-java-events - 3.11.3 + 3.16.0 ... diff --git a/aws-lambda-java-events/RELEASE.CHANGELOG.md b/aws-lambda-java-events/RELEASE.CHANGELOG.md index 3b29f1e08..a4bcd10a0 100644 --- a/aws-lambda-java-events/RELEASE.CHANGELOG.md +++ b/aws-lambda-java-events/RELEASE.CHANGELOG.md @@ -1,3 +1,39 @@ +### 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 diff --git a/aws-lambda-java-events/pom.xml b/aws-lambda-java-events/pom.xml index 59cf05101..714c825d9 100644 --- a/aws-lambda-java-events/pom.xml +++ b/aws-lambda-java-events/pom.xml @@ -1,11 +1,11 @@ + xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/maven-v4_0_0.xsd"> 4.0.0 com.amazonaws aws-lambda-java-events - 3.11.3 + 3.16.1 jar AWS Lambda Java Events Library @@ -35,6 +35,10 @@ 1.8 1.8 1.18.22 + UTF-8 + UTF-8 + 2.20.1 + 2.40.1 @@ -60,13 +64,13 @@ com.fasterxml.jackson.core jackson-databind - 2.14.2 + ${jackson.version} test net.javacrumbs.json-unit json-unit-assertj - 2.36.1 + ${json.unit} test @@ -150,20 +154,18 @@ - org.sonatype.plugins - nexus-staging-maven-plugin - 1.6.3 + org.sonatype.central + central-publishing-maven-plugin + 0.8.0 true - sonatype-nexus-staging - https://aws.oss.sonatype.org/ - false + central org.apache.maven.plugins maven-resources-plugin - 3.2.0 + 3.3.1 UTF-8 @@ -171,7 +173,7 @@ org.apache.maven.plugins maven-compiler-plugin - 3.8.1 + 3.11.0 @@ -187,4 +189,4 @@ - + \ No newline at end of file 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/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/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 3ccb07698..f13121750 100644 --- a/aws-lambda-java-log4j2/README.md +++ b/aws-lambda-java-log4j2/README.md @@ -10,7 +10,7 @@ Example for Maven pom.xml com.amazonaws aws-lambda-java-log4j2 - 1.5.1 + 1.6.0 org.apache.logging.log4j @@ -22,6 +22,11 @@ Example for Maven pom.xml log4j-api 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.1' + 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 2277b7921..49535d388 100644 --- a/aws-lambda-java-log4j2/RELEASE.CHANGELOG.md +++ b/aws-lambda-java-log4j2/RELEASE.CHANGELOG.md @@ -1,3 +1,7 @@ +### 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` diff --git a/aws-lambda-java-log4j2/pom.xml b/aws-lambda-java-log4j2/pom.xml index a1ea80e52..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.1 + 1.6.1 jar AWS Lambda Java Log4j 2.x Libraries @@ -34,7 +34,7 @@ 1.8 1.8 - 2.17.1 + 2.25.3 @@ -48,7 +48,7 @@ com.amazonaws aws-lambda-java-core - 1.2.2 + 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/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 e57daf3a8..6c3a268fb 100644 --- a/aws-lambda-java-runtime-interface-client/Makefile +++ b/aws-lambda-java-runtime-interface-client/Makefile @@ -4,6 +4,13 @@ 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 @@ -15,7 +22,7 @@ target: .PHONY: test test: - mvn test + mvn test $(EXTRA_LOAD_ARG) .PHONY: setup-codebuild-agent setup-codebuild-agent: @@ -44,11 +51,11 @@ pr: test test-smoke .PHONY: build build: - mvn clean install - mvn install -P linux-x86_64 - mvn install -P linux_musl-x86_64 - mvn install -P linux-aarch64 - mvn install -P linux_musl-aarch64 + 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: @@ -58,6 +65,10 @@ publish: test-publish: ./ric-dev-environment/test-platform-specific-jar-snapshot.sh +.PHONY: test-rie +test-rie: + ./scripts/test-rie.sh "EchoHandler::handleRequest" + define HELP_MESSAGE Usage: $ make [TARGETS] @@ -67,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 c9194c0c9..4e03f041f 100644 --- a/aws-lambda-java-runtime-interface-client/README.md +++ b/aws-lambda-java-runtime-interface-client/README.md @@ -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.4.1 + 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: @@ -160,7 +203,7 @@ platform-specific JAR by setting the ``. com.amazonaws aws-lambda-java-runtime-interface-client - 2.4.1 + 2.8.7 linux-x86_64 ``` diff --git a/aws-lambda-java-runtime-interface-client/RELEASE.CHANGELOG.md b/aws-lambda-java-runtime-interface-client/RELEASE.CHANGELOG.md index 09a85363b..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,57 @@ +### 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)) 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 fe299bdd1..ab7166c84 100644 --- a/aws-lambda-java-runtime-interface-client/pom.xml +++ b/aws-lambda-java-runtime-interface-client/pom.xml @@ -1,10 +1,10 @@ + xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd"> 4.0.0 com.amazonaws aws-lambda-java-runtime-interface-client - 2.4.1 + 2.9.0 jar AWS Lambda Java Runtime Interface Client @@ -34,8 +34,11 @@ UTF-8 UTF-8 - 0.8.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.3 + 1.4.0 com.amazonaws aws-lambda-java-serialization - 1.1.2 + 1.2.0 + + + software.amazon.awssdk + utils-lite + 2.34.0 - org.junit.jupiter junit-jupiter-engine @@ -79,6 +91,18 @@ 4.11.0 test + + org.mockito + mockito-junit-jupiter + 4.11.0 + test + + + com.squareup.okhttp3 + mockwebserver + 4.12.0 + test + @@ -90,9 +114,22 @@ + + 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 @@ -110,6 +147,24 @@ maven-antrun-plugin 1.7 + + build-jni-lib-for-tests + generate-test-sources + + run + + + + + + + + + + + + build-jni-lib prepare-package @@ -119,11 +174,11 @@ - - - - + failonerror="true" logError="true"> + + + + @@ -163,6 +218,24 @@ 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 @@ -186,12 +259,12 @@ - PACKAGE + BUNDLE LINE COVEREDRATIO - 0 + 0.5 @@ -200,6 +273,26 @@ + + org.apache.maven.plugins + maven-checkstyle-plugin + ${maven-checkstyle-plugin.version} + + build-tools/checkstyle.xml + true + true + true + + + + validate + validate + + check + + + + @@ -276,16 +369,52 @@ - org.sonatype.plugins - nexus-staging-maven-plugin - 1.6.3 + org.sonatype.central + central-publishing-maven-plugin + 0.8.0 true - sonatype-nexus-staging - https://aws.oss.sonatype.org/ - false + central + + org.codehaus.mojo + build-helper-maven-plugin + 3.4.0 + + + attach-platform-artifacts + package + + attach-artifact + + + + + ${project.build.directory}/${project.build.finalName}-linux-x86_64.jar + jar + linux-x86_64 + + + ${project.build.directory}/${project.build.finalName}-linux-aarch_64.jar + jar + linux-aarch_64 + + + ${project.build.directory}/${project.build.finalName}-linux_musl-x86_64.jar + jar + linux_musl-x86_64 + + + ${project.build.directory}/${project.build.finalName}-linux_musl-aarch_64.jar + jar + linux_musl-aarch_64 + + + + + + diff --git a/aws-lambda-java-runtime-interface-client/ric-dev-environment/publish_snapshot.sh b/aws-lambda-java-runtime-interface-client/ric-dev-environment/publish_snapshot.sh index cf5969e1f..9d2f9837f 100755 --- 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 @@ -18,6 +18,10 @@ 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 @@ -36,7 +40,7 @@ mvn -B -X -P ci-repo \ -DgroupId=com.amazonaws \ -DartifactId=aws-lambda-java-runtime-interface-client \ -Dpackaging=jar \ - -Dversion=$projectVersion \ + -Dversion=$snapshotProjectVersion \ -Dfile=./target/aws-lambda-java-runtime-interface-client-$projectVersion.jar \ -Dfiles=$FILES \ -Dclassifiers=$CLASSIFIERS \ diff --git a/aws-lambda-java-runtime-interface-client/scripts/test-rie.sh b/aws-lambda-java-runtime-interface-client/scripts/test-rie.sh new file mode 100755 index 000000000..b69c967a1 --- /dev/null +++ b/aws-lambda-java-runtime-interface-client/scripts/test-rie.sh @@ -0,0 +1,46 @@ +#!/bin/bash +set -euo pipefail + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +PROJECT_ROOT="$(dirname "$SCRIPT_DIR")" +SERIALIZATION_ROOT="$(dirname "$PROJECT_ROOT")/aws-lambda-java-serialization" + +if ! ls "$PROJECT_ROOT"/target/aws-lambda-java-runtime-interface-client-*.jar >/dev/null 2>&1; then + echo "RIC jar not found. Please build the project first with 'mvn package'." + exit 1 +fi + +IMAGE_TAG="java-ric-rie-test" + +HANDLER="${1:-EchoHandler::handleRequest}" + +echo "Starting RIE test setup for Java..." + +# Build local dependencies if not present +CORE_ROOT="$(dirname "$PROJECT_ROOT")/aws-lambda-java-core" +if ! ls "$PROJECT_ROOT"/target/aws-lambda-java-core-*.jar >/dev/null 2>&1; then + echo "Building local aws-lambda-java-core..." + (cd "$CORE_ROOT" && mvn package -DskipTests) + cp "$CORE_ROOT"/target/aws-lambda-java-core-*.jar "$PROJECT_ROOT/target/" +fi + +if ! ls "$PROJECT_ROOT"/target/aws-lambda-java-serialization-*.jar >/dev/null 2>&1; then + echo "Building local aws-lambda-java-serialization..." + (cd "$SERIALIZATION_ROOT" && mvn package -DskipTests) + cp "$SERIALIZATION_ROOT"/target/aws-lambda-java-serialization-*.jar "$PROJECT_ROOT/target/" +fi + +echo "Compiling EchoHandler..." +javac -source 21 -target 21 -cp "$(ls "$PROJECT_ROOT"/target/aws-lambda-java-runtime-interface-client-*.jar):$(ls "$PROJECT_ROOT"/target/aws-lambda-java-core-*.jar):$(ls "$PROJECT_ROOT"/target/aws-lambda-java-serialization-*.jar)" \ + -d "$PROJECT_ROOT/test-handlers/" "$PROJECT_ROOT/test-handlers/EchoHandler.java" + +echo "Building test Docker image..." +docker build -t "$IMAGE_TAG" -f "$PROJECT_ROOT/Dockerfile.rie" "$PROJECT_ROOT" + +echo "Starting test container on port 9000..." +echo "" +echo "In another terminal, invoke with:" +echo "curl -s -X POST -H 'Content-Type: application/json' \"http://localhost:9000/2015-03-31/functions/function/invocations\" -d '{\"message\":\"test\"}'" +echo "" + +exec docker run -it -p 9000:8080 -e _HANDLER="$HANDLER" "$IMAGE_TAG" \ No newline at end of file diff --git a/aws-lambda-java-runtime-interface-client/src/main/java/com/amazonaws/services/lambda/crac/CheckpointException.java b/aws-lambda-java-runtime-interface-client/src/main/java/com/amazonaws/services/lambda/crac/CheckpointException.java index 8793bfcba..f802ad5f7 100644 --- 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 @@ -1,12 +1,12 @@ /* - * Copyright 2023 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.crac; public class CheckpointException extends Exception { private static final long serialVersionUID = -4956873658083157585L; public CheckpointException() { - super(); } } 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 index 2fc22cdd6..d62ef0143 100644 --- 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 @@ -1,6 +1,7 @@ /* - * Copyright 2023 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.crac; 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 index ac4bc6480..04b1436a8 100644 --- 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 @@ -1,6 +1,7 @@ /* - * Copyright 2023 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.crac; @@ -23,25 +24,9 @@ public class ContextImpl extends Context { @Override public synchronized void beforeCheckpoint(Context context) 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; - } + executeBeforeCheckpointHooks(); + DNSManager.clearCache(); + System.gc(); } @Override @@ -73,18 +58,39 @@ public synchronized void register(Resource resource) { } private List getCheckpointQueueReverseOrderOfRegistration() { - return checkpointQueue.entrySet() - .stream() - .sorted((r1, r2) -> (int) (r2.getValue() - r1.getValue())) - .map(Map.Entry::getKey) - .collect(Collectors.toList()); + 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()); + 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 index b6a7d9e98..7e0b24a2d 100644 --- 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 @@ -1,6 +1,7 @@ /* - * Copyright 2023 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.crac; @@ -9,11 +10,11 @@ */ public final class Core { + private static Context globalContext = new ContextImpl(); + private Core() { } - private static Context globalContext = new ContextImpl(); - public static Context getGlobalContext() { return globalContext; } 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 index 8c8d91c37..7ef933202 100644 --- 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 @@ -1,6 +1,7 @@ /* - * Copyright 2023 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.crac; 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 index d045a0bad..cef38e00f 100644 --- 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 @@ -1,6 +1,7 @@ /* - * Copyright 2023 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.crac; @@ -8,6 +9,5 @@ public class RestoreException extends Exception { private static final long serialVersionUID = -823900409868237860L; public RestoreException() { - super(); } } 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 1424de0cc..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,8 +1,8 @@ -// -// 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; @@ -12,29 +12,35 @@ 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.runtimeapi.InvocationRequest; -import com.amazonaws.services.lambda.runtime.api.client.runtimeapi.LambdaRuntimeClient; +import com.amazonaws.services.lambda.runtime.api.client.runtimeapi.LambdaError; +import com.amazonaws.services.lambda.runtime.api.client.runtimeapi.LambdaRuntimeApiClient; +import com.amazonaws.services.lambda.runtime.api.client.runtimeapi.LambdaRuntimeApiClientImpl; +import com.amazonaws.services.lambda.runtime.api.client.runtimeapi.LambdaRuntimeClientMaxRetriesExceededException; +import com.amazonaws.services.lambda.runtime.api.client.runtimeapi.RapidErrorType; +import com.amazonaws.services.lambda.runtime.api.client.runtimeapi.converters.LambdaErrorConverter; +import com.amazonaws.services.lambda.runtime.api.client.runtimeapi.converters.XRayErrorCauseConverter; +import com.amazonaws.services.lambda.runtime.api.client.runtimeapi.dto.InvocationRequest; +import com.amazonaws.services.lambda.runtime.api.client.util.ConcurrencyConfig; import com.amazonaws.services.lambda.runtime.api.client.util.LambdaOutputStream; import com.amazonaws.services.lambda.runtime.api.client.util.UnsafeUtil; import com.amazonaws.services.lambda.runtime.logging.LogFormat; 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 com.amazonaws.services.lambda.runtime.serialization.factories.JacksonFactory; import com.amazonaws.services.lambda.runtime.serialization.util.ReflectUtil; - 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.Constructor; import java.net.URLClassLoader; import java.security.Security; import java.util.Properties; - +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; +import java.util.concurrent.TimeUnit; +import software.amazon.awssdk.utilslite.SdkInternalThreadLocal; /** * The entrypoint of this class is {@link AWSLambda#startRuntime}. It performs two main tasks: @@ -49,6 +55,8 @@ */ public class AWSLambda { + 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"; @@ -67,6 +75,8 @@ public class AWSLambda { 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. @@ -133,7 +143,41 @@ private static LambdaRequestHandler findRequestHandler(final String handlerStrin return requestHandler; } - public static void setupRuntimeLogger(LambdaLogger lambdaLogger) + private static LambdaRequestHandler getLambdaRequestHandlerObject(String handler, LambdaContextLogger lambdaLogger, LambdaRuntimeApiClient runtimeClient) throws ClassNotFoundException, IOException { + UnsafeUtil.disableIllegalAccessWarning(); + + System.setOut(new PrintStream(new LambdaOutputStream(System.out), false, "UTF-8")); + System.setErr(new PrintStream(new LambdaOutputStream(System.err), false, "UTF-8")); + setupRuntimeLogger(lambdaLogger); + + 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 { + 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); + } + + if (INIT_TYPE_SNAP_START.equals(AWS_LAMBDA_INITIALIZATION_TYPE)) { + onInitComplete(lambdaLogger, runtimeClient); + } + + return requestHandler; + } + + private static void setupRuntimeLogger(LambdaLogger lambdaLogger) throws ClassNotFoundException { ReflectUtil.setStaticField( Class.forName("com.amazonaws.services.lambda.runtime.LambdaRuntime"), @@ -143,17 +187,6 @@ 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); - } - return value; - } - - protected static URLClassLoader customerClassLoader; - /** * convert an integer into a FileDescriptor object using reflection to access private members. */ @@ -183,154 +216,161 @@ private static LogSink createLogSink() { } } - public static void main(String[] args) { - startRuntime(args[0]); - } - - private static void startRuntime(String handler) { - try (LogSink logSink = createLogSink()) { - LambdaLogger logger = new LambdaContextLogger( - logSink, - LogLevel.fromString(LambdaEnvironment.LAMBDA_LOG_LEVEL), - LogFormat.fromString(LambdaEnvironment.LAMBDA_LOG_FORMAT) - ); - startRuntime(handler, logger); - } 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); + } + }); + } - 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); + 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); + } + } - // Load the user's handler - LambdaRequestHandler requestHandler; - try { - requestHandler = findRequestHandler(handler, customerClassLoader); - } catch (UserFault userFault) { - lambdaLogger.log(userFault.reportableError()); - reportInitError(new Failure(userFault), runtimeClient); - 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); } - if (INIT_TYPE_SNAP_START.equals(AWS_LAMBDA_INITIALIZATION_TYPE)) { - onInitComplete(runtimeClient, lambdaLogger); + } + + private static void setEnvVarForXrayTraceId(InvocationRequest request) { + if (request.getXrayTraceId() != null) { + System.setProperty(LAMBDA_TRACE_HEADER_PROP, request.getXrayTraceId()); + } else { + System.clearProperty(LAMBDA_TRACE_HEADER_PROP); } + } + + private static void reportNonLoopTerminatingException(LambdaContextLogger lambdaLogger, Throwable t) { + lambdaLogger.log( + String.format( + "Runtime Loop on Thread ID: %s Faced and Exception. This exception will not stop the runtime loop.\nException:\n%s", + Thread.currentThread().getName(), UserFault.trace(t)), + lambdaLogger.getLogFormat() == LogFormat.JSON ? LogLevel.ERROR : LogLevel.UNDEFINED); + } + + /* + * In multiconcurrent mode (exitLoopOnErrors = false), The Runtime Loop will not exit unless LambdaRuntimeClientMaxRetriesExceededException is thrown when calling nextInvocationWithExponentialBackoff. + * In normal/sequential mode (exitLoopOnErrors = true), The Runtime Loop will exit if nextInvocation call fails, when UserFault is fatal, or an Error of type VirtualMachineError or IOError is thrown. + */ + private static void startRuntimeLoop(LambdaRequestHandler lambdaRequestHandler, LambdaContextLogger lambdaLogger, LambdaRuntimeApiClient runtimeClient, boolean exitLoopOnErrors) throws Exception { boolean shouldExit = false; while (!shouldExit) { - UserFault userFault = null; - InvocationRequest request = runtimeClient.waitForNextInvocation(); - if (request.getXrayTraceId() != null) { - System.setProperty(LAMBDA_TRACE_HEADER_PROP, request.getXrayTraceId()); - } else { - System.clearProperty(LAMBDA_TRACE_HEADER_PROP); - } - - ByteArrayOutputStream payload; try { - payload = requestHandler.call(request); - runtimeClient.postInvocationResponse(request.getId(), payload.toByteArray()); - boolean ignored = Thread.interrupted(); // clear interrupted flag in case if it was set by user's code - } 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()); + UserFault userFault = null; + InvocationRequest request = exitLoopOnErrors ? runtimeClient.nextInvocation() : runtimeClient.nextInvocationWithExponentialBackoff(lambdaLogger); + if (exitLoopOnErrors) { + setEnvVarForXrayTraceId(request); + } else { + SdkInternalThreadLocal.put(CONCURRENT_TRACE_ID_KEY, request.getXrayTraceId()); + } + + try { + ByteArrayOutputStream payload = lambdaRequestHandler.call(request); + runtimeClient.reportInvocationSuccess(request.getId(), payload.toByteArray()); + // clear interrupted flag in case if it was set by user's code + Thread.interrupted(); + } catch (Throwable t) { + UserFault.filterStackTrace(t); + userFault = UserFault.makeUserFault(t); + shouldExit = exitLoopOnErrors && (t instanceof VirtualMachineError || t instanceof IOError || userFault.fatal); + LambdaError error = createLambdaErrorFromThrowableOrUserFault(t); + runtimeClient.reportInvocationError(request.getId(), error); + } finally { + if (userFault != null) { + lambdaLogger.log(userFault.reportableError(), lambdaLogger.getLogFormat() == LogFormat.JSON ? LogLevel.ERROR : LogLevel.UNDEFINED); + } + + SdkInternalThreadLocal.remove(CONCURRENT_TRACE_ID_KEY); + } } catch (Throwable t) { - 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)); - } finally { - if (userFault != null) { - lambdaLogger.log(userFault.reportableError()); + if (exitLoopOnErrors || t instanceof LambdaRuntimeClientMaxRetriesExceededException) { + throw t; } + + reportNonLoopTerminatingException(lambdaLogger, t); } } } - static void onInitComplete(final LambdaRuntimeClient runtimeClient, final LambdaLogger lambdaLogger) throws IOException { + private static void onInitComplete(final LambdaContextLogger lambdaLogger, LambdaRuntimeApiClient runtimeClient) throws IOException { try { Core.getGlobalContext().beforeCheckpoint(null); - // Blocking call to RAPID /restore/next API, will return after taking snapshot. - // This will also be the 'entrypoint' when resuming from snapshots. - runtimeClient.getRestoreNext(); + runtimeClient.restoreNext(); } catch (Exception e1) { logExceptionCloudWatch(lambdaLogger, e1); - reportInitError(new Failure(e1), runtimeClient); + runtimeClient.reportInitError(new LambdaError( + LambdaErrorConverter.fromThrowable(e1), + RapidErrorType.BeforeCheckpointError)); System.exit(64); } + try { Core.getGlobalContext().afterRestore(null); } catch (Exception restoreExc) { logExceptionCloudWatch(lambdaLogger, restoreExc); - Failure errorPayload = new Failure(restoreExc); - reportRestoreError(errorPayload, runtimeClient); + runtimeClient.reportRestoreError(new LambdaError( + LambdaErrorConverter.fromThrowable(restoreExc), + RapidErrorType.AfterRestoreError)); System.exit(64); } } - private static void logExceptionCloudWatch(LambdaLogger lambdaLogger, Exception exc) { + private static void logExceptionCloudWatch(LambdaContextLogger lambdaLogger, Exception exc) { UserFault.filterStackTrace(exc); UserFault userFault = UserFault.makeUserFault(exc, true); - lambdaLogger.log(userFault.reportableError()); - } - - static void reportInitError(final Failure failure, - final LambdaRuntimeClient runtimeClient) throws IOException { - - ByteArrayOutputStream payload = new ByteArrayOutputStream(1024); - JacksonFactory.getInstance().getSerializer(Failure.class).toJson(failure, payload); - runtimeClient.postInitError(payload.toByteArray(), failure.getErrorType()); - } - - static int reportRestoreError(final Failure failure, - final LambdaRuntimeClient runtimeClient) throws IOException { - - ByteArrayOutputStream payload = new ByteArrayOutputStream(1024); - JacksonFactory.getInstance().getSerializer(Failure.class).toJson(failure, payload); - return runtimeClient.postRestoreError(payload.toByteArray(), failure.getErrorType()); + lambdaLogger.log(userFault.reportableError(), lambdaLogger.getLogFormat() == LogFormat.JSON ? LogLevel.ERROR : LogLevel.UNDEFINED); } - private static PojoSerializer xRayErrorCauseSerializer; - - /** - * @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; - } + 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 8e72ea4a9..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; @@ -35,6 +38,7 @@ 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()); } 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 ee554c148..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,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; @@ -24,7 +27,8 @@ class CustomerClassLoader extends URLClassLoader { @Override public boolean accept(File dir, String name) { int offset = name.length() - 4; - if (offset <= 0) { /* must be at least A.jar */ + // must be at least A.jar + if (offset <= 0) { return false; } else { return name.startsWith(".jar", offset); 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 9e3c48ebb..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,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,7 +16,7 @@ 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.InvocationRequest; +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; @@ -21,7 +24,7 @@ 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 java.io.ByteArrayInputStream; import java.io.ByteArrayOutputStream; import java.io.IOException; import java.io.InputStream; @@ -41,8 +44,9 @@ import java.util.Map; import java.util.Objects; import java.util.Optional; - -import static com.amazonaws.services.lambda.runtime.api.client.UserFault.*; +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; public final class EventHandlerLoader { private static final byte[] _JsonNull = new byte[]{'n', 'u', 'l', 'l'}; @@ -53,7 +57,41 @@ 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() { } @@ -78,7 +116,7 @@ private static PojoSerializer getSerializer(Platform platform, Type type if (type instanceof Class) { Class clazz = ((Class) type); if (LambdaEventSerializers.isLambdaSupportedEvent(clazz.getName())) { - return LambdaEventSerializers.serializerFor(clazz, AWSLambda.customerClassLoader); + return LambdaEventSerializers.serializerFor(clazz, AWSLambda.getCustomerClassLoader()); } } // else platform dependent (Android uses GSON but all other platforms use Jackson) @@ -89,10 +127,11 @@ private static PojoSerializer getSerializer(Platform platform, Type type } private static PojoSerializer getSerializerCached(Platform platform, Type type) { - Map> cache = typeCache.get(platform); + EnumMap>> threadTypeCache = typeCache.get(); + Map> cache = threadTypeCache.get(platform); if (cache == null) { cache = new HashMap<>(); - typeCache.put(platform, cache); + threadTypeCache.put(platform, cache); } PojoSerializer serializer = cache.get(type); @@ -104,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(); } @@ -151,760 +187,735 @@ private static boolean isVoid(Type 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()); - } - - if (outputType.isPresent()) { - getSerializerCached(Platform.UNKNOWN, outputType.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; + } - @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); - } + 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); } } /** - * Wraps a java.lang.reflect.Method as a POJO RequestHandler + * perform a breadth-first search for the first parameterized type for iface + * + * @return null of no type found. Otherwise the type found. */ - 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); - } + 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(); - 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)); + 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()); + } } - 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)); + 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; + } - @Override - public Object handleRequest(Object input, Context context) { - final Object[] args = new Object[argSize]; - int idx = 0; - if (pType != null) { - args[idx++] = input; - } + @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")); + } + if (ptypes.length != 2) { + return new UserFaultHandler(makeUserFault( + "Invalid class signature for RequestHandler. Expected two generic types, got " + ptypes.length)); + } - if (this.needsContext) { - args[idx++] = context; - } + 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; + } + } + } - 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)); + if (!foundBound) { + return new UserFaultHandler(makeUserFault("Class " + clazz.getName() + + " does not implement RequestHandler with concrete type parameters: parameter " + + t + " has no upper bound.")); } - } catch (Throwable t) { - throw UnsafeUtil.throwException(filterStackTrace(t)); } } - } - /** - * 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; + final Type pType = ptypes[0]; + final Type rType = ptypes[1]; - 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); + 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 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)); + private static LambdaRequestHandler wrapRequestStreamHandlerClass(final Class clazz) { + final Constructor constructor; + try { + constructor = getConstructor(clazz); + return wrapRequestStreamHandler(newInstance(constructor)); + } catch (UserFault f) { + return new UserFaultHandler(f); + } catch (Throwable e) { + return new UserFaultHandler(makeUserFault(e)); + } + } - return new StreamMethodRequestHandler(m, instance, needsInput, needsOutput, needsContext); + private static LambdaRequestHandler loadStreamingRequestHandler(Class clazz) { + if (RequestStreamHandler.class.isAssignableFrom(clazz)) { + return wrapRequestStreamHandlerClass(clazz.asSubclass(RequestStreamHandler.class)); + } else if (RequestHandler.class.isAssignableFrom(clazz)) { + return wrapRequestHandlerClass(clazz.asSubclass(RequestHandler.class)); + } else { + return new UserFaultHandler(makeUserFault("Class does not implement an appropriate handler interface: " + + clazz.getName())); } + } - 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)); - } + public static LambdaRequestHandler loadEventHandler(HandlerInfo handlerInfo) { + if (handlerInfo.methodName == null) { + return loadStreamingRequestHandler(handlerInfo.clazz); + } else { + return loadEventPojoHandler(handlerInfo); } + } - @Override - public void handleRequest(InputStream inputStream, OutputStream outputStream, Context context) - throws IOException { - final Object[] args = new Object[argSize]; - int idx = 0; + private static Optional getOneLengthHandler( + Class clazz, + Method m, + Type pType, + Type rType + ) { + if (InputStream.class.equals(pType)) { + return Optional.of(StreamMethodRequestHandler.makeRequestHandler(clazz, m, true, false, false)); + } else if (OutputStream.class.equals(pType)) { + return Optional.of(StreamMethodRequestHandler.makeRequestHandler(clazz, m, false, true, false)); + } else if (isContext(pType)) { + return Optional.of(PojoMethodRequestHandler.makeRequestHandler(clazz, m, null, rType, true)); + } else { + return Optional.of(PojoMethodRequestHandler.makeRequestHandler(clazz, m, pType, rType, false)); + } + } - if (needsInput) { - args[idx++] = inputStream; + private static Optional getTwoLengthHandler( + Class clazz, + Method m, + Type pType1, + Type pType2, + Type rType + ) { + if (OutputStream.class.equals(pType1)) { + if (isContext(pType2)) { + return Optional.of(StreamMethodRequestHandler.makeRequestHandler(clazz, m, false, true, true)); } else { - inputStream.close(); - } - - if (needsOutput) { - args[idx++] = outputStream; - } - - if (needsContext) { - args[idx++] = context; + System.err.println( + "Ignoring two-argument overload because first argument type is OutputStream and second argument type is not Context"); + return Optional.empty(); } - - 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)); + } else if (isContext(pType1)) { + System.err.println("Ignoring two-argument overload because first argument type is Context"); + return Optional.empty(); + } else if (InputStream.class.equals(pType1)) { + if (OutputStream.class.equals(pType2)) { + return Optional.of(StreamMethodRequestHandler.makeRequestHandler(clazz, m, true, true, false)); + } else if (isContext(pType2)) { + return Optional.of(StreamMethodRequestHandler.makeRequestHandler(clazz, m, true, false, true)); + } else { + System.err.println("Ignoring two-argument overload because second parameter type, " + + ReflectUtil.getRawClass(pType2).getName() + + ", is not OutputStream."); + return Optional.empty(); } + } else if (isContext(pType2)) { + return Optional.of(PojoMethodRequestHandler.makeRequestHandler(clazz, m, pType1, rType, true)); + } else { + System.err.println("Ignoring two-argument overload because second parameter type is not Context"); + return Optional.empty(); } } - 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); - } + private static Optional getThreeLengthHandler( + Class clazz, + Method m, + Type pType1, + Type pType2, + Type pType3, + Type rType + ) { + if (InputStream.class.equals(pType1) && OutputStream.class.equals(pType2) && isContext(pType3)) { + return Optional.of(StreamMethodRequestHandler.makeRequestHandler(clazz, m, true, true, true)); + } else { + System.err.println( + "Ignoring three-argument overload because argument signature is not (InputStream, OutputStream, Context"); + return Optional.empty(); } - return constructor; } - 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); + private static Optional getHandlerFromOverload(Class clazz, Method m) { + final Type rType = m.getGenericReturnType(); + final Type[] pTypes = m.getGenericParameterTypes(); + + if (pTypes.length == 0) { + return Optional.of(PojoMethodRequestHandler.makeRequestHandler(clazz, m, null, rType, false)); + } else if (pTypes.length == 1) { + return getOneLengthHandler(clazz, m, pTypes[0], rType); + } else if (pTypes.length == 2) { + return getTwoLengthHandler(clazz, m, pTypes[0], pTypes[1], rType); + } else if (pTypes.length == 3) { + return getThreeLengthHandler(clazz, m, pTypes[0], pTypes[1], pTypes[2], rType); + } else { + System.err.println("Ignoring an overload of method " + + m.getName() + + " because it has too many parameters: Expected at most 3, got " + + pTypes.length); + return Optional.empty(); } } - private static final class ClassContext { - public final Class clazz; - public final Type[] actualTypeArguments; + private static boolean isContext(Type t) { + return Context.class.equals(t); + } - @SuppressWarnings({"rawtypes"}) - private TypeVariable[] typeParameters; + /** + * Returns true if the last type in params is a lambda context object interface (Context). + */ + private static boolean lastParameterIsContext(Class[] params) { + return params.length != 0 && isContext(params[params.length - 1]); + } - public ClassContext(Class clazz, Type[] actualTypeArguments) { - this.clazz = clazz; - this.actualTypeArguments = actualTypeArguments; + /** + * Implement a comparator for Methods. We sort overloaded handler methods using this comparator, and then pick the + * lowest sorted method. + */ + + 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)); } - @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]); - } + /* + * 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 + * + */ - this.clazz = clazz; - this.actualTypeArguments = types; + 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; } } - @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); - } - } + final int end = methods.length - slide; + Arrays.sort(methods, 0, end, methodPriority); - Type t = type.getRawType(); - if (t instanceof Class) { - this.clazz = (Class) t; - } else if (t instanceof TypeVariable) { - this.clazz = (Class) ((TypeVariable) t).getGenericDeclaration(); + for (int i = 0; i < end; i++) { + Method m = methods[i]; + Optional result = getHandlerFromOverload(handlerInfo.clazz, m); + if (result.isPresent()) { + return result.get(); } else { - throw new RuntimeException("Type " + t + " is of unexpected type " + t.getClass()); + continue; } - 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; - } + return new UserFaultHandler(makeUserFault("No public method named " + + handlerInfo.methodName + + " with appropriate method signature found on class " + + handlerInfo.clazz.getName())); + } - @SuppressWarnings({"rawtypes"}) - private TypeVariable[] getTypeParameters() { - if (typeParameters == null) { - typeParameters = clazz.getTypeParameters(); - } - return typeParameters; - } + @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) + )); } - /** - * 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(); + private static LambdaRequestHandler wrapRequestStreamHandler(final RequestStreamHandler handler) { + return new LambdaRequestHandler() { + private final ThreadLocal outputBuffers = ThreadLocal.withInitial(() -> new ByteArrayOutputStream(1024)); + private ThreadLocal> log4jContextPutMethod = new ThreadLocal<>(); - 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()); + 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 } } - 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)); + /** + * 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); + } } - } - return null; - } + public ByteArrayOutputStream call(InvocationRequest request) throws Error, Exception { + ByteArrayOutputStream output = outputBuffers.get(); + output.reset(); - @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")); - } - if (ptypes.length != 2) { - return new UserFaultHandler(makeUserFault( - "Invalid class signature for RequestHandler. Expected two generic types, got " + ptypes.length)); - } + LambdaCognitoIdentity cognitoIdentity = null; + if (request.getCognitoIdentity() != null && !request.getCognitoIdentity().isEmpty()) { + cognitoIdentity = getCognitoSerializer().fromJson(request.getCognitoIdentity()); + } - 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; - } - } + 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()); } - if (!foundBound) { - return new UserFaultHandler(makeUserFault("Class " + clazz.getName() - + " does not implement RequestHandler with concrete type parameters: parameter " - + t + " has no upper bound.")); + 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; } - } + }; + } - final Type pType = ptypes[0]; - final Type rType = ptypes[1]; + /** + * 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 { - 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 RequestHandler innerHandler; + public final Optional inputType; + public final Optional outputType; - private static LambdaRequestHandler wrapRequestStreamHandlerClass(final Class clazz) { - final Constructor constructor; - try { - constructor = getConstructor(clazz); - return wrapRequestStreamHandler(newInstance(constructor)); - } catch (UserFault f) { - return new UserFaultHandler(f); - } catch (Throwable e) { - return new UserFaultHandler(makeUserFault(e)); - } - } + public PojoHandlerAsStreamHandler( + RequestHandler innerHandler, + Optional inputType, + Optional outputType + ) { + this.innerHandler = innerHandler; + this.inputType = inputType; + this.outputType = outputType; - private static LambdaRequestHandler loadStreamingRequestHandler(Class clazz) { - if (RequestStreamHandler.class.isAssignableFrom(clazz)) { - return wrapRequestStreamHandlerClass(clazz.asSubclass(RequestStreamHandler.class)); - } else if (RequestHandler.class.isAssignableFrom(clazz)) { - return wrapRequestHandlerClass(clazz.asSubclass(RequestHandler.class)); - } else { - return new UserFaultHandler(makeUserFault("Class does not implement an appropriate handler interface: " - + clazz.getName())); - } - } - public static LambdaRequestHandler loadEventHandler(HandlerInfo handlerInfo) { - if (handlerInfo.methodName == null) { - return loadStreamingRequestHandler(handlerInfo.clazz); - } else { - return loadEventPojoHandler(handlerInfo); - } - } + if (inputType.isPresent()) { + getSerializerCached(Platform.UNKNOWN, inputType.get()); + } - private static Optional getOneLengthHandler( - Class clazz, - Method m, - Type pType, - Type rType - ) { - if (InputStream.class.equals(pType)) { - return Optional.of(StreamMethodRequestHandler.makeRequestHandler(clazz, m, true, false, false)); - } else if (OutputStream.class.equals(pType)) { - return Optional.of(StreamMethodRequestHandler.makeRequestHandler(clazz, m, false, true, false)); - } else if (isContext(pType)) { - return Optional.of(PojoMethodRequestHandler.makeRequestHandler(clazz, m, null, rType, true)); - } else { - return Optional.of(PojoMethodRequestHandler.makeRequestHandler(clazz, m, pType, rType, false)); + if (outputType.isPresent()) { + getSerializerCached(Platform.UNKNOWN, outputType.get()); + } } - } - private static Optional getTwoLengthHandler( - Class clazz, - Method m, - Type pType1, - Type pType2, - Type rType - ) { - if (OutputStream.class.equals(pType1)) { - if (isContext(pType2)) { - return Optional.of(StreamMethodRequestHandler.makeRequestHandler(clazz, m, false, true, true)); - } else { - System.err.println( - "Ignoring two-argument overload because first argument type is OutputStream and second argument type is not Context"); - return Optional.empty(); + @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)); } - } else if (isContext(pType1)) { - System.err.println("Ignoring two-argument overload because first argument type is Context"); - return Optional.empty(); - } else if (InputStream.class.equals(pType1)) { - if (OutputStream.class.equals(pType2)) { - return Optional.of(StreamMethodRequestHandler.makeRequestHandler(clazz, m, true, true, false)); - } else if (isContext(pType2)) { - return Optional.of(StreamMethodRequestHandler.makeRequestHandler(clazz, m, true, false, true)); - } else { - System.err.println("Ignoring two-argument overload because second parameter type, " - + ReflectUtil.getRawClass(pType2).getName() - + ", is not OutputStream."); - return Optional.empty(); + + 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); } - } else if (isContext(pType2)) { - return Optional.of(PojoMethodRequestHandler.makeRequestHandler(clazz, m, pType1, rType, true)); - } else { - System.err.println("Ignoring two-argument overload because second parameter type is not Context"); - return Optional.empty(); } } - private static Optional getThreeLengthHandler( - Class clazz, - Method m, - Type pType1, - Type pType2, - Type pType3, - Type rType - ) { - if (InputStream.class.equals(pType1) && OutputStream.class.equals(pType2) && isContext(pType3)) { - return Optional.of(StreamMethodRequestHandler.makeRequestHandler(clazz, m, true, true, true)); - } else { - System.err.println( - "Ignoring three-argument overload because argument signature is not (InputStream, OutputStream, Context"); - return Optional.empty(); + /** + * 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); } - } - private static Optional getHandlerFromOverload(Class clazz, Method m) { - final Type rType = m.getGenericReturnType(); - final Type[] pTypes = m.getGenericParameterTypes(); + 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)); + } - if (pTypes.length == 0) { - return Optional.of(PojoMethodRequestHandler.makeRequestHandler(clazz, m, null, rType, false)); - } else if (pTypes.length == 1) { - return getOneLengthHandler(clazz, m, pTypes[0], rType); - } else if (pTypes.length == 2) { - return getTwoLengthHandler(clazz, m, pTypes[0], pTypes[1], rType); - } else if (pTypes.length == 3) { - return getThreeLengthHandler(clazz, m, pTypes[0], pTypes[1], pTypes[2], rType); - } else { - System.err.println("Ignoring an overload of method " - + m.getName() - + " because it has too many parameters: Expected at most 3, got " - + pTypes.length); - return Optional.empty(); + return new PojoMethodRequestHandler(m, pType, rType, instance, needsContext); } - } - private static boolean isContext(Type t) { - return Context.class.equals(t); - } + 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)); + } + } - /** - * Returns true if the last type in params is a lambda context object interface (Context). - */ - private static boolean lastParameterIsContext(Class[] params) { - return params.length != 0 && isContext(params[params.length - 1]); - } + @Override + public Object handleRequest(Object input, Context context) { + final Object[] args = new Object[argSize]; + int idx = 0; - /** - * 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) { + if (pType != null) { + args[idx++] = input; + } - //1. Non bridge methods are preferred over bridge methods. - if (!lhs.isBridge() && rhs.isBridge()) { - return -1; - } else if (!rhs.isBridge() && lhs.isBridge()) { - return 1; + if (this.needsContext) { + args[idx++] = context; } - //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(); + 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)); + } + } + } - int lParamCompareLength = lParams.length; - int rParamCompareLength = rParams.length; + /** + * 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; - if (lastParameterIsContext(lParams)) { - ++lParamCompareLength; - } + 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); + } - if (lastParameterIsContext(rParams)) { - ++rParamCompareLength; + 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 -Integer.compare(lParamCompareLength, rParamCompareLength); + return new StreamMethodRequestHandler(m, instance, needsInput, needsOutput, needsContext); } - }; - 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)); + 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)); + } } - /* - * 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 - * - */ + @Override + public void handleRequest(InputStream inputStream, OutputStream outputStream, Context context) + throws IOException { + final Object[] args = new Object[argSize]; + int idx = 0; - int slide = 0; + if (needsInput) { + args[idx++] = inputStream; + } else { + inputStream.close(); + } - for (int i = 0; i < methods.length; i++) { - Method m = methods[i]; - methods[i - slide] = m; - if (!m.getName().equals(handlerInfo.methodName)) { - slide++; - continue; + if (needsOutput) { + args[idx++] = outputStream; } - } - final int end = methods.length - slide; - Arrays.sort(methods, 0, end, methodPriority); + if (needsContext) { + args[idx++] = context; + } - 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; + 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 new UserFaultHandler(makeUserFault("No public method named " - + handlerInfo.methodName - + " with appropriate method signature found on class " - + handlerInfo.clazz.getName())); } - @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 final class ClassContext { + public final Class clazz; + public final Type[] actualTypeArguments; - private static LambdaRequestHandler wrapRequestStreamHandler(final RequestStreamHandler handler) { - return new LambdaRequestHandler() { - private final ByteArrayOutputStream output = new ByteArrayOutputStream(1024); - private Functions.V2 log4jContextPutMethod = null; + @SuppressWarnings({"rawtypes"}) + private TypeVariable[] typeParameters; - 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) { + 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; } + } - /** - * 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); + @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); } } - public ByteArrayOutputStream call(InvocationRequest request) throws Error, Exception { - output.reset(); - - LambdaCognitoIdentity cognitoIdentity = null; - if (request.getCognitoIdentity() != null && !request.getCognitoIdentity().isEmpty()) { - cognitoIdentity = getCognitoSerializer().fromJson(request.getCognitoIdentity()); - } + 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; + } - 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()); + @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]; } + } - 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 - ); - - 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 == 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."); - } - } + return t; + } - handler.handleRequest(request.getContentAsStream(), output, context); - return output; + @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 c445bafef..000000000 --- a/aws-lambda-java-runtime-interface-client/src/main/java/com/amazonaws/services/lambda/runtime/api/client/Failure.java +++ /dev/null @@ -1,88 +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; - } - - public String getErrorMessage() { - return errorMessage; - } -} 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 8f95ba857..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,15 +1,16 @@ -/* 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) { this.clazz = clazz; this.methodName = 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 af3578501..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,10 +1,19 @@ -/* 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.*; +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 { @@ -16,4 +25,5 @@ public class LambdaEnvironment { 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 index 819f9872d..da37f7ca7 100644 --- a/aws-lambda-java-runtime-interface-client/src/main/java/com/amazonaws/services/lambda/runtime/api/client/PojoSerializerLoader.java +++ b/aws-lambda-java-runtime-interface-client/src/main/java/com/amazonaws/services/lambda/runtime/api/client/PojoSerializerLoader.java @@ -1,10 +1,12 @@ -/* Copyright 2023 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.serialization.PojoSerializer; 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; @@ -26,7 +28,7 @@ private static CustomPojoSerializer loadSerializer() return customPojoSerializer; } - ServiceLoader loader = ServiceLoader.load(CustomPojoSerializer.class, AWSLambda.customerClassLoader); + ServiceLoader loader = ServiceLoader.load(CustomPojoSerializer.class, AWSLambda.getCustomerClassLoader()); Iterator serializers = loader.iterator(); if (!serializers.hasNext()) { diff --git a/aws-lambda-java-runtime-interface-client/src/main/java/com/amazonaws/services/lambda/runtime/api/client/ReservedRuntimeEnvironmentVariables.java b/aws-lambda-java-runtime-interface-client/src/main/java/com/amazonaws/services/lambda/runtime/api/client/ReservedRuntimeEnvironmentVariables.java index eca5cef17..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,18 +1,8 @@ /* - * 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; /** @@ -116,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 index bba400346..07fac7170 100644 --- 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 @@ -1,4 +1,7 @@ -/* Copyright 2023 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; @@ -17,4 +20,4 @@ public TooManyServiceProvidersFoundException(Throwable cause) { public TooManyServiceProvidersFoundException(String message, Throwable cause) { super(message, cause); } -} \ No newline at end of file +} diff --git a/aws-lambda-java-runtime-interface-client/src/main/java/com/amazonaws/services/lambda/runtime/api/client/UserFault.java b/aws-lambda-java-runtime-interface-client/src/main/java/com/amazonaws/services/lambda/runtime/api/client/UserFault.java index bc95af572..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 = -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,6 +69,16 @@ 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)) { @@ -78,12 +92,15 @@ public static T filterStackTrace(T t) { Throwable cause = t.getCause(); if (cause != null) { - filterStackTrace(cause); + filterStackTrace(cause, visited, visitedSuppressed); } Throwable[] suppressedExceptions = t.getSuppressed(); - for (Throwable suppressed : suppressedExceptions) { - filterStackTrace(suppressed); + for (Throwable suppressed: suppressedExceptions) { + if (!visitedSuppressed.contains(suppressed)) { + visitedSuppressed.add(suppressed); + filterStackTrace(suppressed, visited, visitedSuppressed); + } } return t; 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 73db5b940..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.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 3b8976b7b..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,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.api; 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 3d57ce4c2..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,6 +22,8 @@ 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( @@ -31,6 +36,8 @@ public LambdaContext( CognitoIdentity identity, String functionVersion, String invokedFunctionArn, + String tenantId, + String xrayTraceId, ClientContext clientContext ) { this.memoryLimit = memoryLimit; @@ -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 index 8246a3ad2..f987b0bdb 100644 --- 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 @@ -1,13 +1,15 @@ -/* Copyright 2023 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 static java.nio.charset.StandardCharsets.UTF_8; - -import com.amazonaws.services.lambda.runtime.api.client.api.LambdaContext; 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. @@ -15,9 +17,9 @@ * void logMessage(byte[] message, LogLevel logLevel); */ public abstract class AbstractLambdaLogger implements LambdaLogger { + protected final LogFormat logFormat; private final LogFiltering logFiltering; private final LogFormatter logFormatter; - protected final LogFormat logFormat; public AbstractLambdaLogger(LogLevel logLevel, LogFormat logFormat) { this.logFiltering = new LogFiltering(logLevel); @@ -65,4 +67,8 @@ public void log(byte[] message) { 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 6663cc1d9..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,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.logging; -import com.amazonaws.services.lambda.runtime.logging.LogLevel; import com.amazonaws.services.lambda.runtime.logging.LogFormat; +import com.amazonaws.services.lambda.runtime.logging.LogLevel; /** * 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. @@ -29,17 +32,17 @@ public class FrameType { private final int 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); - } - FrameType(int val) { 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 f20e77221..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,7 +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.logging; +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; @@ -9,8 +14,6 @@ import java.nio.ByteOrder; import java.time.Instant; -import com.amazonaws.services.lambda.runtime.logging.LogLevel; -import com.amazonaws.services.lambda.runtime.logging.LogFormat; /** * FramedTelemetryLogSink implements the logging contract between runtimes and the platform. It implements a simple @@ -62,7 +65,8 @@ private void writeFrame(LogLevel logLevel, LogFormat logFormat, byte[] message) private long timestamp() { Instant instant = Instant.now(); - return instant.getEpochSecond() * 1_000_000 + instant.getNano() / 1000; // microsecond precision + // microsecond precision + return instant.getEpochSecond() * 1_000_000 + instant.getNano() / 1000; } /** diff --git a/aws-lambda-java-runtime-interface-client/src/main/java/com/amazonaws/services/lambda/runtime/api/client/logging/JsonLogFormatter.java b/aws-lambda-java-runtime-interface-client/src/main/java/com/amazonaws/services/lambda/runtime/api/client/logging/JsonLogFormatter.java index ef9e1c410..f1051a216 100644 --- a/aws-lambda-java-runtime-interface-client/src/main/java/com/amazonaws/services/lambda/runtime/api/client/logging/JsonLogFormatter.java +++ b/aws-lambda-java-runtime-interface-client/src/main/java/com/amazonaws/services/lambda/runtime/api/client/logging/JsonLogFormatter.java @@ -1,4 +1,7 @@ -/* Copyright 2023 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; @@ -6,7 +9,6 @@ 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; @@ -14,13 +16,13 @@ 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 LambdaContext lambdaContext; - private static final DateTimeFormatter dateFormatter = - DateTimeFormatter - .ofPattern("yyyy-MM-dd'T'HH:mm:ss.SSS'Z'") - .withZone(ZoneId.of("UTC")); + private ThreadLocal lambdaContext = new ThreadLocal<>(); @Override public String format(String message, LogLevel logLevel) { @@ -37,9 +39,12 @@ private StructuredLogMessage createLogMessage(String message, LogLevel logLevel) msg.message = message; msg.level = logLevel; - if (lambdaContext != null) { - msg.AWSRequestId = lambdaContext.getAwsRequestId(); + LambdaContext lambdaContextForCurrentThread = lambdaContext.get(); + if (lambdaContextForCurrentThread != null) { + msg.AWSRequestId = lambdaContextForCurrentThread.getAwsRequestId(); + msg.tenantId = lambdaContextForCurrentThread.getTenantId(); } + return msg; } @@ -50,6 +55,10 @@ private StructuredLogMessage createLogMessage(String message, LogLevel logLevel) */ @Override public void setLambdaContext(LambdaContext context) { - this.lambdaContext = context; + if (context == null) { + lambdaContext.remove(); + } else { + lambdaContext.set(context); + } } } diff --git a/aws-lambda-java-runtime-interface-client/src/main/java/com/amazonaws/services/lambda/runtime/api/client/logging/LambdaContextLogger.java b/aws-lambda-java-runtime-interface-client/src/main/java/com/amazonaws/services/lambda/runtime/api/client/logging/LambdaContextLogger.java index 4800b356f..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,14 +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.logging; -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 java.io.Closeable; +import java.io.IOException; import static java.nio.charset.StandardCharsets.UTF_8; -public class LambdaContextLogger extends AbstractLambdaLogger { +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); @@ -23,8 +26,15 @@ public LambdaContextLogger(LogSink sink, LogLevel logLevel, LogFormat logFormat) @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(logLevel, this.logFormat, message); + } + + @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 index 59038efa5..a9bdec86c 100644 --- 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 @@ -1,4 +1,7 @@ -/* Copyright 2023 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; 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 index debe5af41..283b52289 100644 --- 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 @@ -1,4 +1,7 @@ -/* Copyright 2023 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; @@ -9,5 +12,5 @@ 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 77df08e25..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,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.logging; -import java.io.Closeable; - -import com.amazonaws.services.lambda.runtime.logging.LogLevel; 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 { 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 6fd6b87c7..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,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.logging; -import java.io.IOException; - -import com.amazonaws.services.lambda.runtime.logging.LogLevel; 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 @@ -13,7 +15,7 @@ public void log(byte[] message) { log(LogLevel.UNDEFINED, LogFormat.TEXT, message); } - public void log(LogLevel logLevel, LogFormat logFormat, byte[] message) { + public synchronized void log(LogLevel logLevel, LogFormat logFormat, byte[] message) { try { System.out.write(message); } catch (IOException e) { diff --git a/aws-lambda-java-runtime-interface-client/src/main/java/com/amazonaws/services/lambda/runtime/api/client/logging/StructuredLogMessage.java b/aws-lambda-java-runtime-interface-client/src/main/java/com/amazonaws/services/lambda/runtime/api/client/logging/StructuredLogMessage.java index 9c271adf7..0ae19961f 100644 --- 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 @@ -1,4 +1,7 @@ -/* Copyright 2023 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; @@ -9,4 +12,5 @@ class StructuredLogMessage { 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 index 7c3454012..5424bd4bd 100644 --- 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 @@ -1,18 +1,22 @@ -/* Copyright 2023 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.api.client.api.LambdaContext; import com.amazonaws.services.lambda.runtime.logging.LogLevel; - import java.util.HashMap; +import java.util.Map; public class TextLogFormatter implements LogFormatter { - private static final HashMap logLevelMapper = new HashMap() {{ - for (LogLevel logLevel: LogLevel.values()) { - put(logLevel, "[" + logLevel.toString() + "] "); + private static final Map logLevelMapper = new HashMap() { + { + for (LogLevel logLevel: LogLevel.values()) { + put(logLevel, "[" + logLevel.toString() + "] "); + } } - }}; + }; @Override public String format(String message, LogLevel logLevel) { @@ -20,9 +24,9 @@ public String format(String message, LogLevel logLevel) { return message; } - return new StringBuilder() - .append(logLevelMapper.get(logLevel)) - .append(message) - .toString(); + 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 1d4d3aa59..000000000 --- a/aws-lambda-java-runtime-interface-client/src/main/java/com/amazonaws/services/lambda/runtime/api/client/runtimeapi/LambdaRuntimeClient.java +++ /dev/null @@ -1,221 +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.HashMap; -import java.util.Map; -import java.util.Objects; - -import static java.net.HttpURLConnection.HTTP_ACCEPTED; -import static java.net.HttpURLConnection.HTTP_OK; -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(); - NativeClient.init(); - } - - 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 getRestoreNext() throws IOException { - doGet(restoreNextEndpoint(), HTTP_OK); - } - - public int postRestoreError(byte[] errorResponse, String errorType) throws IOException { - String endpoint = restoreErrorEndpoint(); - return postError(endpoint, errorResponse, errorType, null); - } - - 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 getBaseUrl() + "/2018-06-01/runtime/invocation/"; - } - - private String invocationErrorEndpoint(String requestId) { - return invocationEndpoint + requestId + "/error"; - } - - private String initErrorEndpoint() { - return getBaseUrl() + "/2018-06-01/runtime/init/error"; - } - - private String restoreErrorEndpoint() { - return getBaseUrl() + "/2018-06-01/runtime/restore/error"; - } - - private String restoreNextEndpoint() { - return getBaseUrl() + "/2018-06-01/runtime/restore/next"; - } - - private String getBaseUrl() { - return "http://" + hostname + ":" + port; - } - - private int postError(String endpoint, - byte[] errorResponse, - String errorType, - String errorCause) throws IOException { - - Map headers = new HashMap<>(); - if (errorType != null && !errorType.isEmpty()) { - headers.put(ERROR_TYPE_HEADER, errorType); - } - if (errorCause != null && errorCause.getBytes().length < XRAY_ERROR_CAUSE_MAX_HEADER_SIZE) { - headers.put(XRAY_ERROR_CAUSE_HEADER, errorCause); - } - - return doPost(endpoint, DEFAULT_CONTENT_TYPE, headers, errorResponse); - } - - private int doPost(String endpoint, - String contentType, - Map headers, - byte[] payload) throws IOException { - - URL url = createUrl(endpoint); - HttpURLConnection conn = (HttpURLConnection) url.openConnection(); - - conn.setRequestMethod("POST"); - conn.setRequestProperty("Content-Type", contentType); - - 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 void doGet(String endpoint, int expectedHttpResponseCode) throws IOException { - - URL url = createUrl(endpoint); - HttpURLConnection conn = (HttpURLConnection) url.openConnection(); - conn.setRequestMethod("GET"); - - int responseCode = conn.getResponseCode(); - if (responseCode != expectedHttpResponseCode) { - throw new LambdaRuntimeClientException(endpoint, responseCode); - } - - closeInputStreamQuietly(conn); - } - - 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) { - } - } - - 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/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 82088c0f9..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,85 +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.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; +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 NATIVE_LIB_PATH = "/tmp/.libaws-lambda-jni.so"; - public static final String NATIVE_CLIENT_JNI_PROPERTY = "com.amazonaws.services.lambda.runtime.api.client.runtimeapi.NativeClient.JNI"; - - static void init() { - loadJNILib(); - initUserAgent(); - } - - private static void loadJNILib() { - 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" - }; - unpackAndLoadNativeLibrary(libsToTry); - } + static void init(String awsLambdaRuntimeApi) { + JniHelper.load(); + initializeClient(USER_AGENT.getBytes(), awsLambdaRuntimeApi.getBytes()); } - - - /** - * 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 - */ - static void unpackAndLoadNativeLibrary(String[] libsToTry) { - - List errorMessages = new ArrayList<>(); - for (String libToTry : libsToTry) { - try (InputStream inputStream = NativeClient.class.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); - } - - private static void initUserAgent() { - String userAgent = String.format( - "aws-lambda-java/%s-%s", - System.getProperty("java.vendor.version"), - NativeClient.class.getPackage().getImplementationVersion()); - - initializeClient(userAgent.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/UnknownPlatformException.java b/aws-lambda-java-runtime-interface-client/src/main/java/com/amazonaws/services/lambda/runtime/api/client/runtimeapi/UnknownPlatformException.java deleted file mode 100644 index f0b6d76e9..000000000 --- a/aws-lambda-java-runtime-interface-client/src/main/java/com/amazonaws/services/lambda/runtime/api/client/runtimeapi/UnknownPlatformException.java +++ /dev/null @@ -1,11 +0,0 @@ -package com.amazonaws.services.lambda.runtime.api.client.runtimeapi; - -/** - * Copyright (c) 2022 Amazon. All rights reserved. - */ -public class UnknownPlatformException extends RuntimeException { - - public UnknownPlatformException(String message) { - super(message); - } -} 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 79% 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 6a6e7b129..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 { @@ -41,6 +40,11 @@ public class InvocationRequest { */ private String cognitoIdentity; + /** + * The tenant ID associated with the request. + */ + private String tenantId; + private byte[] content; public String getId() { @@ -63,6 +67,7 @@ public String getInvokedFunctionArn() { return invokedFunctionArn; } + @SuppressWarnings("unused") public void setInvokedFunctionArn(String invokedFunctionArn) { this.invokedFunctionArn = invokedFunctionArn; } @@ -71,6 +76,7 @@ public long getDeadlineTimeInMs() { return deadlineTimeInMs; } + @SuppressWarnings("unused") public void setDeadlineTimeInMs(long deadlineTimeInMs) { this.deadlineTimeInMs = deadlineTimeInMs; } @@ -79,6 +85,7 @@ public String getClientContext() { return clientContext; } + @SuppressWarnings("unused") public void setClientContext(String clientContext) { this.clientContext = clientContext; } @@ -87,12 +94,21 @@ public String getCognitoIdentity() { return cognitoIdentity; } + @SuppressWarnings("unused") public void setCognitoIdentity(String cognitoIdentity) { this.cognitoIdentity = cognitoIdentity; } - public InputStream getContentAsStream() { - return new ByteArrayInputStream(content); + public String getTenantId() { + return tenantId; + } + + public void setTenantId(String tenantId) { + this.tenantId = tenantId; + } + + public byte[] getContent() { + return content; } public void setContent(byte[] 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/LambdaOutputStream.java b/aws-lambda-java-runtime-interface-client/src/main/java/com/amazonaws/services/lambda/runtime/api/client/util/LambdaOutputStream.java index 556859a27..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; 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 dd4fdb22e..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 @@ -53,9 +53,16 @@ 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 \ 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 e15f6adc5..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 @@ -54,9 +54,16 @@ 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/lib/ \ -laws-lambda-runtime \ -lcurl \ 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 3b505e74d..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 @@ -51,10 +51,21 @@ function build_for_libc_arch() { 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}" + --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}" 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 87fa9f028..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,27 +1,13 @@ /* - * 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; @@ -34,6 +20,7 @@ static jfieldID contentField; static jfieldID clientContextField; static jfieldID cognitoIdentityField; static jfieldID xrayTraceIdField; +static jfieldID tenantIdField; jint JNI_OnLoad(JavaVM* vm, void* reserved) { @@ -44,7 +31,7 @@ jint JNI_OnLoad(JavaVM* vm, void* reserved) { } jclass tempInvocationRequestClassRef; - tempInvocationRequestClassRef = env->FindClass("com/amazonaws/services/lambda/runtime/api/client/runtimeapi/InvocationRequest"); + tempInvocationRequestClassRef = env->FindClass("com/amazonaws/services/lambda/runtime/api/client/runtimeapi/dto/InvocationRequest"); invocationRequestClass = (jclass) env->NewGlobalRef(tempInvocationRequestClassRef); env->DeleteLocalRef(tempInvocationRequestClassRef); @@ -55,6 +42,7 @@ jint JNI_OnLoad(JavaVM* vm, void* reserved) { 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; } @@ -83,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); } @@ -120,6 +108,10 @@ JNIEXPORT jobject JNICALL Java_com_amazonaws_services_lambda_runtime_api_client_ 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)); 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/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 index b81660730..7a7653dc2 100644 --- 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 @@ -1,22 +1,36 @@ /* - * Copyright 2023 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.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.*; +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 { 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 index 171985dea..aae2f1afe 100644 --- a/aws-lambda-java-runtime-interface-client/src/test/java/com/amazonaws/services/lambda/runtime/api/client/EventHandlerLoaderTest.java +++ b/aws-lambda-java-runtime-interface-client/src/test/java/com/amazonaws/services/lambda/runtime/api/client/EventHandlerLoaderTest.java @@ -1,11 +1,19 @@ 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 org.junit.jupiter.api.Test; import java.io.ByteArrayOutputStream; +import java.util.ArrayList; +import java.util.List; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; +import java.util.concurrent.Future; +import java.util.concurrent.TimeUnit; import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertTrue; class EventHandlerLoaderTest { @@ -37,7 +45,6 @@ void PojoHandlerTest_oneParamEvent() throws Exception { assertSuccessfulInvocation(lambdaRequestHandler); } - @Test void PojoHandlerTest_oneParamContext() throws Exception { String handler = "test.lambda.handlers.POJOHanlderImpl::oneParamHandler_context"; @@ -74,4 +81,72 @@ private static InvocationRequest getTestInvocationRequest() { invocationRequest.setXrayTraceId("traceId"); return invocationRequest; } -} \ No newline at end of file + + // Multithreaded test methods + + @Test + void RequestHandlerTest_Multithreaded() throws Exception { + testHandlerConcurrency("test.lambda.handlers.RequestHandlerImpl"); + } + + @Test + void RequestStreamHandlerTest_Multithreaded() throws Exception { + testHandlerConcurrency("test.lambda.handlers.RequestStreamHandlerImpl"); + } + + @Test + void PojoHandlerTest_noParams_Multithreaded() throws Exception { + testHandlerConcurrency("test.lambda.handlers.POJOHanlderImpl::noParamsHandler"); + } + + @Test + void PojoHandlerTest_oneParamEvent_Multithreaded() throws Exception { + testHandlerConcurrency("test.lambda.handlers.POJOHanlderImpl::oneParamHandler_event"); + } + + @Test + void PojoHandlerTest_oneParamContext_Multithreaded() throws Exception { + testHandlerConcurrency("test.lambda.handlers.POJOHanlderImpl::oneParamHandler_context"); + } + + @Test + void PojoHandlerTest_twoParams_Multithreaded() throws Exception { + testHandlerConcurrency("test.lambda.handlers.POJOHanlderImpl::twoParamsHandler"); + } + + private void testHandlerConcurrency(String handlerName) throws Exception { + // Create one handler instance + LambdaRequestHandler handler = getLambdaRequestHandler(handlerName); + + int threadCount = 10; + ExecutorService executor = Executors.newFixedThreadPool(threadCount); + List> futures = new ArrayList<>(); + CountDownLatch startLatch = new CountDownLatch(1); + + try { + for (int i = 0; i < threadCount; i++) { + futures.add(executor.submit(() -> { + try { + InvocationRequest request = getTestInvocationRequest(); + startLatch.await(); + ByteArrayOutputStream result = handler.call(request); + return result.toString(); + } catch (Exception e) { + throw new RuntimeException(e); + } + })); + } + + // Release all threads simultaneously and Verify all invocations return the expected result + startLatch.countDown(); + + for (Future future : futures) { + String result = future.get(5, TimeUnit.SECONDS); + assertEquals("\"success\"", result); + } + } finally { + executor.shutdown(); + assertTrue(executor.awaitTermination(10, TimeUnit.SECONDS)); + } + } +} diff --git a/aws-lambda-java-runtime-interface-client/src/test/java/com/amazonaws/services/lambda/runtime/api/client/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 9e88024b0..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 @@ -84,4 +84,82 @@ public void testSuppressedExceptionsAreIncluded() { 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 index baeb4c242..3a5ee8d5f 100644 --- a/aws-lambda-java-runtime-interface-client/src/test/java/com/amazonaws/services/lambda/runtime/api/client/logging/AbstractLambdaLoggerTest.java +++ b/aws-lambda-java-runtime-interface-client/src/test/java/com/amazonaws/services/lambda/runtime/api/client/logging/AbstractLambdaLoggerTest.java @@ -5,10 +5,15 @@ import com.amazonaws.services.lambda.runtime.logging.LogFormat; import org.junit.jupiter.api.Test; +import java.nio.charset.StandardCharsets; import java.util.LinkedList; import java.util.List; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; +import com.amazonaws.lambda.thirdparty.org.json.JSONObject; import com.amazonaws.services.lambda.runtime.LambdaLogger; +import com.amazonaws.services.lambda.runtime.api.client.api.LambdaContext; import com.amazonaws.services.lambda.runtime.logging.LogLevel; @@ -20,12 +25,12 @@ public TestSink() { } @Override - public void log(byte[] message) { + public synchronized void log(byte[] message) { messages.add(message); } @Override - public void log(LogLevel logLevel, LogFormat logFormat, byte[] message) { + public synchronized void log(LogLevel logLevel, LogFormat logFormat, byte[] message) { messages.add(message); } @@ -62,6 +67,45 @@ public void testLoggingNullValuesWithoutLogLevelInText() { assertEquals("null", new String(sink.getMessages().get(1))); } + /* + * Makes Sure Logging Contexts are thread local. + * We start `setLambdaContext` operations using the **single** shared `logger` object on a fixed thread pool, differentiating them with thread IDs. + * We then start concurrent `log` operations which are scheduled using that fixed pool. + * It is then verified that a given log operation, which logs the thread ID it is running on, used a context that had the same thread ID. + */ + @Test + public void testMultiConcurrentLoggingWithoutLogLevelInJSON() { + TestSink sink = new TestSink(); + LambdaContextLogger logger = new LambdaContextLogger(sink, LogLevel.INFO, LogFormat.JSON); + + String someMessagePrefix = "Some Message from "; + String reqIDPrefix = "Thread ID as request# "; + + final int nThreads = 5; + ExecutorService es = Executors.newFixedThreadPool(nThreads); + for (int i = 0; i < nThreads; i++) { + es.submit(() -> logger.setLambdaContext(new LambdaContext(Integer.MAX_VALUE, Long.MAX_VALUE, reqIDPrefix + Thread.currentThread().getName(), "", "", "", null, "", "", "", null, null))); + } + + final int nMessages = 100_000; + for (int i = 0; i < nMessages; i++) { + es.submit(() -> logger.log(someMessagePrefix + Thread.currentThread().getName())); + } + + es.shutdown(); + while (!es.isTerminated()) { + ; + } + + assertEquals(nMessages, sink.getMessages().size()); + for (byte[] message : sink.getMessages()) { + JSONObject parsedLog = new JSONObject(new String(message, StandardCharsets.UTF_8)); + String parsedMessage = parsedLog.getString("message"); + String parsedReqID = parsedLog.getString("AWSRequestId"); + assertEquals(parsedMessage.substring(someMessagePrefix.length()), parsedReqID.substring(reqIDPrefix.length())); + } + } + @Test public void testLoggingNullValuesWithoutLogLevelInJSON() { TestSink sink = new TestSink(); diff --git a/aws-lambda-java-runtime-interface-client/src/test/java/com/amazonaws/services/lambda/runtime/api/client/logging/JsonLogFormatterTest.java b/aws-lambda-java-runtime-interface-client/src/test/java/com/amazonaws/services/lambda/runtime/api/client/logging/JsonLogFormatterTest.java index 8630d5fe6..91ce9d2a3 100644 --- a/aws-lambda-java-runtime-interface-client/src/test/java/com/amazonaws/services/lambda/runtime/api/client/logging/JsonLogFormatterTest.java +++ b/aws-lambda-java-runtime-interface-client/src/test/java/com/amazonaws/services/lambda/runtime/api/client/logging/JsonLogFormatterTest.java @@ -29,6 +29,27 @@ void testFormattingWithLambdaContext() { 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); @@ -52,6 +73,7 @@ void assert_expected_log_message(StructuredLogMessage result, String message, Lo 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/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/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/test-handlers/EchoHandler.java b/aws-lambda-java-runtime-interface-client/test-handlers/EchoHandler.java new file mode 100644 index 000000000..cb324e7f7 --- /dev/null +++ b/aws-lambda-java-runtime-interface-client/test-handlers/EchoHandler.java @@ -0,0 +1,20 @@ +import com.amazonaws.services.lambda.runtime.Context; +import com.amazonaws.services.lambda.runtime.RequestHandler; +import java.util.Map; +import java.util.HashMap; + +public class EchoHandler implements RequestHandler, Map> { + + @Override + public Map handleRequest(Map event, Context context) { + context.getLogger().log("Processing event: " + event); + + Map response = new HashMap<>(event); + response.put("timestamp", System.currentTimeMillis()); + response.put("requestId", context.getAwsRequestId()); + response.put("functionName", context.getFunctionName()); + response.put("remainingTimeInMillis", context.getRemainingTimeInMillis()); + + return response; + } +} \ No newline at end of file diff --git a/aws-lambda-java-runtime-interface-client/test/integration/codebuild/buildspec.os.alpine.yml b/aws-lambda-java-runtime-interface-client/test/integration/codebuild/buildspec.os.alpine.yml index 4f8caf39d..2a71cb1b0 100644 --- a/aws-lambda-java-runtime-interface-client/test/integration/codebuild/buildspec.os.alpine.yml +++ b/aws-lambda-java-runtime-interface-client/test/integration/codebuild/buildspec.os.alpine.yml @@ -43,8 +43,9 @@ phases: # Install events (dependency of serialization) - (cd aws-lambda-java-events && mvn install) # Install serialization (dependency of RIC) + - (cd aws-lambda-java-core && mvn install) - (cd aws-lambda-java-serialization && mvn install) - - (cd aws-lambda-java-runtime-interface-client && mvn install) + - (cd aws-lambda-java-runtime-interface-client && 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" 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 fd45aabb8..db8bf2ba0 100644 --- a/aws-lambda-java-runtime-interface-client/test/integration/codebuild/buildspec.os.amazoncorretto.yml +++ b/aws-lambda-java-runtime-interface-client/test/integration/codebuild/buildspec.os.amazoncorretto.yml @@ -42,8 +42,9 @@ phases: # Install events (dependency of serialization) - (cd aws-lambda-java-events && mvn install) # Install serialization (dependency of RIC) + - (cd aws-lambda-java-core && mvn install) - (cd aws-lambda-java-serialization && mvn install) - - (cd aws-lambda-java-runtime-interface-client && mvn install) + - (cd aws-lambda-java-runtime-interface-client && 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" 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 197e97249..e3773cf82 100644 --- a/aws-lambda-java-runtime-interface-client/test/integration/codebuild/buildspec.os.amazonlinux.1.yml +++ b/aws-lambda-java-runtime-interface-client/test/integration/codebuild/buildspec.os.amazonlinux.1.yml @@ -37,8 +37,9 @@ phases: # Install events (dependency of serialization) - (cd aws-lambda-java-events && mvn install) # Install serialization (dependency of RIC) + - (cd aws-lambda-java-core && mvn install) - (cd aws-lambda-java-serialization && mvn install) - - (cd aws-lambda-java-runtime-interface-client && mvn install -DmultiArch=false) + - (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" 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 75b816e8b..a9836fc6f 100644 --- a/aws-lambda-java-runtime-interface-client/test/integration/codebuild/buildspec.os.amazonlinux.2.yml +++ b/aws-lambda-java-runtime-interface-client/test/integration/codebuild/buildspec.os.amazonlinux.2.yml @@ -41,8 +41,9 @@ phases: # Install events (dependency of serialization) - (cd aws-lambda-java-events && mvn install) # Install serialization (dependency of RIC) + - (cd aws-lambda-java-core && mvn install) - (cd aws-lambda-java-serialization && mvn install) - - (cd aws-lambda-java-runtime-interface-client && mvn install) + - (cd aws-lambda-java-runtime-interface-client && 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" diff --git a/aws-lambda-java-runtime-interface-client/test/integration/codebuild/buildspec.os.centos.yml b/aws-lambda-java-runtime-interface-client/test/integration/codebuild/buildspec.os.centos.yml index d718c2647..74d12b01d 100644 --- a/aws-lambda-java-runtime-interface-client/test/integration/codebuild/buildspec.os.centos.yml +++ b/aws-lambda-java-runtime-interface-client/test/integration/codebuild/buildspec.os.centos.yml @@ -41,6 +41,7 @@ phases: # Install events (dependency of serialization) - (cd aws-lambda-java-events && mvn install) # Install serialization (dependency of RIC) + - (cd aws-lambda-java-core && mvn install) - (cd aws-lambda-java-serialization && mvn install) - (cd aws-lambda-java-runtime-interface-client && mvn install) - (cd aws-lambda-java-runtime-interface-client/test/integration/test-handler && mvn install) diff --git a/aws-lambda-java-runtime-interface-client/test/integration/codebuild/buildspec.os.debian.yml b/aws-lambda-java-runtime-interface-client/test/integration/codebuild/buildspec.os.debian.yml index d2772fbfc..222d14a36 100644 --- a/aws-lambda-java-runtime-interface-client/test/integration/codebuild/buildspec.os.debian.yml +++ b/aws-lambda-java-runtime-interface-client/test/integration/codebuild/buildspec.os.debian.yml @@ -42,6 +42,7 @@ phases: # Install events (dependency of serialization) - (cd aws-lambda-java-events && mvn install) # Install serialization (dependency of RIC) + - (cd aws-lambda-java-core && mvn install) - (cd aws-lambda-java-serialization && mvn install) - (cd aws-lambda-java-runtime-interface-client && mvn install) - (cd aws-lambda-java-runtime-interface-client/test/integration/test-handler && mvn install) diff --git a/aws-lambda-java-runtime-interface-client/test/integration/codebuild/buildspec.os.ubuntu.yml b/aws-lambda-java-runtime-interface-client/test/integration/codebuild/buildspec.os.ubuntu.yml index 2a90017b3..ce153c547 100644 --- a/aws-lambda-java-runtime-interface-client/test/integration/codebuild/buildspec.os.ubuntu.yml +++ b/aws-lambda-java-runtime-interface-client/test/integration/codebuild/buildspec.os.ubuntu.yml @@ -44,6 +44,7 @@ phases: # Install events (dependency of serialization) - (cd aws-lambda-java-events && mvn install) # Install serialization (dependency of RIC) + - (cd aws-lambda-java-core && mvn install) - (cd aws-lambda-java-serialization && mvn install) - (cd aws-lambda-java-runtime-interface-client && mvn install) - (cd aws-lambda-java-runtime-interface-client/test/integration/test-handler && mvn install) diff --git a/aws-lambda-java-runtime-interface-client/test/integration/test-handler/pom.xml b/aws-lambda-java-runtime-interface-client/test/integration/test-handler/pom.xml index 028e92f56..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.4.1 + 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 259c78e54..2ce29d758 100644 --- a/aws-lambda-java-serialization/RELEASE.CHANGELOG.md +++ b/aws-lambda-java-serialization/RELEASE.CHANGELOG.md @@ -1,3 +1,16 @@ +### 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 diff --git a/aws-lambda-java-serialization/pom.xml b/aws-lambda-java-serialization/pom.xml index bdfa13b20..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.1.3 + 1.2.0 jar AWS Lambda Java Runtime Serialization @@ -32,9 +32,9 @@ 1.8 1.8 com.amazonaws.lambda.thirdparty - 2.14.2 + 2.15.4 2.10.1 - 20230227 + 20231013 7.3.2 @@ -169,14 +169,12 @@ - org.sonatype.plugins - nexus-staging-maven-plugin - 1.6.3 + org.sonatype.central + central-publishing-maven-plugin + 0.8.0 true - sonatype-nexus-staging - https://aws.oss.sonatype.org/ - false + central @@ -196,7 +194,7 @@ org.apache.maven.plugins maven-shade-plugin - 3.2.1 + 3.6.1 package @@ -211,27 +209,6 @@ com.fasterxml.jackson ${relocation.prefix}.com.fasterxml.jackson - - com.fasterxml.jackson.annotation.JsonInclude - com.fasterxml.jackson.annotation.JsonInclude$Value - com.fasterxml.jackson.annotation.JsonInclude$Include - com.fasterxml.jackson.annotation.JsonProperty - com.fasterxml.jackson.annotation.JsonProperty$Access - com.fasterxml.jackson.annotation.JsonFormat - com.fasterxml.jackson.annotation.JsonFormat$Shape - com.fasterxml.jackson.annotation.JsonFormat$Feature - com.fasterxml.jackson.annotation.JsonIgnore - com.fasterxml.jackson.annotation.JsonSerialize - com.fasterxml.jackson.annotation.JsonView - com.fasterxml.jackson.annotation.JsonTypeInfo - com.fasterxml.jackson.annotation.JsonRawValue - com.fasterxml.jackson.annotation.JsonUnwrapped - com.fasterxml.jackson.annotation.JsonBackReference - com.fasterxml.jackson.annotation.JsonManagedReference - com.fasterxml.jackson.annotation.JacksonAnnotation - com.fasterxml.jackson.annotation.JacksonAnnotationValue - com.fasterxml.jackson.annotation.OptBoolean - com.google.gson 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 b24b40609..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 @@ -118,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", @@ -158,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"))), @@ -170,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( @@ -180,6 +182,14 @@ 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( @@ -206,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)); /** @@ -236,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/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/verify-relocation.sh b/aws-lambda-java-serialization/verify-relocation.sh index 78f2e63b8..d44cae7f4 100755 --- a/aws-lambda-java-serialization/verify-relocation.sh +++ b/aws-lambda-java-serialization/verify-relocation.sh @@ -19,7 +19,7 @@ if [[ ! -z "$OUTPUT" ]]; then fi echo 'Validating that everything other than serialization module classes were relocated' -OUTPUT=$(zipinfo ${ARTIFACT_PATH} | grep '.class' | grep -v ${SERIALIZATION_MODULE_PATTERN//.//} | grep -v 'com.fasterxml.jackson.annotation' | grep -v 'META-INF' | grep -v ${RELOCATION_PREFIX//.//} || true) +OUTPUT=$(zipinfo ${ARTIFACT_PATH} | grep '.class' | grep -v ${SERIALIZATION_MODULE_PATTERN//.//} | grep -v 'META-INF' | grep -v ${RELOCATION_PREFIX//.//} || true) if [[ ! -z "$OUTPUT" ]]; then echo "Some classes were not relocated" echo ${OUTPUT} diff --git a/aws-lambda-java-tests/pom.xml b/aws-lambda-java-tests/pom.xml index 5676d32b3..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 @@ -40,12 +40,12 @@ com.amazonaws aws-lambda-java-serialization - 1.1.2 + 1.1.6 com.amazonaws aws-lambda-java-events - 3.11.3 + 3.16.1 org.junit.jupiter @@ -65,13 +65,13 @@ org.apache.commons commons-lang3 - 3.12.0 + 3.18.0 org.assertj assertj-core - 3.24.2 + 3.27.7 test @@ -220,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 @@ -240,7 +238,7 @@ org.apache.maven.plugins maven-compiler-plugin - 3.8.1 + 3.11.0 ${maven.compiler.source} ${maven.compiler.target} @@ -254,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 e36016a16..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 @@ -275,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 @@ -311,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 @@ -347,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/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 c4a5ac21c..fbd93b64f 100644 --- a/samples/kinesis-firehose-event-handler/pom.xml +++ b/samples/kinesis-firehose-event-handler/pom.xml @@ -46,13 +46,13 @@ com.amazonaws aws-lambda-java-events - 3.11.3 + 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==" + } + ] +}