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

DynamoDB plugin #76

Merged
merged 3 commits into from Feb 13, 2018
Merged
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
7 changes: 4 additions & 3 deletions README.md
Expand Up @@ -13,11 +13,12 @@
## Client APIs
* Apache Cassandra (Thrift and CQL)
* Redis
* Netflix Dynomite (Redis API)
* Elasticsearch
* Elassandra
* Geode
* JanusGraph
* AWS DynamoDB
* Apache Geode
* Apache JanusGraph
* Netflix Dynomite (Redis API)
* Netflix EVCache (Memcache API)

## Features
Expand Down
Binary file modified gradle/wrapper/gradle-wrapper.jar
Binary file not shown.
3 changes: 1 addition & 2 deletions gradle/wrapper/gradle-wrapper.properties
@@ -1,6 +1,5 @@
#Fri Sep 01 15:55:55 PDT 2017
distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-4.1-bin.zip
distributionUrl=https\://services.gradle.org/distributions/gradle-4.5.1-bin.zip
23 changes: 13 additions & 10 deletions gradlew
@@ -1,4 +1,4 @@
#!/usr/bin/env bash
#!/usr/bin/env sh

##############################################################################
##
Expand Down Expand Up @@ -33,11 +33,11 @@ DEFAULT_JVM_OPTS=""
# Use the maximum available, or set MAX_FD != -1 to use that value.
MAX_FD="maximum"

warn ( ) {
warn () {
echo "$*"
}

die ( ) {
die () {
echo
echo "$*"
echo
Expand Down Expand Up @@ -154,16 +154,19 @@ if $cygwin ; then
esac
fi

# Split up the JVM_OPTS And GRADLE_OPTS values into an array, following the shell quoting and substitution rules
function splitJvmOpts() {
JVM_OPTS=("$@")
# Escape application args
save () {
for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done
echo " "
}
eval splitJvmOpts $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS
JVM_OPTS[${#JVM_OPTS[*]}]="-Dorg.gradle.appname=$APP_BASE_NAME"
APP_ARGS=$(save "$@")

# Collect all arguments for the java command, following the shell quoting and substitution rules
eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS"

# by default we should be in the correct project dir, but when run from Finder on Mac, the cwd is wrong
if [[ "$(uname)" == "Darwin" ]] && [[ "$HOME" == "$PWD" ]]; then
if [ "$(uname)" = "Darwin" ] && [ "$HOME" = "$PWD" ]; then
cd "$(dirname "$0")"
fi

exec "$JAVACMD" "${JVM_OPTS[@]}" -classpath "$CLASSPATH" org.gradle.wrapper.GradleWrapperMain "$@"
exec "$JAVACMD" "$@"
4 changes: 4 additions & 0 deletions ndbench-dynamodb-plugins/build.gradle
@@ -0,0 +1,4 @@
dependencies {
compile project(':ndbench-api')
compile "com.amazonaws:aws-java-sdk:latest.release"
}
@@ -0,0 +1,233 @@
/*
* Copyright 2018 Netflix, Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*
*/
package com.netflix.ndbench.plugin.dynamodb;

import java.util.Arrays;
import java.util.List;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import com.amazonaws.AmazonClientException;
import com.amazonaws.AmazonServiceException;
import com.amazonaws.auth.AWSCredentialsProvider;
import com.amazonaws.auth.profile.ProfileCredentialsProvider;
import com.amazonaws.services.dynamodbv2.AmazonDynamoDB;
import com.amazonaws.services.dynamodbv2.AmazonDynamoDBClientBuilder;
import com.amazonaws.services.dynamodbv2.document.DynamoDB;
import com.amazonaws.services.dynamodbv2.document.Item;
import com.amazonaws.services.dynamodbv2.document.PutItemOutcome;
import com.amazonaws.services.dynamodbv2.document.Table;
import com.amazonaws.services.dynamodbv2.document.spec.GetItemSpec;
import com.amazonaws.services.dynamodbv2.model.AttributeDefinition;
import com.amazonaws.services.dynamodbv2.model.DescribeTableRequest;
import com.amazonaws.services.dynamodbv2.model.GlobalTable;
import com.amazonaws.services.dynamodbv2.model.KeySchemaElement;
import com.amazonaws.services.dynamodbv2.model.KeyType;
import com.amazonaws.services.dynamodbv2.model.ProvisionedThroughput;
import com.amazonaws.services.dynamodbv2.model.ScalarAttributeType;
import com.amazonaws.services.dynamodbv2.model.TableDescription;
import com.google.inject.Inject;
import com.google.inject.Singleton;
import com.netflix.ndbench.api.plugin.DataGenerator;
import com.netflix.ndbench.api.plugin.NdBenchClient;
import com.netflix.ndbench.api.plugin.annotations.NdBenchClientPlugin;
import com.netflix.ndbench.api.plugin.common.NdBenchConstants;
import com.netflix.ndbench.plugin.dynamodb.configs.DynamoDBConfigs;

/**
* This NDBench plugin provides a single key value for AWS DynamoDB.
*
* @author ipapapa
*/
@Singleton
@NdBenchClientPlugin("DynamoDBKeyValue")
public class DynamoDBKeyValue implements NdBenchClient {
private final Logger logger = LoggerFactory.getLogger(DynamoDBKeyValue.class);
private static AmazonDynamoDB client;
private static DynamoDB dynamoDB;
private static AWSCredentialsProvider awsCredentialsProvider;
private DynamoDBConfigs config;
private static Table table;

private DataGenerator dataGenerator;

/**
* Credentials will be loaded based on the environment. In AWS, the credentials
* are based on the instance. In a local deployment they will have to provided.
*/
@Inject
public DynamoDBKeyValue(AWSCredentialsProvider credential, DynamoDBConfigs config, DataGenerator dataGenerator) {
this.config = config;
if (System.getenv(NdBenchConstants.DISCOVERY_ENV).equals("AWS")) {
awsCredentialsProvider = credential;
} else {
/*
* The ProfileCredentialsProvider will return your [default] credential profile
* by reading from the credentials file located at
* (/home/username/.aws/credentials).
*/
awsCredentialsProvider = new ProfileCredentialsProvider();
try {
awsCredentialsProvider.getCredentials();
} catch (Exception e) {
throw new AmazonClientException("Cannot load the credentials from the credential profiles file. "
+ "Please make sure that your credentials file is at the correct "
+ "location (/home/<username>/.aws/credentials), and is in valid format.", e);
}
}
}

@Override
public void init(DataGenerator dataGenerator) throws Exception {
logger.info("Initing DynamoDB plugin");
client = AmazonDynamoDBClientBuilder.standard().withCredentials(awsCredentialsProvider).build();
dynamoDB = new DynamoDB(client);

/*
* Create a table with a primary hash key named 'name', which holds a string.
* Several properties such as provisioned throughput and atribute names are
* defined in the configuration interface.
*/

logger.debug("Creating table if it does not exist yet");
table = dynamoDB.createTable(this.config.getTableName(),

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

what happens when the table already exists? does it fail or is ignored?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ResourceInUseException
The operation conflicts with the resource's availability. For example, you attempted to recreate an existing table, or tried to delete a table currently in the CREATING state.

HTTP Status Code: 400

Resource: https://docs.aws.amazon.com/amazondynamodb/latest/APIReference/API_CreateTable.html

Arrays.asList(new KeySchemaElement(config.getAttributeName(), KeyType.HASH)),
Arrays.asList(new AttributeDefinition("Id", ScalarAttributeType.N),
new AttributeDefinition("value", ScalarAttributeType.S)),
new ProvisionedThroughput(this.config.getReadCapacityUnits(), this.config.getWriteCapacityUnits()));

logger.debug("Waiting until the table is in ACTIVE state");
table.waitForActive();

DescribeTableRequest describeTableRequest = new DescribeTableRequest()
.withTableName(this.config.getTableName());
TableDescription tableDescription = client.describeTable(describeTableRequest).getTable();
logger.info("Table Description: " + tableDescription);

logger.info("DynamoDB Plugin initialized");
}

/**
*
* @param key
* @return the item
* @throws Exception
*/
@Override
public String readSingle(String key) throws Exception {
Item item = null;
try {
GetItemSpec spec = new GetItemSpec().withPrimaryKey("Id", key).withConsistentRead(config.consistentRead());

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

consistent reads will use double the RCUs and can't be served by DAX. They're also only consistent within the region and won't be globally consistent when using cross region replication.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Right, the idea is to test it in comparison with a quorum Cassandra read. By default, we will not be using it when we want to test DAX: @DefaultValue("false").

item = table.getItem(spec);
if (item == null) {
return null;
}
} catch (AmazonServiceException ase) {
amazonServiceException(ase);
} catch (AmazonClientException ace) {
amazonClientException(ace);
}
return item.toString();

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

what is item here when an exception is thrown?

}

/**
*
* @param key
* @return A string representation of the output of a PutItemOutcome operation.
* @throws Exception
*/
@Override
public String writeSingle(String key) throws Exception {
PutItemOutcome outcome = null;
try {
Item item = new Item().withPrimaryKey("Id", key).withString("value", this.dataGenerator.getRandomValue());
// Write the item to the table
outcome = table.putItem(item);
if (outcome == null) {
return null;
}

} catch (AmazonServiceException ase) {
amazonServiceException(ase);
} catch (AmazonClientException ace) {
amazonClientException(ace);
}
return outcome.toString();

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

same comments as above

}

@Override
public List<String> readBulk(List<String> keys) throws Exception {
return null;
}

@Override
public List<String> writeBulk(List<String> keys) throws Exception {
return null;
}

@Override
public void shutdown() throws Exception {
try {
logger.info("Issuing DeleteTable request for " + config.getTableName());

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

why do we want to delete the table at shutdown? Can it happen that we restart the test at a pre-filled table?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is a good question. NDBench provides a prefill. This is the proper way to make your database hot. The issue with a managed service (table) is that you can configure with high WCUs and RCUs pay a lot of money and then forget it. After you are done with your tests we need to make sure that there is a way to clean the resources. What other options do you propose?

table.delete();

logger.info("Waiting for " + config.getTableName() + " to be deleted...this may take a while...");

table.waitForDelete();
} catch (Exception e) {
logger.error("DeleteTable request failed for " + config.getTableName());
logger.error(e.getMessage());
}
table.delete(); // cleanup
client.shutdown();
logger.info("DynamoDB shutdown");
}

/*
* Not needed for this plugin
*
* @see com.netflix.ndbench.api.plugin.NdBenchClient#getConnectionInfo()
*/
@Override
public String getConnectionInfo() throws Exception {
return null;
}

@Override
public String runWorkFlow() throws Exception {
return null;
}

private void amazonServiceException(AmazonServiceException ase) {

logger.error("Caught an AmazonServiceException, which means your request made it "

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

DO you want to log a single message or 6 different messages here?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is standard AWS way of reporting errors. In fact, each message presents some information that is useful.

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Agreed. I was just wondering if it is needed for any reason (like grepping) to have the entire message in a single line rather than split across 6 lines. I am fine either ways.

+ "to AWS, but was rejected with an error response for some reason.");
logger.error("Error Message: " + ase.getMessage());
logger.error("HTTP Status Code: " + ase.getStatusCode());
logger.error("AWS Error Code: " + ase.getErrorCode());
logger.error("Error Type: " + ase.getErrorType());
logger.error("Request ID: " + ase.getRequestId());
}

private void amazonClientException(AmazonClientException ace) {
logger.error("Caught an AmazonClientException, which means the client encountered "
+ "a serious internal problem while trying to communicate with AWS, "
+ "such as not being able to access the network.");
logger.error("Error Message: " + ace.getMessage());
}
}
@@ -0,0 +1,42 @@
package com.netflix.ndbench.plugin.dynamodb.configs;

import com.netflix.archaius.api.annotations.Configuration;
import com.netflix.archaius.api.annotations.DefaultValue;

/**
* Configurations for DynamoDB benchmarks
*
* @author ipapapa
*/
@Configuration(prefix = "ndbench.config.dynamodb")
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Just to follow the standard conventions, can you change the prefix to use
NdBenchConstants.PROP_NAMESPACE +

Copy link
Contributor Author

@ipapapa ipapapa Feb 13, 2018

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I saw this too late and I merged it. I will revert the changes and follow up in another PR.

public interface DynamoDBConfigs {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This looks interesting. I am curious about bindings though. Hopefully, this does not require explicit bindings.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good catch, I missed that. I need to add a NdBenchClientPluginGuiceModule to leverage the custom configuration. I will do it.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed it!


@DefaultValue("ndbench-table")
String getTableName();

/*
* Attributes – Each item is composed of one or more attributes. An attribute is

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

can you elaborate on what is an item?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

* a fundamental data element, something that does not need to be broken down
* any further.
*/
@DefaultValue("name")
String getAttributeName();

/*
* Used for provisioned throughput
*/
@DefaultValue("1L")
Long getReadCapacityUnits();

@DefaultValue("1L")
Long getWriteCapacityUnits();

/*
* Consistency: When you request a strongly consistent read, DynamoDB returns a
* response with the most up-to-date data, reflecting the updates from all prior
* write operations that were successful.
*/
@DefaultValue("false")
Boolean consistentRead();

}
@@ -1,3 +1,19 @@
/*
* Copyright 2018 Netflix, Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*
*/
package com.netflix.ndbench.plugins.janusgraph;

import org.janusgraph.core.JanusGraphFactory;
Expand Down
@@ -1,3 +1,19 @@
/*
* Copyright 2018 Netflix, Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*
*/
package com.netflix.ndbench.plugins.janusgraph;

import org.apache.tinkerpop.gremlin.structure.Vertex;
Expand Down