From b16eb60821cd845449c491cad34d5606393fc3ff Mon Sep 17 00:00:00 2001 From: iulianbudau Date: Fri, 21 Aug 2026 14:17:44 +0000 Subject: [PATCH] test: extend DynamoDB performance benchmarks Add deterministic mocked full-pipeline benches (LOW/DOCUMENT/TYPED, sync + async Get) and opt-in live DynamoDB benches. Manual JMH only; BenchmarkRunner, shards, baselines, and CI are unchanged. --- test/sdk-benchmarks/DYNAMODB.md | 157 ++++++++++ test/sdk-benchmarks/README.md | 7 + test/sdk-benchmarks/pom.xml | 41 +++ .../dynamodb/DynamoDbBenchmarkConstant.java | 43 +++ .../DynamoDbBenchmarkSystemSetting.java | 70 +++++ .../dynamodb/fixture/BenchmarkItem.java | 170 +++++++++++ .../fixture/DynamoDbBenchmarkFixture.java | 99 +++++++ .../dynamodb/live/LiveBenchmarkGuard.java | 47 +++ .../dynamodb/live/LiveDynamoDbSupport.java | 277 ++++++++++++++++++ .../dynamodb/live/LiveRetryObserver.java | 131 +++++++++ .../live/LowLevelLiveGetItemBenchmark.java | 84 ++++++ .../live/LowLevelLivePutItemBenchmark.java | 80 +++++ .../live/TypedLiveGetItemBenchmark.java | 80 +++++ .../live/TypedLivePutItemBenchmark.java | 76 +++++ .../live/TypedLiveQueryBenchmark.java | 85 ++++++ .../mock/DynamoDbMockClientFactory.java | 101 +++++++ .../mock/DynamoDbMockResponseFactory.java | 107 +++++++ .../mock/ReplayingMockAsyncHttpClient.java | 62 ++++ .../pipeline/DocumentGetItemBenchmark.java | 101 +++++++ .../pipeline/DocumentPutItemBenchmark.java | 97 ++++++ .../LowLevelAsyncGetItemBenchmark.java | 89 ++++++ .../pipeline/LowLevelGetItemBenchmark.java | 91 ++++++ .../pipeline/LowLevelPutItemBenchmark.java | 88 ++++++ .../pipeline/TypedAsyncGetItemBenchmark.java | 88 ++++++ .../pipeline/TypedGetItemBenchmark.java | 89 ++++++ .../pipeline/TypedPutItemBenchmark.java | 83 ++++++ .../pipeline/TypedQueryBenchmark.java | 97 ++++++ .../DynamoDbBenchmarkInfrastructureTest.java | 109 +++++++ .../dynamodb/LiveBenchmarkGuardTest.java | 69 +++++ 29 files changed, 2718 insertions(+) create mode 100644 test/sdk-benchmarks/DYNAMODB.md create mode 100644 test/sdk-benchmarks/src/main/java/software/amazon/awssdk/benchmark/dynamodb/DynamoDbBenchmarkConstant.java create mode 100644 test/sdk-benchmarks/src/main/java/software/amazon/awssdk/benchmark/dynamodb/DynamoDbBenchmarkSystemSetting.java create mode 100644 test/sdk-benchmarks/src/main/java/software/amazon/awssdk/benchmark/dynamodb/fixture/BenchmarkItem.java create mode 100644 test/sdk-benchmarks/src/main/java/software/amazon/awssdk/benchmark/dynamodb/fixture/DynamoDbBenchmarkFixture.java create mode 100644 test/sdk-benchmarks/src/main/java/software/amazon/awssdk/benchmark/dynamodb/live/LiveBenchmarkGuard.java create mode 100644 test/sdk-benchmarks/src/main/java/software/amazon/awssdk/benchmark/dynamodb/live/LiveDynamoDbSupport.java create mode 100644 test/sdk-benchmarks/src/main/java/software/amazon/awssdk/benchmark/dynamodb/live/LiveRetryObserver.java create mode 100644 test/sdk-benchmarks/src/main/java/software/amazon/awssdk/benchmark/dynamodb/live/LowLevelLiveGetItemBenchmark.java create mode 100644 test/sdk-benchmarks/src/main/java/software/amazon/awssdk/benchmark/dynamodb/live/LowLevelLivePutItemBenchmark.java create mode 100644 test/sdk-benchmarks/src/main/java/software/amazon/awssdk/benchmark/dynamodb/live/TypedLiveGetItemBenchmark.java create mode 100644 test/sdk-benchmarks/src/main/java/software/amazon/awssdk/benchmark/dynamodb/live/TypedLivePutItemBenchmark.java create mode 100644 test/sdk-benchmarks/src/main/java/software/amazon/awssdk/benchmark/dynamodb/live/TypedLiveQueryBenchmark.java create mode 100644 test/sdk-benchmarks/src/main/java/software/amazon/awssdk/benchmark/dynamodb/mock/DynamoDbMockClientFactory.java create mode 100644 test/sdk-benchmarks/src/main/java/software/amazon/awssdk/benchmark/dynamodb/mock/DynamoDbMockResponseFactory.java create mode 100644 test/sdk-benchmarks/src/main/java/software/amazon/awssdk/benchmark/dynamodb/mock/ReplayingMockAsyncHttpClient.java create mode 100644 test/sdk-benchmarks/src/main/java/software/amazon/awssdk/benchmark/dynamodb/pipeline/DocumentGetItemBenchmark.java create mode 100644 test/sdk-benchmarks/src/main/java/software/amazon/awssdk/benchmark/dynamodb/pipeline/DocumentPutItemBenchmark.java create mode 100644 test/sdk-benchmarks/src/main/java/software/amazon/awssdk/benchmark/dynamodb/pipeline/LowLevelAsyncGetItemBenchmark.java create mode 100644 test/sdk-benchmarks/src/main/java/software/amazon/awssdk/benchmark/dynamodb/pipeline/LowLevelGetItemBenchmark.java create mode 100644 test/sdk-benchmarks/src/main/java/software/amazon/awssdk/benchmark/dynamodb/pipeline/LowLevelPutItemBenchmark.java create mode 100644 test/sdk-benchmarks/src/main/java/software/amazon/awssdk/benchmark/dynamodb/pipeline/TypedAsyncGetItemBenchmark.java create mode 100644 test/sdk-benchmarks/src/main/java/software/amazon/awssdk/benchmark/dynamodb/pipeline/TypedGetItemBenchmark.java create mode 100644 test/sdk-benchmarks/src/main/java/software/amazon/awssdk/benchmark/dynamodb/pipeline/TypedPutItemBenchmark.java create mode 100644 test/sdk-benchmarks/src/main/java/software/amazon/awssdk/benchmark/dynamodb/pipeline/TypedQueryBenchmark.java create mode 100644 test/sdk-benchmarks/src/test/java/software/amazon/awssdk/benchmark/dynamodb/DynamoDbBenchmarkInfrastructureTest.java create mode 100644 test/sdk-benchmarks/src/test/java/software/amazon/awssdk/benchmark/dynamodb/LiveBenchmarkGuardTest.java diff --git a/test/sdk-benchmarks/DYNAMODB.md b/test/sdk-benchmarks/DYNAMODB.md new file mode 100644 index 000000000000..8eef69890162 --- /dev/null +++ b/test/sdk-benchmarks/DYNAMODB.md @@ -0,0 +1,157 @@ +# DynamoDB Performance Benchmarks + +Manual-first DynamoDB suite in `sdk-benchmarks`. New Tier C / Tier D classes are **not** selected by +`BenchmarkRunner`, shards, baselines, or CI. + +Results across tiers answer different questions and must **not** be compared as one score. + +## Measurement tiers + +| Tier | What it measures | What it excludes | +|---|---|---| +| **A** — Pure micro / protocol | Marshalling, JSON protocol, schema/JSON conversion, cold start | Full client pipeline, network | +| **B** — Mapper isolation | Enhanced mapping via stub `V2TestDynamoDb*Client` | Marshalling, signing, HTTP, network | +| **C** — Mocked pipeline *(new)* | Full sync/async SDK client path with deterministic mock HTTP | Network and DynamoDB service latency | +| **D** — Live DynamoDB *(new)* | End-to-end path: SDK + HTTP transport + **network latency** + **DynamoDB service latency** + response handling | Table provisioning, seeding, client/fixture construction (those stay in `@Setup` / `@TearDown`, outside the timed `@Benchmark` method) | + +Packages: + +- Tier C: `software.amazon.awssdk.benchmark.dynamodb.pipeline` +- Tier D: `software.amazon.awssdk.benchmark.dynamodb.live` +- Shared: `…dynamodb.fixture`, `…dynamodb.mock`, `DynamoDbBenchmarkConstant` + +## Client layers (LOW / DOCUMENT / TYPED) + +These labels are an analysis taxonomy for the suite, not official AWS client product names: + +| Layer | API surface | What the timed path emphasizes | +|---|---|---| +| **LOW** | `DynamoDbClient` / `DynamoDbAsyncClient` | Direct DynamoDB request/response (pre-built low-level requests in sync Get/Put) | +| **DOCUMENT** | `DynamoDbTable` | Enhanced Document model over the same logical item | +| **TYPED** | `DynamoDbTable` (bean mapping) | Enhanced typed mapping to/from the shared fixture bean | + +LOW, DOCUMENT, and TYPED reuse the same logical fixture and keys so cross-layer comparisons stay fair within a tier. + +## Inventory (implemented) + +| Tier | Layer | Sync/Async | Operations | +|---|---|---|---| +| A | Protocol / micro | Sync (existing) | Existing marshaller / protocol / cold-start benches | +| B | TYPED mapper isolation | Sync (existing) | Get/Put/Query/Update/Delete/Scan via stub clients | +| C | LOW | Sync | GetItem, PutItem | +| C | DOCUMENT | Sync | GetItem, PutItem | +| C | TYPED | Sync | GetItem, PutItem, Query (first page) | +| C | LOW | Async | GetItem (`.join()`) | +| C | TYPED | Async | GetItem (`.join()`) | +| D | LOW | Sync | GetItem, PutItem | +| D | TYPED | Sync | GetItem, PutItem, Query (first page) | + +Deferred by design: DOCUMENT async, LOW Query, async Put/Query, concurrency, Batch/Transact, CI/shards/baselines. + +## JMH modes + +| Family | Mode | Unit | Defaults | +|---|---|---|---| +| Tier C (`pipeline`) | `AverageTime` | µs/op | warmup 5 / measurement 5 / forks 2 | +| Tier D (`live`) | `SampleTime` | ms/op | warmup 3 / measurement 5 / fork 1 | + +Reduced CLI overrides (`-wi 1 -i 1 -f 1`) are fine for smoke checks. Use class defaults for meaningful comparisons. + +## Manual build and run + +From the repository root: + +```bash +mvn clean install -P quick -pl :sdk-benchmarks --am +cd test/sdk-benchmarks +``` + +List DynamoDB-related benchmarks: + +```bash +# Windows (cmd / PowerShell) +java -jar target/benchmarks.jar -l | findstr /i dynamodb + +# Unix-like (macOS / Linux / Git Bash) +java -jar target/benchmarks.jar -l | grep -i dynamodb +``` + +### Tier C (mocked — no AWS) + +```bash +# All Tier C pipeline benches +java -jar target/benchmarks.jar "software.amazon.awssdk.benchmark.dynamodb.pipeline" + +# LOW +java -jar target/benchmarks.jar ".*pipeline.LowLevel" + +# DOCUMENT +java -jar target/benchmarks.jar ".*pipeline.Document" + +# TYPED +java -jar target/benchmarks.jar ".*pipeline.Typed" + +# Async Get only +java -jar target/benchmarks.jar ".*pipeline.*Async" + +# Single class +java -jar target/benchmarks.jar LowLevelGetItemBenchmark +``` + +### Tier D (live — opt-in required) + +```bash +# PowerShell +$env:DYNAMODB_BENCHMARK_LIVE="true" +$env:AWS_REGION="us-east-1" # or rely on the default AWS region chain / DYNAMODB_BENCHMARK_REGION + +java -jar target/benchmarks.jar "software.amazon.awssdk.benchmark.dynamodb.live" + +# Equivalent system property +java -Ddynamodb.benchmark.live=true -jar target/benchmarks.jar "software.amazon.awssdk.benchmark.dynamodb.live" +``` + +Region override (optional): + +```bash +$env:DYNAMODB_BENCHMARK_REGION="us-west-2" +# or: -Ddynamodb.benchmark.region=us-west-2 +``` + +### JSON output and profiling + +```bash +java -jar target/benchmarks.jar LowLevelGetItemBenchmark -rf json -rff results.json +java -jar target/benchmarks.jar LowLevelGetItemBenchmark -prof gc +``` + +## Live safety + +- AWS credentials are required (`DefaultCredentialsProvider`). +- Opt-in is mandatory: `DYNAMODB_BENCHMARK_LIVE=true` or `-Ddynamodb.benchmark.live=true`. + Without it, `@Setup` aborts **before** credential resolution, client construction, or any AWS call. + (System property takes precedence over the environment variable if both are set.) +- Each trial creates a **unique** table: `sdk-java-ddb-perf-{op}-{8hex}`. +- Billing mode: **PAY_PER_REQUEST**. +- Tables are tagged (`sdk-java-ddb-perf-benchmark=owned`, `Purpose=aws-sdk-java-v2-dynamodb-live-benchmark`) so orphans from interrupted runs can be identified. +- Teardown deletes **only** the exact table created by that trial (`createdByThisTrial`). +- Interrupted runs (kill/OOM) may leave orphaned tagged tables — clean up manually if needed. +- Live runs incur DynamoDB request cost and service/network variance. + +Retry observation (default on): a lightweight `ExecutionInterceptor` counts attempts. Disable with +`DYNAMODB_BENCHMARK_LIVE_RETRY_OBSERVE=false` / `-Ddynamodb.benchmark.live.retryObserve=false`. +Default SDK retry policy is unchanged. Runs with retries print a warning — interpret scores carefully. + +## Interpretation + +- **Tier C** is deterministic SDK-side cost (mock HTTP). Best signal for SDK regressions. +- **Tier D** includes network and DynamoDB latency. Use for directional E2E checks, not µs SDK diffs. +- **LOW vs TYPED** under Tier D is directional only (service variance dominates). +- **Async** single-op benches include mock/async executor scheduling and `.join()` completion. +- Untimed smoke calls (DNS/TLS/pool init on live; pipeline warm on mocked) are **not** JMH warmup + iterations — JMH still runs its own warmup afterward. + +## Automation invariant + +Do **not** add these classes to `BenchmarkRunner`, shards, `baseline.json`, or CI without an explicit +follow-up design. `mvn package` / `exec:exec` behavior for existing suites is unchanged. diff --git a/test/sdk-benchmarks/README.md b/test/sdk-benchmarks/README.md index 3e0bf0648513..cee89f5d076e 100755 --- a/test/sdk-benchmarks/README.md +++ b/test/sdk-benchmarks/README.md @@ -8,6 +8,13 @@ JMH configurations tailored to SDK's build job and you might need to adjust them based on your test environment such as increasing warmup iterations or measurement time in order to get more reliable data. +## DynamoDB suite (manual) + +New DynamoDB Tier C (mocked pipeline) and Tier D (live) benchmarks live under +`software.amazon.awssdk.benchmark.dynamodb`. They are **manual-only** and are not +selected by `BenchmarkRunner` / CI. See **[DYNAMODB.md](DYNAMODB.md)** for tiers, +inventory, commands, live opt-in, and interpretation notes. + There are three ways to run benchmarks. - Using the executable JAR (Preferred usage per JMH site) diff --git a/test/sdk-benchmarks/pom.xml b/test/sdk-benchmarks/pom.xml index 89fb865dcf0f..31a29a9db2eb 100644 --- a/test/sdk-benchmarks/pom.xml +++ b/test/sdk-benchmarks/pom.xml @@ -272,6 +272,47 @@ ${awsjavasdk.version} compile + + junit + junit + test + + + + software.amazon.awssdk + service-test-utils + ${awsjavasdk.version} + + + junit + junit + + + org.junit.jupiter + junit-jupiter + + + org.junit.vintage + junit-vintage-engine + + + org.hamcrest + hamcrest-core + + + io.projectreactor.tools + blockhound + + + io.projectreactor.tools + blockhound-junit-platform + + + software.amazon.awssdk + test-utils + + + diff --git a/test/sdk-benchmarks/src/main/java/software/amazon/awssdk/benchmark/dynamodb/DynamoDbBenchmarkConstant.java b/test/sdk-benchmarks/src/main/java/software/amazon/awssdk/benchmark/dynamodb/DynamoDbBenchmarkConstant.java new file mode 100644 index 000000000000..521e1bf63690 --- /dev/null +++ b/test/sdk-benchmarks/src/main/java/software/amazon/awssdk/benchmark/dynamodb/DynamoDbBenchmarkConstant.java @@ -0,0 +1,43 @@ +/* + * Copyright 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. + */ + +package software.amazon.awssdk.benchmark.dynamodb; + +import software.amazon.awssdk.auth.credentials.AwsBasicCredentials; +import software.amazon.awssdk.auth.credentials.AwsCredentialsProvider; +import software.amazon.awssdk.auth.credentials.StaticCredentialsProvider; +import software.amazon.awssdk.regions.Region; + +/** + * Shared constants for DynamoDB performance benchmark infrastructure (mocked and live). + * Does not construct clients; factories and benchmarks consume these values. + */ +public final class DynamoDbBenchmarkConstant { + + public static final Region REGION = Region.US_EAST_1; + + public static final String TABLE_NAME = "sdk-java-ddb-perf-benchmark"; + + /** + * Deterministic fake credentials for mocked Tier C clients. Not used for live Tier D. + */ + public static final AwsCredentialsProvider MOCK_CREDENTIALS_PROVIDER = + StaticCredentialsProvider.create(AwsBasicCredentials.create("akid", "skid")); + + public static final String MOCK_ERROR_RESPONSE_BODY = "{}"; + + private DynamoDbBenchmarkConstant() { + } +} diff --git a/test/sdk-benchmarks/src/main/java/software/amazon/awssdk/benchmark/dynamodb/DynamoDbBenchmarkSystemSetting.java b/test/sdk-benchmarks/src/main/java/software/amazon/awssdk/benchmark/dynamodb/DynamoDbBenchmarkSystemSetting.java new file mode 100644 index 000000000000..0607d312f6ce --- /dev/null +++ b/test/sdk-benchmarks/src/main/java/software/amazon/awssdk/benchmark/dynamodb/DynamoDbBenchmarkSystemSetting.java @@ -0,0 +1,70 @@ +/* + * Copyright 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. + */ + +package software.amazon.awssdk.benchmark.dynamodb; + +import software.amazon.awssdk.utils.SystemSetting; + +/** + * Environment variables and system properties for DynamoDB Tier D live benchmarks. + * + *

System properties take precedence over environment variables (SDK {@link SystemSetting} rule). + */ +public enum DynamoDbBenchmarkSystemSetting implements SystemSetting { + + /** + * Explicit opt-in for live DynamoDB benchmarks ({@code true} required). + */ + LIVE_OPT_IN("dynamodb.benchmark.live", "DYNAMODB_BENCHMARK_LIVE"), + + /** + * Optional region override for live benchmarks. + */ + REGION("dynamodb.benchmark.region", "DYNAMODB_BENCHMARK_REGION"), + + /** + * Whether to attach the lightweight retry observer interceptor (default {@code true}). + */ + LIVE_RETRY_OBSERVE("dynamodb.benchmark.live.retryObserve", "DYNAMODB_BENCHMARK_LIVE_RETRY_OBSERVE", "true"); + + private final String property; + private final String environmentVariable; + private final String defaultValue; + + DynamoDbBenchmarkSystemSetting(String property, String environmentVariable) { + this(property, environmentVariable, null); + } + + DynamoDbBenchmarkSystemSetting(String property, String environmentVariable, String defaultValue) { + this.property = property; + this.environmentVariable = environmentVariable; + this.defaultValue = defaultValue; + } + + @Override + public String property() { + return property; + } + + @Override + public String environmentVariable() { + return environmentVariable; + } + + @Override + public String defaultValue() { + return defaultValue; + } +} diff --git a/test/sdk-benchmarks/src/main/java/software/amazon/awssdk/benchmark/dynamodb/fixture/BenchmarkItem.java b/test/sdk-benchmarks/src/main/java/software/amazon/awssdk/benchmark/dynamodb/fixture/BenchmarkItem.java new file mode 100644 index 000000000000..a18505db50c7 --- /dev/null +++ b/test/sdk-benchmarks/src/main/java/software/amazon/awssdk/benchmark/dynamodb/fixture/BenchmarkItem.java @@ -0,0 +1,170 @@ +/* + * Copyright 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. + */ + +package software.amazon.awssdk.benchmark.dynamodb.fixture; + +import java.util.List; +import java.util.Map; +import java.util.Objects; +import software.amazon.awssdk.enhanced.dynamodb.mapper.annotations.DynamoDbBean; +import software.amazon.awssdk.enhanced.dynamodb.mapper.annotations.DynamoDbPartitionKey; + +/** + * Representative typed item for DynamoDB performance benchmarks. + * Values are populated deterministically by {@link DynamoDbBenchmarkFixture}. + */ +@DynamoDbBean +public class BenchmarkItem { + + private String pk; + private String stringAttr; + private Integer numberAttr; + private Boolean boolAttr; + private List stringList; + private Map stringMap; + private NestedAttrs nested; + + @DynamoDbPartitionKey + public String getPk() { + return pk; + } + + public void setPk(String pk) { + this.pk = pk; + } + + public String getStringAttr() { + return stringAttr; + } + + public void setStringAttr(String stringAttr) { + this.stringAttr = stringAttr; + } + + public Integer getNumberAttr() { + return numberAttr; + } + + public void setNumberAttr(Integer numberAttr) { + this.numberAttr = numberAttr; + } + + public Boolean getBoolAttr() { + return boolAttr; + } + + public void setBoolAttr(Boolean boolAttr) { + this.boolAttr = boolAttr; + } + + public List getStringList() { + return stringList; + } + + public void setStringList(List stringList) { + this.stringList = stringList; + } + + public Map getStringMap() { + return stringMap; + } + + public void setStringMap(Map stringMap) { + this.stringMap = stringMap; + } + + public NestedAttrs getNested() { + return nested; + } + + public void setNested(NestedAttrs nested) { + this.nested = nested; + } + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (!(o instanceof BenchmarkItem)) { + return false; + } + BenchmarkItem that = (BenchmarkItem) o; + return Objects.equals(pk, that.pk) + && Objects.equals(stringAttr, that.stringAttr) + && Objects.equals(numberAttr, that.numberAttr) + && Objects.equals(boolAttr, that.boolAttr) + && Objects.equals(stringList, that.stringList) + && Objects.equals(stringMap, that.stringMap) + && Objects.equals(nested, that.nested); + } + + @Override + public int hashCode() { + int result = Objects.hashCode(pk); + result = 31 * result + Objects.hashCode(stringAttr); + result = 31 * result + Objects.hashCode(numberAttr); + result = 31 * result + Objects.hashCode(boolAttr); + result = 31 * result + Objects.hashCode(stringList); + result = 31 * result + Objects.hashCode(stringMap); + result = 31 * result + Objects.hashCode(nested); + return result; + } + + /** + * Nested document attributes for representative object complexity. + */ + @DynamoDbBean + public static class NestedAttrs { + private String nestedString; + private Integer nestedNumber; + + public String getNestedString() { + return nestedString; + } + + public void setNestedString(String nestedString) { + this.nestedString = nestedString; + } + + public Integer getNestedNumber() { + return nestedNumber; + } + + public void setNestedNumber(Integer nestedNumber) { + this.nestedNumber = nestedNumber; + } + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (!(o instanceof NestedAttrs)) { + return false; + } + NestedAttrs that = (NestedAttrs) o; + return Objects.equals(nestedString, that.nestedString) + && Objects.equals(nestedNumber, that.nestedNumber); + } + + @Override + public int hashCode() { + int result = Objects.hashCode(nestedString); + result = 31 * result + Objects.hashCode(nestedNumber); + return result; + } + } +} diff --git a/test/sdk-benchmarks/src/main/java/software/amazon/awssdk/benchmark/dynamodb/fixture/DynamoDbBenchmarkFixture.java b/test/sdk-benchmarks/src/main/java/software/amazon/awssdk/benchmark/dynamodb/fixture/DynamoDbBenchmarkFixture.java new file mode 100644 index 000000000000..4096d299d55e --- /dev/null +++ b/test/sdk-benchmarks/src/main/java/software/amazon/awssdk/benchmark/dynamodb/fixture/DynamoDbBenchmarkFixture.java @@ -0,0 +1,99 @@ +/* + * Copyright 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. + */ + +package software.amazon.awssdk.benchmark.dynamodb.fixture; + +import java.util.Arrays; +import java.util.Collections; +import java.util.LinkedHashMap; +import java.util.Map; +import software.amazon.awssdk.enhanced.dynamodb.TableSchema; +import software.amazon.awssdk.enhanced.dynamodb.document.EnhancedDocument; +import software.amazon.awssdk.services.dynamodb.model.AttributeValue; + +/** + * Deterministic DynamoDB item available as typed bean, {@link EnhancedDocument}, and low-level + * {@link AttributeValue} map. Does not construct SDK clients or JMH state. + */ +public final class DynamoDbBenchmarkFixture { + + public static final String PARTITION_KEY_NAME = "pk"; + public static final String PARTITION_KEY_VALUE = "benchmark-item-1"; + + private static final TableSchema TABLE_SCHEMA = TableSchema.fromBean(BenchmarkItem.class); + + private final BenchmarkItem item; + private final Map attributeMap; + private final EnhancedDocument document; + + private DynamoDbBenchmarkFixture(BenchmarkItem item, + Map attributeMap, + EnhancedDocument document) { + this.item = item; + this.attributeMap = attributeMap; + this.document = document; + } + + /** + * Creates the shared representative fixture used across LOW / DOCUMENT / TYPED benchmarks. + */ + public static DynamoDbBenchmarkFixture create() { + BenchmarkItem item = newItem(); + Map attributeMap = + Collections.unmodifiableMap(new LinkedHashMap<>(TABLE_SCHEMA.itemToMap(item, true))); + EnhancedDocument document = EnhancedDocument.fromAttributeValueMap(attributeMap); + return new DynamoDbBenchmarkFixture(item, attributeMap, document); + } + + public BenchmarkItem item() { + return item; + } + + public Map attributeMap() { + return attributeMap; + } + + public EnhancedDocument document() { + return document; + } + + public TableSchema tableSchema() { + return TABLE_SCHEMA; + } + + public AttributeValue partitionKeyAttribute() { + return AttributeValue.fromS(PARTITION_KEY_VALUE); + } + + private static BenchmarkItem newItem() { + BenchmarkItem.NestedAttrs nested = new BenchmarkItem.NestedAttrs(); + nested.setNestedString("nested-value"); + nested.setNestedNumber(7); + + Map stringMap = new LinkedHashMap<>(); + stringMap.put("mapKeyA", "mapValueA"); + stringMap.put("mapKeyB", "mapValueB"); + + BenchmarkItem item = new BenchmarkItem(); + item.setPk(PARTITION_KEY_VALUE); + item.setStringAttr("benchmark-string"); + item.setNumberAttr(42); + item.setBoolAttr(true); + item.setStringList(Arrays.asList("list-one", "list-two", "list-three")); + item.setStringMap(stringMap); + item.setNested(nested); + return item; + } +} diff --git a/test/sdk-benchmarks/src/main/java/software/amazon/awssdk/benchmark/dynamodb/live/LiveBenchmarkGuard.java b/test/sdk-benchmarks/src/main/java/software/amazon/awssdk/benchmark/dynamodb/live/LiveBenchmarkGuard.java new file mode 100644 index 000000000000..27fb419d22fe --- /dev/null +++ b/test/sdk-benchmarks/src/main/java/software/amazon/awssdk/benchmark/dynamodb/live/LiveBenchmarkGuard.java @@ -0,0 +1,47 @@ +/* + * Copyright 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. + */ + +package software.amazon.awssdk.benchmark.dynamodb.live; + +import software.amazon.awssdk.benchmark.dynamodb.DynamoDbBenchmarkSystemSetting; + +/** + * Hard safety gate for Tier D live DynamoDB benchmarks. + * + *

Must be invoked before credential resolution, client construction, or any AWS interaction. + */ +public final class LiveBenchmarkGuard { + + private LiveBenchmarkGuard() { + } + + /** + * @throws IllegalStateException if live opt-in is not present + */ + public static void requireLiveOptIn() { + if (!isLiveOptInEnabled()) { + throw new IllegalStateException( + "Live DynamoDB benchmarks are disabled. Refusing to resolve credentials, build " + + "clients, or create AWS resources. Set " + + DynamoDbBenchmarkSystemSetting.LIVE_OPT_IN.environmentVariable() + + "=true or -D" + DynamoDbBenchmarkSystemSetting.LIVE_OPT_IN.property() + + "=true to opt in explicitly."); + } + } + + public static boolean isLiveOptInEnabled() { + return DynamoDbBenchmarkSystemSetting.LIVE_OPT_IN.getBooleanValue().orElse(false); + } +} diff --git a/test/sdk-benchmarks/src/main/java/software/amazon/awssdk/benchmark/dynamodb/live/LiveDynamoDbSupport.java b/test/sdk-benchmarks/src/main/java/software/amazon/awssdk/benchmark/dynamodb/live/LiveDynamoDbSupport.java new file mode 100644 index 000000000000..0f466b9ec5cb --- /dev/null +++ b/test/sdk-benchmarks/src/main/java/software/amazon/awssdk/benchmark/dynamodb/live/LiveDynamoDbSupport.java @@ -0,0 +1,277 @@ +/* + * Copyright 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. + */ + +package software.amazon.awssdk.benchmark.dynamodb.live; + +import java.util.UUID; +import software.amazon.awssdk.auth.credentials.DefaultCredentialsProvider; +import software.amazon.awssdk.benchmark.dynamodb.DynamoDbBenchmarkSystemSetting; +import software.amazon.awssdk.benchmark.dynamodb.fixture.BenchmarkItem; +import software.amazon.awssdk.benchmark.dynamodb.fixture.DynamoDbBenchmarkFixture; +import software.amazon.awssdk.enhanced.dynamodb.DynamoDbEnhancedClient; +import software.amazon.awssdk.enhanced.dynamodb.DynamoDbTable; +import software.amazon.awssdk.enhanced.dynamodb.Key; +import software.amazon.awssdk.regions.Region; +import software.amazon.awssdk.regions.providers.DefaultAwsRegionProviderChain; +import software.amazon.awssdk.services.dynamodb.DynamoDbClient; +import software.amazon.awssdk.services.dynamodb.model.AttributeDefinition; +import software.amazon.awssdk.services.dynamodb.model.BillingMode; +import software.amazon.awssdk.services.dynamodb.model.CreateTableRequest; +import software.amazon.awssdk.services.dynamodb.model.DeleteTableRequest; +import software.amazon.awssdk.services.dynamodb.model.KeySchemaElement; +import software.amazon.awssdk.services.dynamodb.model.KeyType; +import software.amazon.awssdk.services.dynamodb.model.ResourceInUseException; +import software.amazon.awssdk.services.dynamodb.model.ScalarAttributeType; +import software.amazon.awssdk.services.dynamodb.model.Tag; +import software.amazon.awssdk.utils.Logger; +import software.amazon.awssdk.utils.SdkAutoCloseable; + +/** + * Shared Tier D live table lifecycle: opt-in gate, client, unique owned table, seed, teardown. + * + *

Automatic teardown deletes only the exact table created by this trial and only when + * {@code createdByThisTrial} is true. Tags identify orphaned tables after interrupted runs. + */ +public final class LiveDynamoDbSupport implements SdkAutoCloseable { + + public static final String OWNERSHIP_TAG_KEY = "sdk-java-ddb-perf-benchmark"; + public static final String OWNERSHIP_TAG_VALUE = "owned"; + public static final String PURPOSE_TAG_KEY = "Purpose"; + public static final String PURPOSE_TAG_VALUE = "aws-sdk-java-v2-dynamodb-live-benchmark"; + + private static final Logger LOG = Logger.loggerFor(LiveDynamoDbSupport.class); + + private final DynamoDbClient client; + private final DynamoDbEnhancedClient enhancedClient; + private final DynamoDbBenchmarkFixture fixture; + private final String tableName; + private final Region region; + private final boolean createdByThisTrial; + private final LiveRetryObserver retryObserver; + private final boolean retryObserverEnabled; + private boolean closed; + + private LiveDynamoDbSupport(DynamoDbClient client, + DynamoDbEnhancedClient enhancedClient, + DynamoDbBenchmarkFixture fixture, + String tableName, + Region region, + boolean createdByThisTrial, + LiveRetryObserver retryObserver, + boolean retryObserverEnabled) { + this.client = client; + this.enhancedClient = enhancedClient; + this.fixture = fixture; + this.tableName = tableName; + this.region = region; + this.createdByThisTrial = createdByThisTrial; + this.retryObserver = retryObserver; + this.retryObserverEnabled = retryObserverEnabled; + } + + /** + * Opens a live support context. The opt-in guard runs before any credential or AWS work. + * + * @param operationLabel short label embedded in the unique table name (e.g. {@code get}, {@code typedquery}) + */ + public static LiveDynamoDbSupport open(String operationLabel) { + LiveBenchmarkGuard.requireLiveOptIn(); + + Region region = resolveRegion(); + boolean retryObserve = LiveRetryObserver.isEnabled(); + LiveRetryObserver observer = retryObserve ? new LiveRetryObserver() : null; + + DynamoDbClient client = DynamoDbClient.builder() + .region(region) + .credentialsProvider(DefaultCredentialsProvider.create()) + .overrideConfiguration(o -> { + if (observer != null) { + o.addExecutionInterceptor(observer); + } + }) + .build(); + + String tableName = newUniqueTableName(operationLabel); + boolean created = false; + try { + createOwnedTable(client, tableName); + created = true; + client.waiter().waitUntilTableExists(b -> b.tableName(tableName)); + + DynamoDbBenchmarkFixture fixture = DynamoDbBenchmarkFixture.create(); + client.putItem(r -> r.tableName(tableName).item(fixture.attributeMap())); + + DynamoDbEnhancedClient enhancedClient = DynamoDbEnhancedClient.builder() + .dynamoDbClient(client) + .build(); + + LOG.info(() -> "region=" + region.id() + + " table=" + tableName + + " billing=PAY_PER_REQUEST" + + " createdByThisTrial=true" + + " retryObserver=" + retryObserve); + + return new LiveDynamoDbSupport(client, enhancedClient, fixture, tableName, region, created, + observer, retryObserve); + } catch (RuntimeException e) { + if (created) { + safeDelete(client, tableName); + } + client.close(); + throw e; + } + } + + public DynamoDbClient client() { + return client; + } + + public DynamoDbEnhancedClient enhancedClient() { + return enhancedClient; + } + + public DynamoDbTable typedTable() { + return enhancedClient.table(tableName, fixture.tableSchema()); + } + + public DynamoDbBenchmarkFixture fixture() { + return fixture; + } + + public String tableName() { + return tableName; + } + + public Region region() { + return region; + } + + public Key partitionKey() { + return Key.builder() + .partitionValue(DynamoDbBenchmarkFixture.PARTITION_KEY_VALUE) + .build(); + } + + public LiveRetryObserver retryObserver() { + return retryObserver; + } + + public boolean retryObserverEnabled() { + return retryObserverEnabled; + } + + @Override + public void close() { + if (closed) { + return; + } + closed = true; + try { + if (retryObserverEnabled && retryObserver != null) { + LOG.info(() -> "retryObserver: " + retryObserver.summary()); + if (retryObserver.observedRetries()) { + LOG.warn(() -> "LIVE BENCHMARK WARNING: retries observed — interpret scores with caution " + + "(hidden retries change latency interpretation). " + + retryObserver.summary()); + } + } + } finally { + try { + if (createdByThisTrial) { + safeDelete(client, tableName); + LOG.info(() -> "deleted owned table=" + tableName); + } else { + LOG.info(() -> "skipping delete (createdByThisTrial=false) table=" + tableName); + } + } finally { + client.close(); + } + } + } + + private static Region resolveRegion() { + return DynamoDbBenchmarkSystemSetting.REGION.getStringValue() + .filter(v -> !v.trim().isEmpty()) + .map(v -> Region.of(v.trim())) + .orElseGet(LiveDynamoDbSupport::resolveDefaultRegion); + } + + private static Region resolveDefaultRegion() { + try { + return new DefaultAwsRegionProviderChain().getRegion(); + } catch (RuntimeException e) { + throw new IllegalStateException( + "Unable to resolve AWS region for live DynamoDB benchmarks. Set " + + DynamoDbBenchmarkSystemSetting.REGION.environmentVariable() + + ", -D" + DynamoDbBenchmarkSystemSetting.REGION.property() + + ", AWS_REGION, or configure a default region in the standard provider chain.", + e); + } + } + + private static String newUniqueTableName(String operationLabel) { + String safeLabel = operationLabel == null ? "op" : operationLabel.replaceAll("[^A-Za-z0-9_-]", ""); + if (safeLabel.isEmpty()) { + safeLabel = "op"; + } + String suffix = UUID.randomUUID().toString().replace("-", "").substring(0, 8); + String name = "sdk-java-ddb-perf-" + safeLabel + "-" + suffix; + if (name.length() > 255) { + throw new IllegalStateException("Generated table name exceeds DynamoDB limit: " + name); + } + return name; + } + + private static void createOwnedTable(DynamoDbClient client, String tableName) { + String partitionKeyName = DynamoDbBenchmarkFixture.PARTITION_KEY_NAME; + try { + client.createTable(CreateTableRequest.builder() + .tableName(tableName) + .billingMode(BillingMode.PAY_PER_REQUEST) + .attributeDefinitions(AttributeDefinition.builder() + .attributeName(partitionKeyName) + .attributeType(ScalarAttributeType.S) + .build()) + .keySchema(KeySchemaElement.builder() + .attributeName(partitionKeyName) + .keyType(KeyType.HASH) + .build()) + .tags( + Tag.builder() + .key(OWNERSHIP_TAG_KEY) + .value(OWNERSHIP_TAG_VALUE) + .build(), + Tag.builder() + .key(PURPOSE_TAG_KEY) + .value(PURPOSE_TAG_VALUE) + .build()) + .build()); + } catch (ResourceInUseException e) { + throw new IllegalStateException( + "Refusing to adopt pre-existing table '" + tableName + + "'. Live benchmarks only use uniquely named tables created by this trial.", + e); + } + } + + private static void safeDelete(DynamoDbClient client, String tableName) { + try { + client.deleteTable(DeleteTableRequest.builder().tableName(tableName).build()); + client.waiter().waitUntilTableNotExists(b -> b.tableName(tableName)); + } catch (RuntimeException e) { + LOG.warn(() -> "failed to delete owned table=" + tableName + " : " + e); + } + } +} diff --git a/test/sdk-benchmarks/src/main/java/software/amazon/awssdk/benchmark/dynamodb/live/LiveRetryObserver.java b/test/sdk-benchmarks/src/main/java/software/amazon/awssdk/benchmark/dynamodb/live/LiveRetryObserver.java new file mode 100644 index 000000000000..bb9e304c718a --- /dev/null +++ b/test/sdk-benchmarks/src/main/java/software/amazon/awssdk/benchmark/dynamodb/live/LiveRetryObserver.java @@ -0,0 +1,131 @@ +/* + * Copyright 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. + */ + +package software.amazon.awssdk.benchmark.dynamodb.live; + +import java.util.concurrent.atomic.AtomicInteger; +import software.amazon.awssdk.benchmark.dynamodb.DynamoDbBenchmarkSystemSetting; +import software.amazon.awssdk.core.exception.SdkException; +import software.amazon.awssdk.core.interceptor.Context; +import software.amazon.awssdk.core.interceptor.ExecutionAttribute; +import software.amazon.awssdk.core.interceptor.ExecutionAttributes; +import software.amazon.awssdk.core.interceptor.ExecutionInterceptor; +import software.amazon.awssdk.core.retry.RetryUtils; + +/** + * Low-impact retry / failure observer for live benchmarks. + * + *

Uses a public {@link ExecutionInterceptor} and counts HTTP transmissions per API call + * ({@code beforeTransmission} runs once per attempt). This avoids attaching a + * {@code MetricPublisher}, which would add publish overhead on the hot path. + * + *

Enabled by default for live clients. Disable with + * {@code DYNAMODB_BENCHMARK_LIVE_RETRY_OBSERVE=false} or + * {@code -Ddynamodb.benchmark.live.retryObserve=false} for an unmodified default client path. + */ +final class LiveRetryObserver implements ExecutionInterceptor { + + private static final ExecutionAttribute ATTEMPTS_THIS_CALL = + new ExecutionAttribute<>("LiveDynamoDbBenchmarkAttempts"); + + private final AtomicInteger completedCalls = new AtomicInteger(); + private final AtomicInteger failedCalls = new AtomicInteger(); + private final AtomicInteger callsWithRetries = new AtomicInteger(); + private final AtomicInteger totalAttempts = new AtomicInteger(); + private final AtomicInteger maxAttemptsOnSingleCall = new AtomicInteger(); + private final AtomicInteger throttlingFailures = new AtomicInteger(); + + static boolean isEnabled() { + return DynamoDbBenchmarkSystemSetting.LIVE_RETRY_OBSERVE.getBooleanValue().orElse(true); + } + + @Override + public void beforeTransmission(Context.BeforeTransmission context, ExecutionAttributes executionAttributes) { + AtomicInteger attempts = executionAttributes.getAttribute(ATTEMPTS_THIS_CALL); + if (attempts == null) { + attempts = new AtomicInteger(); + executionAttributes.putAttribute(ATTEMPTS_THIS_CALL, attempts); + } + int attempt = attempts.incrementAndGet(); + totalAttempts.incrementAndGet(); + maxAttemptsOnSingleCall.accumulateAndGet(attempt, Math::max); + } + + @Override + public void afterExecution(Context.AfterExecution context, ExecutionAttributes executionAttributes) { + completedCalls.incrementAndGet(); + recordRetryIfNeeded(executionAttributes); + } + + @Override + public void onExecutionFailure(Context.FailedExecution context, ExecutionAttributes executionAttributes) { + failedCalls.incrementAndGet(); + recordRetryIfNeeded(executionAttributes); + Throwable exception = context.exception(); + if (exception instanceof SdkException && RetryUtils.isThrottlingException((SdkException) exception)) { + throttlingFailures.incrementAndGet(); + } + if (exception instanceof SdkException) { + Integer numAttempts = ((SdkException) exception).numAttempts(); + if (numAttempts != null) { + maxAttemptsOnSingleCall.accumulateAndGet(numAttempts, Math::max); + } + } + } + + private void recordRetryIfNeeded(ExecutionAttributes executionAttributes) { + AtomicInteger attempts = executionAttributes.getAttribute(ATTEMPTS_THIS_CALL); + if (attempts != null && attempts.get() > 1) { + callsWithRetries.incrementAndGet(); + } + } + + int completedCalls() { + return completedCalls.get(); + } + + int failedCalls() { + return failedCalls.get(); + } + + int callsWithRetries() { + return callsWithRetries.get(); + } + + int totalAttempts() { + return totalAttempts.get(); + } + + int maxAttemptsOnSingleCall() { + return maxAttemptsOnSingleCall.get(); + } + + int throttlingFailures() { + return throttlingFailures.get(); + } + + boolean observedRetries() { + return callsWithRetries.get() > 0 || maxAttemptsOnSingleCall.get() > 1; + } + + String summary() { + return "completedCalls=" + completedCalls.get() + + ", failedCalls=" + failedCalls.get() + + ", callsWithRetries=" + callsWithRetries.get() + + ", totalAttempts=" + totalAttempts.get() + + ", maxAttemptsOnSingleCall=" + maxAttemptsOnSingleCall.get() + + ", throttlingFailures=" + throttlingFailures.get(); + } +} diff --git a/test/sdk-benchmarks/src/main/java/software/amazon/awssdk/benchmark/dynamodb/live/LowLevelLiveGetItemBenchmark.java b/test/sdk-benchmarks/src/main/java/software/amazon/awssdk/benchmark/dynamodb/live/LowLevelLiveGetItemBenchmark.java new file mode 100644 index 000000000000..840d2a0a188b --- /dev/null +++ b/test/sdk-benchmarks/src/main/java/software/amazon/awssdk/benchmark/dynamodb/live/LowLevelLiveGetItemBenchmark.java @@ -0,0 +1,84 @@ +/* + * Copyright 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. + */ + +package software.amazon.awssdk.benchmark.dynamodb.live; + +import java.util.Collections; +import java.util.concurrent.TimeUnit; +import org.openjdk.jmh.annotations.Benchmark; +import org.openjdk.jmh.annotations.BenchmarkMode; +import org.openjdk.jmh.annotations.Fork; +import org.openjdk.jmh.annotations.Level; +import org.openjdk.jmh.annotations.Measurement; +import org.openjdk.jmh.annotations.Mode; +import org.openjdk.jmh.annotations.OutputTimeUnit; +import org.openjdk.jmh.annotations.Scope; +import org.openjdk.jmh.annotations.Setup; +import org.openjdk.jmh.annotations.State; +import org.openjdk.jmh.annotations.TearDown; +import org.openjdk.jmh.annotations.Warmup; +import software.amazon.awssdk.benchmark.dynamodb.fixture.DynamoDbBenchmarkFixture; +import software.amazon.awssdk.services.dynamodb.model.GetItemRequest; +import software.amazon.awssdk.services.dynamodb.model.GetItemResponse; + +/** + * Tier D LOW-level live GetItem: end-to-end latency including network and DynamoDB service. + * + *

Requires explicit opt-in ({@code DYNAMODB_BENCHMARK_LIVE=true} or + * {@code -Ddynamodb.benchmark.live=true}). Not selected by {@code BenchmarkRunner}. + * + *

The untimed smoke GetItem (DNS/TLS/connection-pool init) is distinct from JMH warmup + * iterations configured below. + */ +@State(Scope.Benchmark) +@BenchmarkMode(Mode.SampleTime) +@OutputTimeUnit(TimeUnit.MILLISECONDS) +@Warmup(iterations = 3) +@Measurement(iterations = 5) +@Fork(1) +public class LowLevelLiveGetItemBenchmark { + + private LiveDynamoDbSupport support; + private GetItemRequest request; + + @Setup(Level.Trial) + public void setup() { + support = LiveDynamoDbSupport.open("get"); + request = GetItemRequest.builder() + .tableName(support.tableName()) + .key(Collections.singletonMap( + DynamoDbBenchmarkFixture.PARTITION_KEY_NAME, + support.fixture().partitionKeyAttribute())) + .build(); + + // Untimed smoke call: DNS/TLS/connection-pool initialization. Distinct from JMH warmup. + GetItemResponse smoke = support.client().getItem(request); + if (smoke.item() == null || smoke.item().isEmpty()) { + throw new IllegalStateException("Live GetItem smoke call did not return a non-empty item"); + } + } + + @TearDown(Level.Trial) + public void tearDown() { + if (support != null) { + support.close(); + } + } + + @Benchmark + public GetItemResponse getItem() { + return support.client().getItem(request); + } +} diff --git a/test/sdk-benchmarks/src/main/java/software/amazon/awssdk/benchmark/dynamodb/live/LowLevelLivePutItemBenchmark.java b/test/sdk-benchmarks/src/main/java/software/amazon/awssdk/benchmark/dynamodb/live/LowLevelLivePutItemBenchmark.java new file mode 100644 index 000000000000..548d6e9d81e4 --- /dev/null +++ b/test/sdk-benchmarks/src/main/java/software/amazon/awssdk/benchmark/dynamodb/live/LowLevelLivePutItemBenchmark.java @@ -0,0 +1,80 @@ +/* + * Copyright 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. + */ + +package software.amazon.awssdk.benchmark.dynamodb.live; + +import java.util.concurrent.TimeUnit; +import org.openjdk.jmh.annotations.Benchmark; +import org.openjdk.jmh.annotations.BenchmarkMode; +import org.openjdk.jmh.annotations.Fork; +import org.openjdk.jmh.annotations.Level; +import org.openjdk.jmh.annotations.Measurement; +import org.openjdk.jmh.annotations.Mode; +import org.openjdk.jmh.annotations.OutputTimeUnit; +import org.openjdk.jmh.annotations.Scope; +import org.openjdk.jmh.annotations.Setup; +import org.openjdk.jmh.annotations.State; +import org.openjdk.jmh.annotations.TearDown; +import org.openjdk.jmh.annotations.Warmup; +import software.amazon.awssdk.services.dynamodb.model.PutItemRequest; +import software.amazon.awssdk.services.dynamodb.model.PutItemResponse; + +/** + * Tier D LOW-level live PutItem: end-to-end latency including network and DynamoDB service. + * + *

Requires explicit opt-in ({@code DYNAMODB_BENCHMARK_LIVE=true} or + * {@code -Ddynamodb.benchmark.live=true}). Not selected by {@code BenchmarkRunner}. + * + *

The untimed smoke PutItem (DNS/TLS/connection-pool init) is distinct from JMH warmup + * iterations configured below. + */ +@State(Scope.Benchmark) +@BenchmarkMode(Mode.SampleTime) +@OutputTimeUnit(TimeUnit.MILLISECONDS) +@Warmup(iterations = 3) +@Measurement(iterations = 5) +@Fork(1) +public class LowLevelLivePutItemBenchmark { + + private LiveDynamoDbSupport support; + private PutItemRequest request; + + @Setup(Level.Trial) + public void setup() { + support = LiveDynamoDbSupport.open("put"); + request = PutItemRequest.builder() + .tableName(support.tableName()) + .item(support.fixture().attributeMap()) + .build(); + + // Untimed smoke call: DNS/TLS/connection-pool initialization. Distinct from JMH warmup. + PutItemResponse smoke = support.client().putItem(request); + if (smoke == null) { + throw new IllegalStateException("Live PutItem smoke call returned null"); + } + } + + @TearDown(Level.Trial) + public void tearDown() { + if (support != null) { + support.close(); + } + } + + @Benchmark + public PutItemResponse putItem() { + return support.client().putItem(request); + } +} diff --git a/test/sdk-benchmarks/src/main/java/software/amazon/awssdk/benchmark/dynamodb/live/TypedLiveGetItemBenchmark.java b/test/sdk-benchmarks/src/main/java/software/amazon/awssdk/benchmark/dynamodb/live/TypedLiveGetItemBenchmark.java new file mode 100644 index 000000000000..c6da87b10b53 --- /dev/null +++ b/test/sdk-benchmarks/src/main/java/software/amazon/awssdk/benchmark/dynamodb/live/TypedLiveGetItemBenchmark.java @@ -0,0 +1,80 @@ +/* + * Copyright 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. + */ + +package software.amazon.awssdk.benchmark.dynamodb.live; + +import java.util.concurrent.TimeUnit; +import org.openjdk.jmh.annotations.Benchmark; +import org.openjdk.jmh.annotations.BenchmarkMode; +import org.openjdk.jmh.annotations.Fork; +import org.openjdk.jmh.annotations.Level; +import org.openjdk.jmh.annotations.Measurement; +import org.openjdk.jmh.annotations.Mode; +import org.openjdk.jmh.annotations.OutputTimeUnit; +import org.openjdk.jmh.annotations.Scope; +import org.openjdk.jmh.annotations.Setup; +import org.openjdk.jmh.annotations.State; +import org.openjdk.jmh.annotations.TearDown; +import org.openjdk.jmh.annotations.Warmup; +import software.amazon.awssdk.benchmark.dynamodb.fixture.BenchmarkItem; +import software.amazon.awssdk.enhanced.dynamodb.DynamoDbTable; +import software.amazon.awssdk.enhanced.dynamodb.Key; + +/** + * Tier D TYPED live GetItem: Enhanced mapping plus end-to-end DynamoDB latency. + * + *

Requires explicit opt-in ({@code DYNAMODB_BENCHMARK_LIVE=true} or + * {@code -Ddynamodb.benchmark.live=true}). Not selected by {@code BenchmarkRunner}. + * + *

The untimed smoke GetItem (DNS/TLS/connection-pool init) is distinct from JMH warmup + * iterations configured below. TYPED−LOW is directional only under live variance. + */ +@State(Scope.Benchmark) +@BenchmarkMode(Mode.SampleTime) +@OutputTimeUnit(TimeUnit.MILLISECONDS) +@Warmup(iterations = 3) +@Measurement(iterations = 5) +@Fork(1) +public class TypedLiveGetItemBenchmark { + + private LiveDynamoDbSupport support; + private DynamoDbTable table; + private Key key; + + @Setup(Level.Trial) + public void setup() { + support = LiveDynamoDbSupport.open("typedget"); + table = support.typedTable(); + key = support.partitionKey(); + + // Untimed smoke call: DNS/TLS/connection-pool initialization. Distinct from JMH warmup. + BenchmarkItem smoke = table.getItem(key); + if (smoke == null || smoke.getPk() == null) { + throw new IllegalStateException("Typed live GetItem smoke call did not map a non-null item"); + } + } + + @TearDown(Level.Trial) + public void tearDown() { + if (support != null) { + support.close(); + } + } + + @Benchmark + public BenchmarkItem getItem() { + return table.getItem(key); + } +} diff --git a/test/sdk-benchmarks/src/main/java/software/amazon/awssdk/benchmark/dynamodb/live/TypedLivePutItemBenchmark.java b/test/sdk-benchmarks/src/main/java/software/amazon/awssdk/benchmark/dynamodb/live/TypedLivePutItemBenchmark.java new file mode 100644 index 000000000000..ffe3113ccdd8 --- /dev/null +++ b/test/sdk-benchmarks/src/main/java/software/amazon/awssdk/benchmark/dynamodb/live/TypedLivePutItemBenchmark.java @@ -0,0 +1,76 @@ +/* + * Copyright 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. + */ + +package software.amazon.awssdk.benchmark.dynamodb.live; + +import java.util.concurrent.TimeUnit; +import org.openjdk.jmh.annotations.Benchmark; +import org.openjdk.jmh.annotations.BenchmarkMode; +import org.openjdk.jmh.annotations.Fork; +import org.openjdk.jmh.annotations.Level; +import org.openjdk.jmh.annotations.Measurement; +import org.openjdk.jmh.annotations.Mode; +import org.openjdk.jmh.annotations.OutputTimeUnit; +import org.openjdk.jmh.annotations.Scope; +import org.openjdk.jmh.annotations.Setup; +import org.openjdk.jmh.annotations.State; +import org.openjdk.jmh.annotations.TearDown; +import org.openjdk.jmh.annotations.Warmup; +import software.amazon.awssdk.benchmark.dynamodb.fixture.BenchmarkItem; +import software.amazon.awssdk.enhanced.dynamodb.DynamoDbTable; + +/** + * Tier D TYPED live PutItem: Enhanced mapping plus end-to-end DynamoDB latency. + * + *

Requires explicit opt-in ({@code DYNAMODB_BENCHMARK_LIVE=true} or + * {@code -Ddynamodb.benchmark.live=true}). Not selected by {@code BenchmarkRunner}. + * + *

The untimed smoke PutItem (DNS/TLS/connection-pool init) is distinct from JMH warmup + * iterations configured below. TYPED−LOW is directional only under live variance. + */ +@State(Scope.Benchmark) +@BenchmarkMode(Mode.SampleTime) +@OutputTimeUnit(TimeUnit.MILLISECONDS) +@Warmup(iterations = 3) +@Measurement(iterations = 5) +@Fork(1) +public class TypedLivePutItemBenchmark { + + private LiveDynamoDbSupport support; + private DynamoDbTable table; + private BenchmarkItem item; + + @Setup(Level.Trial) + public void setup() { + support = LiveDynamoDbSupport.open("typedput"); + table = support.typedTable(); + item = support.fixture().item(); + + // Untimed smoke call: DNS/TLS/connection-pool initialization. Distinct from JMH warmup. + table.putItem(item); + } + + @TearDown(Level.Trial) + public void tearDown() { + if (support != null) { + support.close(); + } + } + + @Benchmark + public void putItem() { + table.putItem(item); + } +} diff --git a/test/sdk-benchmarks/src/main/java/software/amazon/awssdk/benchmark/dynamodb/live/TypedLiveQueryBenchmark.java b/test/sdk-benchmarks/src/main/java/software/amazon/awssdk/benchmark/dynamodb/live/TypedLiveQueryBenchmark.java new file mode 100644 index 000000000000..256e8cdb9d6f --- /dev/null +++ b/test/sdk-benchmarks/src/main/java/software/amazon/awssdk/benchmark/dynamodb/live/TypedLiveQueryBenchmark.java @@ -0,0 +1,85 @@ +/* + * Copyright 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. + */ + +package software.amazon.awssdk.benchmark.dynamodb.live; + +import java.util.concurrent.TimeUnit; +import org.openjdk.jmh.annotations.Benchmark; +import org.openjdk.jmh.annotations.BenchmarkMode; +import org.openjdk.jmh.annotations.Fork; +import org.openjdk.jmh.annotations.Level; +import org.openjdk.jmh.annotations.Measurement; +import org.openjdk.jmh.annotations.Mode; +import org.openjdk.jmh.annotations.OutputTimeUnit; +import org.openjdk.jmh.annotations.Scope; +import org.openjdk.jmh.annotations.Setup; +import org.openjdk.jmh.annotations.State; +import org.openjdk.jmh.annotations.TearDown; +import org.openjdk.jmh.annotations.Warmup; +import software.amazon.awssdk.benchmark.dynamodb.fixture.BenchmarkItem; +import software.amazon.awssdk.enhanced.dynamodb.DynamoDbTable; +import software.amazon.awssdk.enhanced.dynamodb.model.Page; +import software.amazon.awssdk.enhanced.dynamodb.model.QueryConditional; + +/** + * Tier D TYPED live Query (first page only): Enhanced mapping plus end-to-end DynamoDB latency. + * + *

Uses the same deterministic fixture/table setup as other live benches. Requires explicit + * opt-in ({@code DYNAMODB_BENCHMARK_LIVE=true} or {@code -Ddynamodb.benchmark.live=true}). + * Not selected by {@code BenchmarkRunner}. + * + *

The untimed smoke Query (DNS/TLS/connection-pool init) is distinct from JMH warmup + * iterations configured below. + */ +@State(Scope.Benchmark) +@BenchmarkMode(Mode.SampleTime) +@OutputTimeUnit(TimeUnit.MILLISECONDS) +@Warmup(iterations = 3) +@Measurement(iterations = 5) +@Fork(1) +public class TypedLiveQueryBenchmark { + + private LiveDynamoDbSupport support; + private DynamoDbTable table; + private QueryConditional queryConditional; + + @Setup(Level.Trial) + public void setup() { + support = LiveDynamoDbSupport.open("typedquery"); + table = support.typedTable(); + queryConditional = QueryConditional.keyEqualTo(support.partitionKey()); + + // Untimed smoke call: DNS/TLS/connection-pool initialization. Distinct from JMH warmup. + Page smoke = table.query(queryConditional).iterator().next(); + if (smoke == null || smoke.items() == null || smoke.items().isEmpty()) { + throw new IllegalStateException("Typed live Query smoke call did not return at least one item"); + } + } + + @TearDown(Level.Trial) + public void tearDown() { + if (support != null) { + support.close(); + } + } + + /** + * First page only for this phase. + */ + @Benchmark + public Page query() { + return table.query(queryConditional).iterator().next(); + } +} diff --git a/test/sdk-benchmarks/src/main/java/software/amazon/awssdk/benchmark/dynamodb/mock/DynamoDbMockClientFactory.java b/test/sdk-benchmarks/src/main/java/software/amazon/awssdk/benchmark/dynamodb/mock/DynamoDbMockClientFactory.java new file mode 100644 index 000000000000..621ab7f5f9db --- /dev/null +++ b/test/sdk-benchmarks/src/main/java/software/amazon/awssdk/benchmark/dynamodb/mock/DynamoDbMockClientFactory.java @@ -0,0 +1,101 @@ +/* + * Copyright 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. + */ + +package software.amazon.awssdk.benchmark.dynamodb.mock; + +import java.util.Map; +import software.amazon.awssdk.benchmark.dynamodb.DynamoDbBenchmarkConstant; +import software.amazon.awssdk.benchmark.utils.MockHttpClient; +import software.amazon.awssdk.http.async.SdkAsyncHttpClient; +import software.amazon.awssdk.services.dynamodb.DynamoDbAsyncClient; +import software.amazon.awssdk.services.dynamodb.DynamoDbClient; +import software.amazon.awssdk.services.dynamodb.model.AttributeValue; +import software.amazon.awssdk.utils.SdkAutoCloseable; + +/** + * Factory for sync/async DynamoDB clients backed by deterministic mock HTTP responses. + */ +public final class DynamoDbMockClientFactory { + + private DynamoDbMockClientFactory() { + } + + public static DynamoDbClient syncClient(String successResponseBody) { + MockHttpClient httpClient = new MockHttpClient( + successResponseBody, + DynamoDbBenchmarkConstant.MOCK_ERROR_RESPONSE_BODY); + return DynamoDbClient.builder() + .credentialsProvider(DynamoDbBenchmarkConstant.MOCK_CREDENTIALS_PROVIDER) + .region(DynamoDbBenchmarkConstant.REGION) + .httpClient(httpClient) + .build(); + } + + public static DynamoDbClient syncGetItemClient(Map item) { + return syncClient(DynamoDbMockResponseFactory.getItemResponseBody(item)); + } + + public static DynamoDbClient syncPutItemClient() { + return syncClient(DynamoDbMockResponseFactory.putItemResponseBody()); + } + + public static DynamoDbClient syncQueryClient(Map item) { + return syncClient(DynamoDbMockResponseFactory.queryResponseBody(item)); + } + + /** + * Async GetItem client using the same GetItem response bytes as the sync Get benchmarks. + * Caller must {@link AsyncMockResources#close()} to shut down the mock executor. + */ + public static AsyncMockResources asyncGetItemClient(Map item) { + byte[] body = DynamoDbMockResponseFactory.getItemResponseBytes(item); + SdkAsyncHttpClient httpClient = new ReplayingMockAsyncHttpClient(body); + DynamoDbAsyncClient client = DynamoDbAsyncClient.builder() + .credentialsProvider( + DynamoDbBenchmarkConstant.MOCK_CREDENTIALS_PROVIDER) + .region(DynamoDbBenchmarkConstant.REGION) + .httpClient(httpClient) + .build(); + return new AsyncMockResources(client, httpClient); + } + + /** + * Holds an async DynamoDB client and its mock HTTP client so both can be closed. + * User-supplied {@code httpClient()} instances are not closed by the SDK client. + */ + public static final class AsyncMockResources implements SdkAutoCloseable { + private final DynamoDbAsyncClient client; + private final SdkAsyncHttpClient httpClient; + + AsyncMockResources(DynamoDbAsyncClient client, SdkAsyncHttpClient httpClient) { + this.client = client; + this.httpClient = httpClient; + } + + public DynamoDbAsyncClient client() { + return client; + } + + @Override + public void close() { + if (client != null) { + client.close(); + } + if (httpClient != null) { + httpClient.close(); + } + } + } +} diff --git a/test/sdk-benchmarks/src/main/java/software/amazon/awssdk/benchmark/dynamodb/mock/DynamoDbMockResponseFactory.java b/test/sdk-benchmarks/src/main/java/software/amazon/awssdk/benchmark/dynamodb/mock/DynamoDbMockResponseFactory.java new file mode 100644 index 000000000000..b82e114018f8 --- /dev/null +++ b/test/sdk-benchmarks/src/main/java/software/amazon/awssdk/benchmark/dynamodb/mock/DynamoDbMockResponseFactory.java @@ -0,0 +1,107 @@ +/* + * Copyright 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. + */ + +package software.amazon.awssdk.benchmark.dynamodb.mock; + +import java.net.URI; +import java.nio.charset.StandardCharsets; +import java.util.Collections; +import java.util.Map; +import software.amazon.awssdk.core.SdkPojo; +import software.amazon.awssdk.http.SdkHttpFullRequest; +import software.amazon.awssdk.http.SdkHttpMethod; +import software.amazon.awssdk.protocols.core.OperationInfo; +import software.amazon.awssdk.protocols.core.ProtocolMarshaller; +import software.amazon.awssdk.protocols.json.AwsJsonProtocol; +import software.amazon.awssdk.protocols.json.AwsJsonProtocolMetadata; +import software.amazon.awssdk.protocols.json.internal.AwsStructuredPlainJsonFactory; +import software.amazon.awssdk.protocols.json.internal.marshall.JsonProtocolMarshallerBuilder; +import software.amazon.awssdk.services.dynamodb.model.AttributeValue; +import software.amazon.awssdk.services.dynamodb.model.GetItemResponse; +import software.amazon.awssdk.services.dynamodb.model.PutItemResponse; +import software.amazon.awssdk.services.dynamodb.model.QueryResponse; +import software.amazon.awssdk.utils.FunctionalUtils; +import software.amazon.awssdk.utils.IoUtils; + +/** + * Builds deterministic DynamoDB AWS JSON protocol response bodies for mocked Tier C benchmarks. + */ +public final class DynamoDbMockResponseFactory { + + private static final String CONTENT_TYPE = "application/x-amz-json-1.0"; + private static final URI ENDPOINT = URI.create("https://dynamodb.us-east-1.amazonaws.com"); + + private static final OperationInfo OPERATION_INFO = OperationInfo.builder() + .httpMethod(SdkHttpMethod.POST) + .hasImplicitPayloadMembers(true) + .build(); + + private static final AwsJsonProtocolMetadata PROTOCOL_METADATA = + AwsJsonProtocolMetadata.builder() + .protocol(AwsJsonProtocol.AWS_JSON) + .contentType(CONTENT_TYPE) + .build(); + + private DynamoDbMockResponseFactory() { + } + + public static String getItemResponseBody(Map item) { + return marshall(GetItemResponse.builder().item(item).build()); + } + + public static String putItemResponseBody() { + return marshall(PutItemResponse.builder().build()); + } + + /** + * First-page Query response containing a single deterministic item. + */ + public static String queryResponseBody(Map item) { + return marshall(QueryResponse.builder() + .items(Collections.singletonList(item)) + .count(1) + .scannedCount(1) + .build()); + } + + public static byte[] getItemResponseBytes(Map item) { + return getItemResponseBody(item).getBytes(StandardCharsets.UTF_8); + } + + private static String marshall(SdkPojo response) { + try { + ProtocolMarshaller marshaller = + JsonProtocolMarshallerBuilder.create() + .endpoint(ENDPOINT) + .jsonGenerator(AwsStructuredPlainJsonFactory.SDK_JSON_FACTORY + .createWriter(CONTENT_TYPE)) + .contentType(CONTENT_TYPE) + .operationInfo(OPERATION_INFO) + .sendExplicitNullForPayload(false) + .protocolMetadata(PROTOCOL_METADATA) + .build(); + + SdkHttpFullRequest request = marshaller.marshall(response); + return request.contentStreamProvider() + .map(provider -> FunctionalUtils.invokeSafely( + () -> IoUtils.toUtf8String(provider.newStream()))) + .orElse("{}"); + } catch (RuntimeException e) { + throw e; + } catch (Exception e) { + throw new IllegalStateException("Failed to marshall DynamoDB mock response", e); + } + } +} diff --git a/test/sdk-benchmarks/src/main/java/software/amazon/awssdk/benchmark/dynamodb/mock/ReplayingMockAsyncHttpClient.java b/test/sdk-benchmarks/src/main/java/software/amazon/awssdk/benchmark/dynamodb/mock/ReplayingMockAsyncHttpClient.java new file mode 100644 index 000000000000..f488d0ff291f --- /dev/null +++ b/test/sdk-benchmarks/src/main/java/software/amazon/awssdk/benchmark/dynamodb/mock/ReplayingMockAsyncHttpClient.java @@ -0,0 +1,62 @@ +/* + * Copyright 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. + */ + +package software.amazon.awssdk.benchmark.dynamodb.mock; + +import java.io.ByteArrayInputStream; +import java.time.Duration; +import java.util.concurrent.CompletableFuture; +import software.amazon.awssdk.http.AbortableInputStream; +import software.amazon.awssdk.http.HttpExecuteResponse; +import software.amazon.awssdk.http.SdkHttpResponse; +import software.amazon.awssdk.http.async.AsyncExecuteRequest; +import software.amazon.awssdk.http.async.SdkAsyncHttpClient; +import software.amazon.awssdk.testutils.service.http.MockAsyncHttpClient; + +/** + * Adapter over {@link MockAsyncHttpClient} that: + *

    + *
  • stubs each execute with {@link Duration#ZERO} (no 50 ms artificial delay),
  • + *
  • supplies a fresh response body stream every call (the underlying mock drains streams).
  • + *
+ * Completion still runs on {@link MockAsyncHttpClient}'s executor via {@code runAsync}. + */ +final class ReplayingMockAsyncHttpClient implements SdkAsyncHttpClient { + + private final MockAsyncHttpClient delegate = new MockAsyncHttpClient(); + private final byte[] successBody; + + ReplayingMockAsyncHttpClient(byte[] successBody) { + this.successBody = successBody.clone(); + } + + @Override + public CompletableFuture execute(AsyncExecuteRequest request) { + delegate.stubNextResponse(successResponse(), Duration.ZERO); + return delegate.execute(request); + } + + @Override + public void close() { + delegate.close(); + } + + private HttpExecuteResponse successResponse() { + return HttpExecuteResponse.builder() + .response(SdkHttpResponse.builder().statusCode(200).build()) + .responseBody(AbortableInputStream.create(new ByteArrayInputStream(successBody))) + .build(); + } +} diff --git a/test/sdk-benchmarks/src/main/java/software/amazon/awssdk/benchmark/dynamodb/pipeline/DocumentGetItemBenchmark.java b/test/sdk-benchmarks/src/main/java/software/amazon/awssdk/benchmark/dynamodb/pipeline/DocumentGetItemBenchmark.java new file mode 100644 index 000000000000..b943aebbe982 --- /dev/null +++ b/test/sdk-benchmarks/src/main/java/software/amazon/awssdk/benchmark/dynamodb/pipeline/DocumentGetItemBenchmark.java @@ -0,0 +1,101 @@ +/* + * Copyright 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. + */ + +package software.amazon.awssdk.benchmark.dynamodb.pipeline; + +import static software.amazon.awssdk.enhanced.dynamodb.AttributeConverterProvider.defaultProvider; + +import java.util.concurrent.TimeUnit; +import org.openjdk.jmh.annotations.Benchmark; +import org.openjdk.jmh.annotations.BenchmarkMode; +import org.openjdk.jmh.annotations.Fork; +import org.openjdk.jmh.annotations.Level; +import org.openjdk.jmh.annotations.Measurement; +import org.openjdk.jmh.annotations.Mode; +import org.openjdk.jmh.annotations.OutputTimeUnit; +import org.openjdk.jmh.annotations.Scope; +import org.openjdk.jmh.annotations.Setup; +import org.openjdk.jmh.annotations.State; +import org.openjdk.jmh.annotations.TearDown; +import org.openjdk.jmh.annotations.Warmup; +import software.amazon.awssdk.benchmark.dynamodb.DynamoDbBenchmarkConstant; +import software.amazon.awssdk.benchmark.dynamodb.fixture.DynamoDbBenchmarkFixture; +import software.amazon.awssdk.benchmark.dynamodb.mock.DynamoDbMockClientFactory; +import software.amazon.awssdk.enhanced.dynamodb.AttributeValueType; +import software.amazon.awssdk.enhanced.dynamodb.DynamoDbEnhancedClient; +import software.amazon.awssdk.enhanced.dynamodb.DynamoDbTable; +import software.amazon.awssdk.enhanced.dynamodb.Key; +import software.amazon.awssdk.enhanced.dynamodb.TableMetadata; +import software.amazon.awssdk.enhanced.dynamodb.TableSchema; +import software.amazon.awssdk.enhanced.dynamodb.document.EnhancedDocument; +import software.amazon.awssdk.services.dynamodb.DynamoDbClient; + +/** + * Tier C DOCUMENT sync GetItem: Enhanced Document mapping plus full mocked SDK pipeline. + * + *

No low-level {@code GetItemRequest} is pre-built. Response AttributeValues are mapped to + * {@link EnhancedDocument} inside the timed {@link DynamoDbTable#getItem(Key)} call. + */ +@State(Scope.Benchmark) +@BenchmarkMode(Mode.AverageTime) +@OutputTimeUnit(TimeUnit.MICROSECONDS) +@Warmup(iterations = 5) +@Measurement(iterations = 5) +@Fork(2) +public class DocumentGetItemBenchmark { + + private DynamoDbClient lowLevelClient; + private DynamoDbTable table; + private Key key; + + @Setup(Level.Trial) + public void setup() { + DynamoDbBenchmarkFixture fixture = DynamoDbBenchmarkFixture.create(); + lowLevelClient = DynamoDbMockClientFactory.syncGetItemClient(fixture.attributeMap()); + DynamoDbEnhancedClient enhancedClient = DynamoDbEnhancedClient.builder() + .dynamoDbClient(lowLevelClient) + .build(); + TableSchema documentSchema = + TableSchema.documentSchemaBuilder() + .addIndexPartitionKey(TableMetadata.primaryIndexName(), + DynamoDbBenchmarkFixture.PARTITION_KEY_NAME, + AttributeValueType.S) + .attributeConverterProviders(defaultProvider()) + .build(); + table = enhancedClient.table(DynamoDbBenchmarkConstant.TABLE_NAME, documentSchema); + key = Key.builder() + .partitionValue(DynamoDbBenchmarkFixture.PARTITION_KEY_VALUE) + .build(); + + // One-time smoke call: verifies response→EnhancedDocument mapping and warms the SDK + // pipeline before JMH warmup iterations begin. + EnhancedDocument smoke = table.getItem(key); + if (smoke == null || smoke.getString(DynamoDbBenchmarkFixture.PARTITION_KEY_NAME) == null) { + throw new IllegalStateException("Document GetItem smoke call did not map a usable document"); + } + } + + @TearDown(Level.Trial) + public void tearDown() { + if (lowLevelClient != null) { + lowLevelClient.close(); + } + } + + @Benchmark + public EnhancedDocument getItem() { + return table.getItem(key); + } +} diff --git a/test/sdk-benchmarks/src/main/java/software/amazon/awssdk/benchmark/dynamodb/pipeline/DocumentPutItemBenchmark.java b/test/sdk-benchmarks/src/main/java/software/amazon/awssdk/benchmark/dynamodb/pipeline/DocumentPutItemBenchmark.java new file mode 100644 index 000000000000..bbc86f91bdcf --- /dev/null +++ b/test/sdk-benchmarks/src/main/java/software/amazon/awssdk/benchmark/dynamodb/pipeline/DocumentPutItemBenchmark.java @@ -0,0 +1,97 @@ +/* + * Copyright 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. + */ + +package software.amazon.awssdk.benchmark.dynamodb.pipeline; + +import static software.amazon.awssdk.enhanced.dynamodb.AttributeConverterProvider.defaultProvider; + +import java.util.concurrent.TimeUnit; +import org.openjdk.jmh.annotations.Benchmark; +import org.openjdk.jmh.annotations.BenchmarkMode; +import org.openjdk.jmh.annotations.Fork; +import org.openjdk.jmh.annotations.Level; +import org.openjdk.jmh.annotations.Measurement; +import org.openjdk.jmh.annotations.Mode; +import org.openjdk.jmh.annotations.OutputTimeUnit; +import org.openjdk.jmh.annotations.Scope; +import org.openjdk.jmh.annotations.Setup; +import org.openjdk.jmh.annotations.State; +import org.openjdk.jmh.annotations.TearDown; +import org.openjdk.jmh.annotations.Warmup; +import software.amazon.awssdk.benchmark.dynamodb.DynamoDbBenchmarkConstant; +import software.amazon.awssdk.benchmark.dynamodb.fixture.DynamoDbBenchmarkFixture; +import software.amazon.awssdk.benchmark.dynamodb.mock.DynamoDbMockClientFactory; +import software.amazon.awssdk.enhanced.dynamodb.AttributeValueType; +import software.amazon.awssdk.enhanced.dynamodb.DynamoDbEnhancedClient; +import software.amazon.awssdk.enhanced.dynamodb.DynamoDbTable; +import software.amazon.awssdk.enhanced.dynamodb.TableMetadata; +import software.amazon.awssdk.enhanced.dynamodb.TableSchema; +import software.amazon.awssdk.enhanced.dynamodb.document.EnhancedDocument; +import software.amazon.awssdk.services.dynamodb.DynamoDbClient; + +/** + * Tier C DOCUMENT sync PutItem: Enhanced Document mapping plus full mocked SDK pipeline. + * + *

No low-level {@code PutItemRequest} is pre-built. Document→AttributeValue mapping occurs + * inside the timed {@link DynamoDbTable#putItem(Object)} call; setup does not call {@code toMap()}. + */ +@State(Scope.Benchmark) +@BenchmarkMode(Mode.AverageTime) +@OutputTimeUnit(TimeUnit.MICROSECONDS) +@Warmup(iterations = 5) +@Measurement(iterations = 5) +@Fork(2) +public class DocumentPutItemBenchmark { + + private DynamoDbClient lowLevelClient; + private DynamoDbTable table; + private EnhancedDocument document; + + @Setup(Level.Trial) + public void setup() { + DynamoDbBenchmarkFixture fixture = DynamoDbBenchmarkFixture.create(); + lowLevelClient = DynamoDbMockClientFactory.syncPutItemClient(); + DynamoDbEnhancedClient enhancedClient = DynamoDbEnhancedClient.builder() + .dynamoDbClient(lowLevelClient) + .build(); + TableSchema documentSchema = + TableSchema.documentSchemaBuilder() + .addIndexPartitionKey(TableMetadata.primaryIndexName(), + DynamoDbBenchmarkFixture.PARTITION_KEY_NAME, + AttributeValueType.S) + .attributeConverterProviders(defaultProvider()) + .build(); + table = enhancedClient.table(DynamoDbBenchmarkConstant.TABLE_NAME, documentSchema); + // Same logical EnhancedDocument as LOW/TYPED fixture; no toMap() here so document→AV + // conversion remains inside the timed putItem path. + document = fixture.document(); + + // One-time smoke call: verifies the put path succeeds and warms the SDK pipeline before + // JMH warmup iterations begin. + table.putItem(document); + } + + @TearDown(Level.Trial) + public void tearDown() { + if (lowLevelClient != null) { + lowLevelClient.close(); + } + } + + @Benchmark + public void putItem() { + table.putItem(document); + } +} diff --git a/test/sdk-benchmarks/src/main/java/software/amazon/awssdk/benchmark/dynamodb/pipeline/LowLevelAsyncGetItemBenchmark.java b/test/sdk-benchmarks/src/main/java/software/amazon/awssdk/benchmark/dynamodb/pipeline/LowLevelAsyncGetItemBenchmark.java new file mode 100644 index 000000000000..d35fec20dbe4 --- /dev/null +++ b/test/sdk-benchmarks/src/main/java/software/amazon/awssdk/benchmark/dynamodb/pipeline/LowLevelAsyncGetItemBenchmark.java @@ -0,0 +1,89 @@ +/* + * Copyright 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. + */ + +package software.amazon.awssdk.benchmark.dynamodb.pipeline; + +import java.util.Collections; +import java.util.concurrent.TimeUnit; +import org.openjdk.jmh.annotations.Benchmark; +import org.openjdk.jmh.annotations.BenchmarkMode; +import org.openjdk.jmh.annotations.Fork; +import org.openjdk.jmh.annotations.Level; +import org.openjdk.jmh.annotations.Measurement; +import org.openjdk.jmh.annotations.Mode; +import org.openjdk.jmh.annotations.OutputTimeUnit; +import org.openjdk.jmh.annotations.Scope; +import org.openjdk.jmh.annotations.Setup; +import org.openjdk.jmh.annotations.State; +import org.openjdk.jmh.annotations.TearDown; +import org.openjdk.jmh.annotations.Warmup; +import software.amazon.awssdk.benchmark.dynamodb.DynamoDbBenchmarkConstant; +import software.amazon.awssdk.benchmark.dynamodb.fixture.DynamoDbBenchmarkFixture; +import software.amazon.awssdk.benchmark.dynamodb.mock.DynamoDbMockClientFactory; +import software.amazon.awssdk.services.dynamodb.DynamoDbAsyncClient; +import software.amazon.awssdk.services.dynamodb.model.GetItemRequest; +import software.amazon.awssdk.services.dynamodb.model.GetItemResponse; + +/** + * Tier C LOW-level async GetItem: single-operation completion latency via mocked async HTTP. + * + *

Timed path waits on {@code join()} so the measurement includes async scheduling, the SDK + * pipeline, zero-delay mock completion on the mock executor, unmarshalling, and future completion. + */ +@State(Scope.Benchmark) +@BenchmarkMode(Mode.AverageTime) +@OutputTimeUnit(TimeUnit.MICROSECONDS) +@Warmup(iterations = 5) +@Measurement(iterations = 5) +@Fork(2) +public class LowLevelAsyncGetItemBenchmark { + + private DynamoDbMockClientFactory.AsyncMockResources resources; + private DynamoDbAsyncClient client; + private GetItemRequest request; + + @Setup(Level.Trial) + public void setup() { + DynamoDbBenchmarkFixture fixture = DynamoDbBenchmarkFixture.create(); + resources = DynamoDbMockClientFactory.asyncGetItemClient(fixture.attributeMap()); + client = resources.client(); + request = GetItemRequest.builder() + .tableName(DynamoDbBenchmarkConstant.TABLE_NAME) + .key(Collections.singletonMap( + DynamoDbBenchmarkFixture.PARTITION_KEY_NAME, + fixture.partitionKeyAttribute())) + .build(); + + // One-time smoke call: verifies async completion + unmarshalling and warms the pipeline + // before JMH warmup iterations begin. + GetItemResponse smoke = client.getItem(request).join(); + if (smoke.item() == null || smoke.item().isEmpty()) { + throw new IllegalStateException("Async GetItem smoke call did not deserialize a non-empty item"); + } + } + + @TearDown(Level.Trial) + public void tearDown() { + if (resources != null) { + // Closes DynamoDbAsyncClient and MockAsyncHttpClient (executor shutdown). + resources.close(); + } + } + + @Benchmark + public GetItemResponse getItem() { + return client.getItem(request).join(); + } +} diff --git a/test/sdk-benchmarks/src/main/java/software/amazon/awssdk/benchmark/dynamodb/pipeline/LowLevelGetItemBenchmark.java b/test/sdk-benchmarks/src/main/java/software/amazon/awssdk/benchmark/dynamodb/pipeline/LowLevelGetItemBenchmark.java new file mode 100644 index 000000000000..b305350e70da --- /dev/null +++ b/test/sdk-benchmarks/src/main/java/software/amazon/awssdk/benchmark/dynamodb/pipeline/LowLevelGetItemBenchmark.java @@ -0,0 +1,91 @@ +/* + * Copyright 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. + */ + +package software.amazon.awssdk.benchmark.dynamodb.pipeline; + +import java.util.Collections; +import java.util.concurrent.TimeUnit; +import org.openjdk.jmh.annotations.Benchmark; +import org.openjdk.jmh.annotations.BenchmarkMode; +import org.openjdk.jmh.annotations.Fork; +import org.openjdk.jmh.annotations.Level; +import org.openjdk.jmh.annotations.Measurement; +import org.openjdk.jmh.annotations.Mode; +import org.openjdk.jmh.annotations.OutputTimeUnit; +import org.openjdk.jmh.annotations.Scope; +import org.openjdk.jmh.annotations.Setup; +import org.openjdk.jmh.annotations.State; +import org.openjdk.jmh.annotations.TearDown; +import org.openjdk.jmh.annotations.Warmup; +import software.amazon.awssdk.benchmark.dynamodb.DynamoDbBenchmarkConstant; +import software.amazon.awssdk.benchmark.dynamodb.fixture.DynamoDbBenchmarkFixture; +import software.amazon.awssdk.benchmark.dynamodb.mock.DynamoDbMockClientFactory; +import software.amazon.awssdk.services.dynamodb.DynamoDbClient; +import software.amazon.awssdk.services.dynamodb.model.GetItemRequest; +import software.amazon.awssdk.services.dynamodb.model.GetItemResponse; + +/** + * Tier C LOW-level sync GetItem: deterministic full SDK client-side pipeline via MockHttpClient. + * + *

Timed path: endpoint resolution → marshalling → SigV4 → HTTP construction → mock HTTP → + * unmarshalling. Setup (client, fixture, request) is outside the timed method. + */ +@State(Scope.Benchmark) +@BenchmarkMode(Mode.AverageTime) +@OutputTimeUnit(TimeUnit.MICROSECONDS) +@Warmup(iterations = 5) +@Measurement(iterations = 5) +@Fork(2) +public class LowLevelGetItemBenchmark { + + private DynamoDbClient client; + private GetItemRequest request; + + @Setup(Level.Trial) + public void setup() { + DynamoDbBenchmarkFixture fixture = DynamoDbBenchmarkFixture.create(); + client = DynamoDbMockClientFactory.syncGetItemClient(fixture.attributeMap()); + request = GetItemRequest.builder() + .tableName(DynamoDbBenchmarkConstant.TABLE_NAME) + .key(Collections.singletonMap( + DynamoDbBenchmarkFixture.PARTITION_KEY_NAME, + fixture.partitionKeyAttribute())) + .build(); + + // One-time smoke call: verifies the mock GetItem body unmarshals to a non-empty item and + // intentionally warms/initializes the SDK pipeline before JMH warmup iterations begin. + GetItemResponse smoke = client.getItem(request); + if (smoke.item() == null || smoke.item().isEmpty()) { + throw new IllegalStateException("GetItem smoke call did not deserialize a non-empty item"); + } + } + + /** + * Closes the DynamoDbClient. The injected {@code MockHttpClient} is treated as a shared + * instance by the SDK (not closed by the client); {@code MockHttpClient#close()} is a no-op. + * Each JMH fork runs in a separate JVM with its own client, so there is no cross-fork shared state. + */ + @TearDown(Level.Trial) + public void tearDown() { + if (client != null) { + client.close(); + } + } + + @Benchmark + public GetItemResponse getItem() { + return client.getItem(request); + } +} diff --git a/test/sdk-benchmarks/src/main/java/software/amazon/awssdk/benchmark/dynamodb/pipeline/LowLevelPutItemBenchmark.java b/test/sdk-benchmarks/src/main/java/software/amazon/awssdk/benchmark/dynamodb/pipeline/LowLevelPutItemBenchmark.java new file mode 100644 index 000000000000..f36fdc4648b4 --- /dev/null +++ b/test/sdk-benchmarks/src/main/java/software/amazon/awssdk/benchmark/dynamodb/pipeline/LowLevelPutItemBenchmark.java @@ -0,0 +1,88 @@ +/* + * Copyright 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. + */ + +package software.amazon.awssdk.benchmark.dynamodb.pipeline; + +import java.util.concurrent.TimeUnit; +import org.openjdk.jmh.annotations.Benchmark; +import org.openjdk.jmh.annotations.BenchmarkMode; +import org.openjdk.jmh.annotations.Fork; +import org.openjdk.jmh.annotations.Level; +import org.openjdk.jmh.annotations.Measurement; +import org.openjdk.jmh.annotations.Mode; +import org.openjdk.jmh.annotations.OutputTimeUnit; +import org.openjdk.jmh.annotations.Scope; +import org.openjdk.jmh.annotations.Setup; +import org.openjdk.jmh.annotations.State; +import org.openjdk.jmh.annotations.TearDown; +import org.openjdk.jmh.annotations.Warmup; +import software.amazon.awssdk.benchmark.dynamodb.DynamoDbBenchmarkConstant; +import software.amazon.awssdk.benchmark.dynamodb.fixture.DynamoDbBenchmarkFixture; +import software.amazon.awssdk.benchmark.dynamodb.mock.DynamoDbMockClientFactory; +import software.amazon.awssdk.services.dynamodb.DynamoDbClient; +import software.amazon.awssdk.services.dynamodb.model.PutItemRequest; +import software.amazon.awssdk.services.dynamodb.model.PutItemResponse; + +/** + * Tier C LOW-level sync PutItem: deterministic full SDK client-side pipeline via MockHttpClient. + * + *

Timed path: endpoint resolution → marshalling → SigV4 → HTTP construction → mock HTTP → + * unmarshalling. Setup (client, fixture, request) is outside the timed method. + */ +@State(Scope.Benchmark) +@BenchmarkMode(Mode.AverageTime) +@OutputTimeUnit(TimeUnit.MICROSECONDS) +@Warmup(iterations = 5) +@Measurement(iterations = 5) +@Fork(2) +public class LowLevelPutItemBenchmark { + + private DynamoDbClient client; + private PutItemRequest request; + + @Setup(Level.Trial) + public void setup() { + DynamoDbBenchmarkFixture fixture = DynamoDbBenchmarkFixture.create(); + client = DynamoDbMockClientFactory.syncPutItemClient(); + request = PutItemRequest.builder() + .tableName(DynamoDbBenchmarkConstant.TABLE_NAME) + .item(fixture.attributeMap()) + .build(); + + // One-time smoke call: verifies the mock PutItem response unmarshals successfully and + // intentionally warms/initializes the SDK pipeline before JMH warmup iterations begin. + PutItemResponse smoke = client.putItem(request); + if (smoke == null) { + throw new IllegalStateException("PutItem smoke call returned null"); + } + } + + /** + * Closes the DynamoDbClient. The injected {@code MockHttpClient} is treated as a shared + * instance by the SDK (not closed by the client); {@code MockHttpClient#close()} is a no-op. + * Each JMH fork runs in a separate JVM with its own client, so there is no cross-fork shared state. + */ + @TearDown(Level.Trial) + public void tearDown() { + if (client != null) { + client.close(); + } + } + + @Benchmark + public PutItemResponse putItem() { + return client.putItem(request); + } +} diff --git a/test/sdk-benchmarks/src/main/java/software/amazon/awssdk/benchmark/dynamodb/pipeline/TypedAsyncGetItemBenchmark.java b/test/sdk-benchmarks/src/main/java/software/amazon/awssdk/benchmark/dynamodb/pipeline/TypedAsyncGetItemBenchmark.java new file mode 100644 index 000000000000..9dbc61257745 --- /dev/null +++ b/test/sdk-benchmarks/src/main/java/software/amazon/awssdk/benchmark/dynamodb/pipeline/TypedAsyncGetItemBenchmark.java @@ -0,0 +1,88 @@ +/* + * Copyright 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. + */ + +package software.amazon.awssdk.benchmark.dynamodb.pipeline; + +import java.util.concurrent.TimeUnit; +import org.openjdk.jmh.annotations.Benchmark; +import org.openjdk.jmh.annotations.BenchmarkMode; +import org.openjdk.jmh.annotations.Fork; +import org.openjdk.jmh.annotations.Level; +import org.openjdk.jmh.annotations.Measurement; +import org.openjdk.jmh.annotations.Mode; +import org.openjdk.jmh.annotations.OutputTimeUnit; +import org.openjdk.jmh.annotations.Scope; +import org.openjdk.jmh.annotations.Setup; +import org.openjdk.jmh.annotations.State; +import org.openjdk.jmh.annotations.TearDown; +import org.openjdk.jmh.annotations.Warmup; +import software.amazon.awssdk.benchmark.dynamodb.DynamoDbBenchmarkConstant; +import software.amazon.awssdk.benchmark.dynamodb.fixture.BenchmarkItem; +import software.amazon.awssdk.benchmark.dynamodb.fixture.DynamoDbBenchmarkFixture; +import software.amazon.awssdk.benchmark.dynamodb.mock.DynamoDbMockClientFactory; +import software.amazon.awssdk.enhanced.dynamodb.DynamoDbAsyncTable; +import software.amazon.awssdk.enhanced.dynamodb.DynamoDbEnhancedAsyncClient; +import software.amazon.awssdk.enhanced.dynamodb.Key; + +/** + * Tier C TYPED async GetItem: Enhanced mapping plus single-operation async completion latency. + * + *

No low-level request is pre-built. {@code join()} keeps Enhanced response→bean mapping inside + * the measured path. + */ +@State(Scope.Benchmark) +@BenchmarkMode(Mode.AverageTime) +@OutputTimeUnit(TimeUnit.MICROSECONDS) +@Warmup(iterations = 5) +@Measurement(iterations = 5) +@Fork(2) +public class TypedAsyncGetItemBenchmark { + + private DynamoDbMockClientFactory.AsyncMockResources resources; + private DynamoDbAsyncTable table; + private Key key; + + @Setup(Level.Trial) + public void setup() { + DynamoDbBenchmarkFixture fixture = DynamoDbBenchmarkFixture.create(); + resources = DynamoDbMockClientFactory.asyncGetItemClient(fixture.attributeMap()); + DynamoDbEnhancedAsyncClient enhancedClient = + DynamoDbEnhancedAsyncClient.builder() + .dynamoDbClient(resources.client()) + .build(); + table = enhancedClient.table(DynamoDbBenchmarkConstant.TABLE_NAME, fixture.tableSchema()); + key = Key.builder() + .partitionValue(DynamoDbBenchmarkFixture.PARTITION_KEY_VALUE) + .build(); + + // One-time smoke call: verifies async Enhanced mapping completes before JMH warmup. + BenchmarkItem smoke = table.getItem(key).join(); + if (smoke == null || smoke.getPk() == null) { + throw new IllegalStateException("Typed async GetItem smoke call did not map a non-null item"); + } + } + + @TearDown(Level.Trial) + public void tearDown() { + if (resources != null) { + resources.close(); + } + } + + @Benchmark + public BenchmarkItem getItem() { + return table.getItem(key).join(); + } +} diff --git a/test/sdk-benchmarks/src/main/java/software/amazon/awssdk/benchmark/dynamodb/pipeline/TypedGetItemBenchmark.java b/test/sdk-benchmarks/src/main/java/software/amazon/awssdk/benchmark/dynamodb/pipeline/TypedGetItemBenchmark.java new file mode 100644 index 000000000000..305dd065e194 --- /dev/null +++ b/test/sdk-benchmarks/src/main/java/software/amazon/awssdk/benchmark/dynamodb/pipeline/TypedGetItemBenchmark.java @@ -0,0 +1,89 @@ +/* + * Copyright 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. + */ + +package software.amazon.awssdk.benchmark.dynamodb.pipeline; + +import java.util.concurrent.TimeUnit; +import org.openjdk.jmh.annotations.Benchmark; +import org.openjdk.jmh.annotations.BenchmarkMode; +import org.openjdk.jmh.annotations.Fork; +import org.openjdk.jmh.annotations.Level; +import org.openjdk.jmh.annotations.Measurement; +import org.openjdk.jmh.annotations.Mode; +import org.openjdk.jmh.annotations.OutputTimeUnit; +import org.openjdk.jmh.annotations.Scope; +import org.openjdk.jmh.annotations.Setup; +import org.openjdk.jmh.annotations.State; +import org.openjdk.jmh.annotations.TearDown; +import org.openjdk.jmh.annotations.Warmup; +import software.amazon.awssdk.benchmark.dynamodb.DynamoDbBenchmarkConstant; +import software.amazon.awssdk.benchmark.dynamodb.fixture.BenchmarkItem; +import software.amazon.awssdk.benchmark.dynamodb.fixture.DynamoDbBenchmarkFixture; +import software.amazon.awssdk.benchmark.dynamodb.mock.DynamoDbMockClientFactory; +import software.amazon.awssdk.enhanced.dynamodb.DynamoDbEnhancedClient; +import software.amazon.awssdk.enhanced.dynamodb.DynamoDbTable; +import software.amazon.awssdk.enhanced.dynamodb.Key; +import software.amazon.awssdk.services.dynamodb.DynamoDbClient; + +/** + * Tier C TYPED sync GetItem: Enhanced Client mapping plus full mocked SDK pipeline. + * + *

No low-level {@code GetItemRequest} is pre-built; the timed path includes Enhanced mapping + * into the low-level request and mapping of the response back to {@link BenchmarkItem}. + */ +@State(Scope.Benchmark) +@BenchmarkMode(Mode.AverageTime) +@OutputTimeUnit(TimeUnit.MICROSECONDS) +@Warmup(iterations = 5) +@Measurement(iterations = 5) +@Fork(2) +public class TypedGetItemBenchmark { + + private DynamoDbClient lowLevelClient; + private DynamoDbTable table; + private Key key; + + @Setup(Level.Trial) + public void setup() { + DynamoDbBenchmarkFixture fixture = DynamoDbBenchmarkFixture.create(); + lowLevelClient = DynamoDbMockClientFactory.syncGetItemClient(fixture.attributeMap()); + DynamoDbEnhancedClient enhancedClient = DynamoDbEnhancedClient.builder() + .dynamoDbClient(lowLevelClient) + .build(); + table = enhancedClient.table(DynamoDbBenchmarkConstant.TABLE_NAME, fixture.tableSchema()); + key = Key.builder() + .partitionValue(DynamoDbBenchmarkFixture.PARTITION_KEY_VALUE) + .build(); + + // One-time smoke call: verifies response→bean mapping and warms the SDK pipeline before + // JMH warmup iterations begin. + BenchmarkItem smoke = table.getItem(key); + if (smoke == null || smoke.getPk() == null) { + throw new IllegalStateException("Typed GetItem smoke call did not map a non-null item"); + } + } + + @TearDown(Level.Trial) + public void tearDown() { + if (lowLevelClient != null) { + lowLevelClient.close(); + } + } + + @Benchmark + public BenchmarkItem getItem() { + return table.getItem(key); + } +} diff --git a/test/sdk-benchmarks/src/main/java/software/amazon/awssdk/benchmark/dynamodb/pipeline/TypedPutItemBenchmark.java b/test/sdk-benchmarks/src/main/java/software/amazon/awssdk/benchmark/dynamodb/pipeline/TypedPutItemBenchmark.java new file mode 100644 index 000000000000..0ca7bd7fb9c8 --- /dev/null +++ b/test/sdk-benchmarks/src/main/java/software/amazon/awssdk/benchmark/dynamodb/pipeline/TypedPutItemBenchmark.java @@ -0,0 +1,83 @@ +/* + * Copyright 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. + */ + +package software.amazon.awssdk.benchmark.dynamodb.pipeline; + +import java.util.concurrent.TimeUnit; +import org.openjdk.jmh.annotations.Benchmark; +import org.openjdk.jmh.annotations.BenchmarkMode; +import org.openjdk.jmh.annotations.Fork; +import org.openjdk.jmh.annotations.Level; +import org.openjdk.jmh.annotations.Measurement; +import org.openjdk.jmh.annotations.Mode; +import org.openjdk.jmh.annotations.OutputTimeUnit; +import org.openjdk.jmh.annotations.Scope; +import org.openjdk.jmh.annotations.Setup; +import org.openjdk.jmh.annotations.State; +import org.openjdk.jmh.annotations.TearDown; +import org.openjdk.jmh.annotations.Warmup; +import software.amazon.awssdk.benchmark.dynamodb.DynamoDbBenchmarkConstant; +import software.amazon.awssdk.benchmark.dynamodb.fixture.BenchmarkItem; +import software.amazon.awssdk.benchmark.dynamodb.fixture.DynamoDbBenchmarkFixture; +import software.amazon.awssdk.benchmark.dynamodb.mock.DynamoDbMockClientFactory; +import software.amazon.awssdk.enhanced.dynamodb.DynamoDbEnhancedClient; +import software.amazon.awssdk.enhanced.dynamodb.DynamoDbTable; +import software.amazon.awssdk.services.dynamodb.DynamoDbClient; + +/** + * Tier C TYPED sync PutItem: Enhanced Client mapping plus full mocked SDK pipeline. + * + *

No low-level {@code PutItemRequest} is pre-built; bean→AttributeValue mapping occurs inside + * the timed {@link DynamoDbTable#putItem(Object)} call. + */ +@State(Scope.Benchmark) +@BenchmarkMode(Mode.AverageTime) +@OutputTimeUnit(TimeUnit.MICROSECONDS) +@Warmup(iterations = 5) +@Measurement(iterations = 5) +@Fork(2) +public class TypedPutItemBenchmark { + + private DynamoDbClient lowLevelClient; + private DynamoDbTable table; + private BenchmarkItem item; + + @Setup(Level.Trial) + public void setup() { + DynamoDbBenchmarkFixture fixture = DynamoDbBenchmarkFixture.create(); + lowLevelClient = DynamoDbMockClientFactory.syncPutItemClient(); + DynamoDbEnhancedClient enhancedClient = DynamoDbEnhancedClient.builder() + .dynamoDbClient(lowLevelClient) + .build(); + table = enhancedClient.table(DynamoDbBenchmarkConstant.TABLE_NAME, fixture.tableSchema()); + item = fixture.item(); + + // One-time smoke call: verifies the put path succeeds and warms the SDK pipeline before + // JMH warmup iterations begin. + table.putItem(item); + } + + @TearDown(Level.Trial) + public void tearDown() { + if (lowLevelClient != null) { + lowLevelClient.close(); + } + } + + @Benchmark + public void putItem() { + table.putItem(item); + } +} diff --git a/test/sdk-benchmarks/src/main/java/software/amazon/awssdk/benchmark/dynamodb/pipeline/TypedQueryBenchmark.java b/test/sdk-benchmarks/src/main/java/software/amazon/awssdk/benchmark/dynamodb/pipeline/TypedQueryBenchmark.java new file mode 100644 index 000000000000..ba7116ec9249 --- /dev/null +++ b/test/sdk-benchmarks/src/main/java/software/amazon/awssdk/benchmark/dynamodb/pipeline/TypedQueryBenchmark.java @@ -0,0 +1,97 @@ +/* + * Copyright 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. + */ + +package software.amazon.awssdk.benchmark.dynamodb.pipeline; + +import java.util.concurrent.TimeUnit; +import org.openjdk.jmh.annotations.Benchmark; +import org.openjdk.jmh.annotations.BenchmarkMode; +import org.openjdk.jmh.annotations.Fork; +import org.openjdk.jmh.annotations.Level; +import org.openjdk.jmh.annotations.Measurement; +import org.openjdk.jmh.annotations.Mode; +import org.openjdk.jmh.annotations.OutputTimeUnit; +import org.openjdk.jmh.annotations.Scope; +import org.openjdk.jmh.annotations.Setup; +import org.openjdk.jmh.annotations.State; +import org.openjdk.jmh.annotations.TearDown; +import org.openjdk.jmh.annotations.Warmup; +import software.amazon.awssdk.benchmark.dynamodb.DynamoDbBenchmarkConstant; +import software.amazon.awssdk.benchmark.dynamodb.fixture.BenchmarkItem; +import software.amazon.awssdk.benchmark.dynamodb.fixture.DynamoDbBenchmarkFixture; +import software.amazon.awssdk.benchmark.dynamodb.mock.DynamoDbMockClientFactory; +import software.amazon.awssdk.enhanced.dynamodb.DynamoDbEnhancedClient; +import software.amazon.awssdk.enhanced.dynamodb.DynamoDbTable; +import software.amazon.awssdk.enhanced.dynamodb.Key; +import software.amazon.awssdk.enhanced.dynamodb.model.Page; +import software.amazon.awssdk.enhanced.dynamodb.model.QueryConditional; +import software.amazon.awssdk.services.dynamodb.DynamoDbClient; + +/** + * Tier C TYPED sync Query (first page): Enhanced Client mapping plus full mocked SDK pipeline. + * + *

No low-level {@code QueryRequest} is pre-built. {@link QueryConditional} is prepared in setup; + * the timed path builds the low-level query, executes the mock round-trip, and maps the first page + * of items to {@link BenchmarkItem}. + */ +@State(Scope.Benchmark) +@BenchmarkMode(Mode.AverageTime) +@OutputTimeUnit(TimeUnit.MICROSECONDS) +@Warmup(iterations = 5) +@Measurement(iterations = 5) +@Fork(2) +public class TypedQueryBenchmark { + + private DynamoDbClient lowLevelClient; + private DynamoDbTable table; + private QueryConditional queryConditional; + + @Setup(Level.Trial) + public void setup() { + DynamoDbBenchmarkFixture fixture = DynamoDbBenchmarkFixture.create(); + lowLevelClient = DynamoDbMockClientFactory.syncQueryClient(fixture.attributeMap()); + DynamoDbEnhancedClient enhancedClient = DynamoDbEnhancedClient.builder() + .dynamoDbClient(lowLevelClient) + .build(); + table = enhancedClient.table(DynamoDbBenchmarkConstant.TABLE_NAME, fixture.tableSchema()); + Key key = Key.builder() + .partitionValue(DynamoDbBenchmarkFixture.PARTITION_KEY_VALUE) + .build(); + queryConditional = QueryConditional.keyEqualTo(key); + + // One-time smoke call: materializes the first page and verifies at least one mapped item, + // warming the SDK pipeline before JMH warmup iterations begin. + Page smoke = table.query(queryConditional).iterator().next(); + if (smoke == null || smoke.items() == null || smoke.items().isEmpty()) { + throw new IllegalStateException("Typed Query smoke call did not map at least one item"); + } + } + + @TearDown(Level.Trial) + public void tearDown() { + if (lowLevelClient != null) { + lowLevelClient.close(); + } + } + + /** + * Returns the first {@link Page}, forcing first-page retrieval and AttributeValue→bean mapping + * inside the measured invocation. + */ + @Benchmark + public Page query() { + return table.query(queryConditional).iterator().next(); + } +} diff --git a/test/sdk-benchmarks/src/test/java/software/amazon/awssdk/benchmark/dynamodb/DynamoDbBenchmarkInfrastructureTest.java b/test/sdk-benchmarks/src/test/java/software/amazon/awssdk/benchmark/dynamodb/DynamoDbBenchmarkInfrastructureTest.java new file mode 100644 index 000000000000..891c09381966 --- /dev/null +++ b/test/sdk-benchmarks/src/test/java/software/amazon/awssdk/benchmark/dynamodb/DynamoDbBenchmarkInfrastructureTest.java @@ -0,0 +1,109 @@ +/* + * Copyright 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. + */ + +package software.amazon.awssdk.benchmark.dynamodb; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertTrue; + +import java.util.Collections; +import java.util.Map; +import org.junit.Test; +import software.amazon.awssdk.benchmark.dynamodb.fixture.BenchmarkItem; +import software.amazon.awssdk.benchmark.dynamodb.fixture.DynamoDbBenchmarkFixture; +import software.amazon.awssdk.benchmark.dynamodb.mock.DynamoDbMockClientFactory; +import software.amazon.awssdk.benchmark.dynamodb.mock.DynamoDbMockResponseFactory; +import software.amazon.awssdk.enhanced.dynamodb.document.EnhancedDocument; +import software.amazon.awssdk.services.dynamodb.DynamoDbClient; +import software.amazon.awssdk.services.dynamodb.model.AttributeValue; +import software.amazon.awssdk.services.dynamodb.model.GetItemResponse; +import software.amazon.awssdk.services.dynamodb.model.PutItemResponse; +import software.amazon.awssdk.services.dynamodb.model.QueryResponse; + +/** + * Verifies Phase 1 shared fixture and sync mock infrastructure. + */ +public class DynamoDbBenchmarkInfrastructureTest { + + @Test + public void fixtureRepresentationsAreEquivalent() { + DynamoDbBenchmarkFixture fixture = DynamoDbBenchmarkFixture.create(); + + Map low = fixture.attributeMap(); + EnhancedDocument document = fixture.document(); + BenchmarkItem typed = fixture.item(); + + assertEquals(low, document.toMap()); + assertEquals(typed, fixture.tableSchema().mapToItem(low)); + assertEquals(low, fixture.tableSchema().itemToMap(typed, true)); + assertEquals(DynamoDbBenchmarkFixture.PARTITION_KEY_VALUE, + low.get(DynamoDbBenchmarkFixture.PARTITION_KEY_NAME).s()); + assertTrue(low.containsKey("stringAttr")); + assertTrue(low.containsKey("stringList")); + assertTrue(low.containsKey("stringMap")); + assertTrue(low.containsKey("nested")); + } + + @Test + public void mockResponseBodiesAreNonEmptyProtocolJson() { + DynamoDbBenchmarkFixture fixture = DynamoDbBenchmarkFixture.create(); + Map item = fixture.attributeMap(); + + String getBody = DynamoDbMockResponseFactory.getItemResponseBody(item); + String putBody = DynamoDbMockResponseFactory.putItemResponseBody(); + String queryBody = DynamoDbMockResponseFactory.queryResponseBody(item); + + assertTrue(getBody.contains("\"Item\"")); + assertTrue(getBody.contains(DynamoDbBenchmarkFixture.PARTITION_KEY_VALUE)); + assertNotNull(putBody); + assertTrue(queryBody.contains("\"Items\"")); + assertTrue(queryBody.contains("\"Count\"")); + } + + @Test + public void syncMockGetPutQuerySmokeTest() { + DynamoDbBenchmarkFixture fixture = DynamoDbBenchmarkFixture.create(); + Map item = fixture.attributeMap(); + + try (DynamoDbClient getClient = DynamoDbMockClientFactory.syncGetItemClient(item)) { + GetItemResponse getResponse = getClient.getItem(r -> r.tableName(DynamoDbBenchmarkConstant.TABLE_NAME) + .key(Collections.singletonMap( + DynamoDbBenchmarkFixture.PARTITION_KEY_NAME, + fixture.partitionKeyAttribute()))); + assertEquals(item, getResponse.item()); + assertFalse(getResponse.item().isEmpty()); + } + + try (DynamoDbClient putClient = DynamoDbMockClientFactory.syncPutItemClient()) { + PutItemResponse putResponse = putClient.putItem(r -> r.tableName(DynamoDbBenchmarkConstant.TABLE_NAME) + .item(item)); + assertNotNull(putResponse); + } + + try (DynamoDbClient queryClient = DynamoDbMockClientFactory.syncQueryClient(item)) { + QueryResponse queryResponse = queryClient.query(r -> r.tableName(DynamoDbBenchmarkConstant.TABLE_NAME) + .keyConditionExpression("pk = :pk") + .expressionAttributeValues( + Collections.singletonMap( + ":pk", + fixture.partitionKeyAttribute()))); + assertEquals(1, queryResponse.count().intValue()); + assertEquals(1, queryResponse.items().size()); + assertEquals(item, queryResponse.items().get(0)); + } + } +} diff --git a/test/sdk-benchmarks/src/test/java/software/amazon/awssdk/benchmark/dynamodb/LiveBenchmarkGuardTest.java b/test/sdk-benchmarks/src/test/java/software/amazon/awssdk/benchmark/dynamodb/LiveBenchmarkGuardTest.java new file mode 100644 index 000000000000..d337f8c5d632 --- /dev/null +++ b/test/sdk-benchmarks/src/test/java/software/amazon/awssdk/benchmark/dynamodb/LiveBenchmarkGuardTest.java @@ -0,0 +1,69 @@ +/* + * Copyright 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. + */ + +package software.amazon.awssdk.benchmark.dynamodb; + +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertTrue; +import static org.junit.Assert.fail; + +import org.junit.After; +import org.junit.Test; +import software.amazon.awssdk.benchmark.dynamodb.live.LiveBenchmarkGuard; + +/** + * Guard-only tests (no AWS). Ensures live opt-in fails closed before any resource work. + */ +public class LiveBenchmarkGuardTest { + + private String previousProp; + + @After + public void restore() { + if (previousProp == null) { + System.clearProperty(DynamoDbBenchmarkSystemSetting.LIVE_OPT_IN.property()); + } else { + System.setProperty(DynamoDbBenchmarkSystemSetting.LIVE_OPT_IN.property(), previousProp); + } + } + + @Test + public void requireLiveOptIn_withoutProperty_throwsBeforeAwsWork() { + previousProp = System.getProperty(DynamoDbBenchmarkSystemSetting.LIVE_OPT_IN.property()); + System.clearProperty(DynamoDbBenchmarkSystemSetting.LIVE_OPT_IN.property()); + // If the process environment already opts in, skip this assertion. + if (DynamoDbBenchmarkSystemSetting.LIVE_OPT_IN.getBooleanValue().orElse(false)) { + return; + } + assertFalse(LiveBenchmarkGuard.isLiveOptInEnabled()); + try { + LiveBenchmarkGuard.requireLiveOptIn(); + fail("expected IllegalStateException"); + } catch (IllegalStateException expected) { + assertTrue(expected.getMessage().contains( + DynamoDbBenchmarkSystemSetting.LIVE_OPT_IN.environmentVariable())); + assertTrue(expected.getMessage().contains( + DynamoDbBenchmarkSystemSetting.LIVE_OPT_IN.property())); + } + } + + @Test + public void requireLiveOptIn_withSystemProperty_passes() { + previousProp = System.getProperty(DynamoDbBenchmarkSystemSetting.LIVE_OPT_IN.property()); + System.setProperty(DynamoDbBenchmarkSystemSetting.LIVE_OPT_IN.property(), "true"); + assertTrue(LiveBenchmarkGuard.isLiveOptInEnabled()); + LiveBenchmarkGuard.requireLiveOptIn(); + } +}