Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

test: add concurrency testing for storage operations #1132

Open
wants to merge 6 commits into
base: mutations/mutations
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
123 changes: 123 additions & 0 deletions .github/workflows/library_concurrency_tests.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,123 @@
# This workflow performs interoperability tests across the supported runtimes of the MPL.
name: Library Interoperability Tests

on:
workflow_call:
inputs:
dafny:
description: "The Dafny version to run"
required: true
type: string
regenerate-code:
description: "Regenerate code using smithy-dafny"
required: false
default: false
type: boolean

jobs:
generateEncryptVectors:
strategy:
matrix:
library: [AwsCryptographicMaterialProviders]
os: [
# https://taskei.amazon.dev/tasks/CrypTool-5283
# windows-latest,
ubuntu-latest,
macos-13,
]
language: [
java,
# net,
# python,
# rust
]
# https://taskei.amazon.dev/tasks/CrypTool-5284
dotnet-version: ["6.0.x"]
java-versions: [8, 11, 16, 17]
runs-on: ${{ matrix.os }}
permissions:
id-token: write
contents: read
steps:
- name: Support longpaths on Git checkout
run: |
git config --global core.longpaths true

# Test Vectors need to call KMS
- name: Configure AWS Credentials for Tests
uses: aws-actions/configure-aws-credentials@v2
with:
aws-region: us-west-2
role-to-assume: arn:aws:iam::370957321024:role/GitHub-CI-MPL-Dafny-Role-us-west-2
role-session-name: InterOpTests

- uses: actions/checkout@v3
# Not all submodules are needed.
# We manually pull the submodule we DO need.
- run: git submodule update --init libraries
- run: git submodule update --init --recursive smithy-dafny

# Setup Java in Rust is needed for running polymorph
- name: Setup Java 17
if: matrix.language == 'java' || matrix.language == 'rust'
uses: actions/setup-java@v3
with:
distribution: "corretto"
java-version: 17

- name: Setup .NET Core SDK '6.0.x'
uses: actions/setup-dotnet@v3
with:
dotnet-version: "6.0.x"

- name: Setup Dafny
uses: dafny-lang/[email protected]
with:
dafny-version: ${{ inputs.dafny }}

- name: Regenerate code using smithy-dafny if necessary
if: ${{ inputs.regenerate-code }}
uses: ./.github/actions/polymorph_codegen
with:
dafny: ${{ inputs.dafny }}
library: ${{ matrix.library }}
diff-generated-code: false

# Build implementation for each runtime
- name: Build ${{ matrix.library }} implementation in Java
shell: bash
working-directory: ./${{ matrix.library }}
run: |
# This works because `node` is installed by default on GHA runners
CORES=$(node -e 'console.log(os.cpus().length)')
make build_java CORES=$CORES

- name: Setup gradle
if: matrix.language == 'java'
uses: gradle/gradle-build-action@v2
with:
gradle-version: 7.2

- name: Setup Java ${{matrix.java-versions}}
uses: actions/setup-java@v3
with:
distribution: "corretto"
java-version: ${{matrix.java-versions}}

- name: Clean for next Java
uses: gradle/gradle-build-action@v3
with:
arguments: clean
build-root-directory: ./${{ matrix.library }}/runtimes/java

- name: Compile Java 8
uses: gradle/gradle-build-action@v3
with:
arguments: build
build-root-directory: ./${{ matrix.library }}/runtimes/java

- name: Test Java 8
uses: gradle/gradle-build-action@v3
with:
arguments: testConcurrentExamples
build-root-directory: ./${{ matrix.library }}/runtimes/java
Original file line number Diff line number Diff line change
Expand Up @@ -315,6 +315,30 @@ val testExamples = task<Test>("testExamples") {
testLogging {
events("passed")
}
filter {
excludeTestsMatching("software.amazon.cryptography.example.hierarchy.concurrent.*")
}
}

val testConcurrentExamples = task<Test>("testConcurrentExamples") {
description = "Runs examples tests."
group = "verification"

testClassesDirs = sourceSets["testExamples"].output.classesDirs
classpath = sourceSets["testExamples"].runtimeClasspath + sourceSets["examples"].output + sourceSets.main.get().output
// This will show System.out.println statements
testLogging.showStandardStreams = true
useTestNG() {
suites("src/testExamples/java/software/amazon/cryptography/example/hierarchy/concurrent/testng-parallel.xml")
maxParallelForks = 2
}

testLogging {
events("passed")
}
filter {
includeTestsMatching("software.amazon.cryptography.example.hierarchy.concurrent.*")
}
}

fun buildPom(mavenPublication: MavenPublication) {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,156 @@
package software.amazon.cryptography.example.hierarchy.concurrent;

import java.text.DateFormat;
import java.text.SimpleDateFormat;
import java.util.Arrays;
import java.util.Collections;
import java.util.Date;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.TimeZone;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentLinkedDeque;
import org.testng.Assert;
import org.testng.annotations.AfterClass;
import org.testng.annotations.BeforeClass;
import org.testng.annotations.Test;
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.TransactWriteItem;
import software.amazon.awssdk.services.dynamodb.model.TransactWriteItemsRequest;
import software.amazon.awssdk.services.dynamodb.model.TransactWriteItemsResponse;
import software.amazon.awssdk.services.dynamodb.model.TransactionCanceledException;
import software.amazon.awssdk.utils.ImmutableMap;
import software.amazon.cryptography.example.Constants;
import software.amazon.cryptography.example.DdbHelper;
import software.amazon.cryptography.example.Fixtures;

// These concurrent tests check that the
public class ConcurrentConditionCheckWriteTest {

private static final Integer threadCount = 5;
private static final String mLockedId = "concurrency-test-write-key";
private static final Map<String, String> INDEX_EXPR_ATT_NAMES =
ImmutableMap.of("#pk", "branch-key-id");

private static final List<String> identifiers = Collections.unmodifiableList(
Arrays.asList("1", "2", "3", "4", "5")
);
private Map<String, DynamoDbClient> threadIdToDdbClient;
private static Map<String, String> indexToThreadId;
private ConcurrentLinkedDeque<String> unpickedIndices;

@BeforeClass
public void setup() {
threadIdToDdbClient = new ConcurrentHashMap<>(6, 1, threadCount);
identifiers.forEach(id ->
threadIdToDdbClient.put(id, DynamoDbClient.create())
);
indexToThreadId = new ConcurrentHashMap<>(6, 1, threadCount);
unpickedIndices = new ConcurrentLinkedDeque<>(identifiers);
}

@AfterClass
public void teardown() {
DynamoDbClient _ddbClient = DynamoDbClient.create();
identifiers.forEach(id ->
DdbHelper.deleteKeyStoreDdbItem(
mLockedId,
"branch:ACTIVE",
Fixtures.TEST_KEYSTORE_NAME,
_ddbClient,
true
)
);
}

public static Map<String, AttributeValue> indexItem(
final AttributeValue value,
final String timestamp
) {
Map<String, AttributeValue> item = new HashMap<>();

item.put("branch-key-id", AttributeValue.builder().s(mLockedId).build());
item.put("type", AttributeValue.builder().s(indexType()).build());
item.put("value", value);
item.put("timestamp", AttributeValue.builder().s(timestamp).build());
return item;
}

private static String indexType() {
return "branch:ACTIVE";
}

public static TransactWriteItem conditionalWrite(
final AttributeValue value,
final String timestamp
) {
return TransactWriteItem
.builder()
.put(putBuilder ->
putBuilder
.tableName(Fixtures.TEST_KEYSTORE_NAME)
.item(indexItem(value, timestamp))
.conditionExpression("attribute_not_exists(#pk)")
.expressionAttributeNames(INDEX_EXPR_ATT_NAMES)
)
.build();
}

private DynamoDbClient clientForThread(final String threadIdToIndex) {
return threadIdToDdbClient.computeIfAbsent(
threadIdToIndex,
ddbClient -> DynamoDbClient.create()
);
}

@Test(threadPoolSize = 5, invocationCount = 30, timeOut = 1000)
public void TestConcurrentWriteCheck() {
String threadId = String.valueOf(Thread.currentThread().getId());
String threadIdToIndex = indexToThreadId.computeIfAbsent(
threadId,
str -> unpickedIndices.pop()
);
AttributeValue value = AttributeValue.builder().s(threadIdToIndex).build();
TimeZone tz = TimeZone.getTimeZone("UTC");
DateFormat df = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss.SSSSS'Z'"); // Quoted "Z" to indicate UTC, no timezone offset
df.setTimeZone(tz);
String timestamp = df.format(new Date());

System.out.println(
"Thread ID: " +
Thread.currentThread().getId() +
" ThreadIndex: " +
threadIdToIndex +
" Timestamp: " +
timestamp
);

try {
DynamoDbClient client = clientForThread(threadIdToIndex);
TransactWriteItemsResponse transactWriteItemsResponse =
client.transactWriteItems(
TransactWriteItemsRequest
.builder()
.transactItems(conditionalWrite(value, timestamp))
.build()
);
Assert.assertTrue(
transactWriteItemsResponse.sdkHttpResponse().isSuccessful()
);
} catch (TransactionCanceledException exception) {
// We can fail for two reasons, either there's already a transact write in flight
// 0r we have failed the condition check.
exception
.cancellationReasons()
.forEach(cancellationReason -> {
Assert.assertTrue(
(cancellationReason.code().equals("TransactionConflict") ||
cancellationReason.code().equals("ConditionalCheckFailed"))
);
});
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
[//]: # "Copyright Amazon.com Inc. or its affiliates. All Rights Reserved."
[//]: # "SPDX-License-Identifier: CC-BY-SA-4.0"

# AWS Cryptographic Material Providers Library Concurrency Testing Framework

Welcome to the AWS Cryptographic Material Providers Library Concurrency and Parallelization
Testing Framework 🎉!

This framework helps set up scenarios that we would like to run in a parallel or multithreaded environment.

Some things to keep in mind when you add tests. Think about how you will be creating resources per
thread and what kind of state you need to keep between tests.

Examples:

- [Test regular DynamoDB Client TransactWrites](./ConcurrentConditionCheckWriteTest.java)
- [Test ACTIVE branch key reads while branch key creation is inflight](./StorageWriteReadConcurrencyTests.java)
- [Test branch key reads while branch key versioning is inflight](./StorageVersionReadConcurrencyTests.java)

[Security issue notifications](./CONTRIBUTING.md#security-issue-notifications)

## Security

If you discover a potential security issue in this project
we ask that you notify AWS/Amazon Security via our
[vulnerability reporting page](http://aws.amazon.com/security/vulnerability-reporting/).
Please **do not** create a public GitHub issue.
Loading
Loading