Skip to content
Snippets Groups Projects
Commit 70f4362b authored by Andrey Lomakin's avatar Andrey Lomakin Committed by Kevin Risden
Browse files

[orientdb] OrientDB was updated to 2.2.10 and multithreading bugs fixed (#848)

parent 1afb9af7
No related branches found
No related tags found
No related merge requests found
......@@ -60,12 +60,6 @@ WARNING: Creating a new database will be done safely with multiple threads on a
* ```orientdb.newdb``` - Overwrite the database if it already exists.
* Only effects the ```load``` phase.
* Default: ```false```
* ```orientdb.intent``` - Declare an Intent to the database.
* This is an optimization feature provided by OrientDB: http://orientdb.com/docs/2.1/Performance-Tuning.html#massive-insertion
* Possible values are:
* massiveinsert
* massiveread
* nocache
* ```orientdb.remote.storagetype``` - Storage type of the database on remote server
* This is only required if using a ```remote:``` connection url
......
......@@ -17,197 +17,191 @@
package com.yahoo.ycsb.db;
import com.orientechnologies.common.exception.OException;
import com.orientechnologies.orient.client.remote.OEngineRemote;
import com.orientechnologies.orient.client.remote.OServerAdmin;
import com.orientechnologies.orient.core.db.ODatabaseRecordThreadLocal;
import com.orientechnologies.orient.core.config.OGlobalConfiguration;
import com.orientechnologies.orient.core.db.OPartitionedDatabasePool;
import com.orientechnologies.orient.core.db.document.ODatabaseDocumentTx;
import com.orientechnologies.orient.core.db.record.OIdentifiable;
import com.orientechnologies.orient.core.dictionary.ODictionary;
import com.orientechnologies.orient.core.exception.ODatabaseException;
import com.orientechnologies.orient.core.exception.OConcurrentModificationException;
import com.orientechnologies.orient.core.index.OIndexCursor;
import com.orientechnologies.orient.core.intent.OIntentMassiveInsert;
import com.orientechnologies.orient.core.intent.OIntentMassiveRead;
import com.orientechnologies.orient.core.intent.OIntentNoCache;
import com.orientechnologies.orient.core.record.ORecord;
import com.orientechnologies.orient.core.record.impl.ODocument;
import com.yahoo.ycsb.ByteIterator;
import com.yahoo.ycsb.DB;
import com.yahoo.ycsb.DBException;
import com.yahoo.ycsb.Status;
import com.yahoo.ycsb.StringByteIterator;
import com.yahoo.ycsb.*;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.io.IOException;
import java.util.HashMap;
import java.util.Map.Entry;
import java.util.Properties;
import java.util.Set;
import java.util.Vector;
import java.io.File;
import java.util.*;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
/**
* OrientDB client for YCSB framework.
*/
public class OrientDBClient extends DB {
private static final String URL_PROPERTY = "orientdb.url";
private static final String URL_PROPERTY_DEFAULT =
"plocal:." + File.separator + "target" + File.separator + "databases" + File.separator + "ycsb";
private static final String CLASS = "usertable";
private ODatabaseDocumentTx db;
private ODictionary<ORecord> dictionary;
private boolean isRemote = false;
private static final String URL_PROPERTY = "orientdb.url";
private static final String USER_PROPERTY = "orientdb.user";
private static final String USER_PROPERTY = "orientdb.user";
private static final String USER_PROPERTY_DEFAULT = "admin";
private static final String PASSWORD_PROPERTY = "orientdb.password";
private static final String PASSWORD_PROPERTY = "orientdb.password";
private static final String PASSWORD_PROPERTY_DEFAULT = "admin";
private static final String NEWDB_PROPERTY = "orientdb.newdb";
private static final String NEWDB_PROPERTY = "orientdb.newdb";
private static final String NEWDB_PROPERTY_DEFAULT = "false";
private static final String STORAGE_TYPE_PROPERTY = "orientdb.remote.storagetype";
private static final String INTENT_PROPERTY = "orientdb.intent";
private static final String INTENT_PROPERTY_DEFAULT = "";
private static final String ORIENTDB_DOCUMENT_TYPE = "document";
private static final String DO_TRANSACTIONS_PROPERTY = "dotransactions";
private static final String DO_TRANSACTIONS_PROPERTY_DEFAULT = "true";
private static final String CLASS = "usertable";
private static final String ORIENTDB_DOCUMENT_TYPE = "document";
private static final String ORIENTDB_MASSIVEINSERT = "massiveinsert";
private static final String ORIENTDB_MASSIVEREAD = "massiveread";
private static final String ORIENTDB_NOCACHE = "nocache";
private static final Lock INIT_LOCK = new ReentrantLock();
private static boolean dbChecked = false;
private static volatile OPartitionedDatabasePool databasePool;
private static boolean initialized = false;
private static int clientCounter = 0;
private boolean isRemote = false;
private static final Logger LOG = LoggerFactory.getLogger(OrientDBClient.class);
/**
* This method abstracts the administration of OrientDB namely creating and connecting to a database.
* Creating a database needs to be done in a synchronized method so that multiple threads do not all try
* to run the creation operation simultaneously, this ends in failure.
*
* @param props Workload properties object
* @return a usable ODatabaseDocumentTx object
* @throws DBException
* Initialize any state for this DB. Called once per DB instance; there is one DB instance per client thread.
*/
private static synchronized ODatabaseDocumentTx initDB(Properties props) throws DBException {
String url = props.getProperty(URL_PROPERTY);
public void init() throws DBException {
// initialize OrientDB driver
final Properties props = getProperties();
String url = props.getProperty(URL_PROPERTY, URL_PROPERTY_DEFAULT);
String user = props.getProperty(USER_PROPERTY, USER_PROPERTY_DEFAULT);
String password = props.getProperty(PASSWORD_PROPERTY, PASSWORD_PROPERTY_DEFAULT);
Boolean newdb = Boolean.parseBoolean(props.getProperty(NEWDB_PROPERTY, NEWDB_PROPERTY_DEFAULT));
String remoteStorageType = props.getProperty(STORAGE_TYPE_PROPERTY);
Boolean isrun = Boolean.parseBoolean(props.getProperty(DO_TRANSACTIONS_PROPERTY, DO_TRANSACTIONS_PROPERTY_DEFAULT));
ODatabaseDocumentTx dbconn;
if (url == null) {
throw new DBException(String.format("Required property \"%s\" missing for OrientDBClient", URL_PROPERTY));
}
LOG.info("OrientDB loading database url = " + url);
INIT_LOCK.lock();
try {
clientCounter++;
if (!initialized) {
OGlobalConfiguration.dumpConfiguration(System.out);
// If using a remote database, use the OServerAdmin interface to connect
if (url.startsWith(OEngineRemote.NAME)) {
if (remoteStorageType == null) {
throw new DBException("When connecting to a remote OrientDB instance, " +
"specify a database storage type (plocal or memory) with " + STORAGE_TYPE_PROPERTY);
}
LOG.info("OrientDB loading database url = " + url);
try {
OServerAdmin server = new OServerAdmin(url).connect(user, password);
ODatabaseDocumentTx db = new ODatabaseDocumentTx(url);
if (server.existsDatabase()) {
if (newdb && !isrun) {
LOG.info("OrientDB dropping and recreating fresh db on remote server.");
server.dropDatabase(remoteStorageType);
server.createDatabase(server.getURL(), ORIENTDB_DOCUMENT_TYPE, remoteStorageType);
}
} else {
LOG.info("OrientDB database not found, creating fresh db");
server.createDatabase(server.getURL(), ORIENTDB_DOCUMENT_TYPE, remoteStorageType);
if (db.getStorage().isRemote()) {
isRemote = true;
}
server.close();
dbconn = new ODatabaseDocumentTx(url).open(user, password);
} catch (IOException | OException e) {
throw new DBException(String.format("Error interfacing with %s", url), e);
}
} else {
try {
dbconn = new ODatabaseDocumentTx(url);
if (dbconn.exists()) {
dbconn.open(user, password);
if (newdb && !isrun) {
LOG.info("OrientDB dropping and recreating fresh db.");
dbconn.drop();
dbconn.create();
if (!dbChecked) {
if (!isRemote) {
if (newdb) {
if (db.exists()) {
db.open(user, password);
LOG.info("OrientDB drop and recreate fresh db");
db.drop();
}
db.create();
} else {
if (!db.exists()) {
LOG.info("OrientDB database not found, creating fresh db");
db.create();
}
}
} else {
OServerAdmin server = new OServerAdmin(url).connect(user, password);
if (remoteStorageType == null) {
throw new DBException(
"When connecting to a remote OrientDB instance, "
+ "specify a database storage type (plocal or memory) with "
+ STORAGE_TYPE_PROPERTY);
}
if (newdb) {
if (server.existsDatabase()) {
LOG.info("OrientDB drop and recreate fresh db");
server.dropDatabase(remoteStorageType);
}
server.createDatabase(db.getName(), ORIENTDB_DOCUMENT_TYPE, remoteStorageType);
} else {
if (!server.existsDatabase()) {
LOG.info("OrientDB database not found, creating fresh db");
server.createDatabase(server.getURL(), ORIENTDB_DOCUMENT_TYPE, remoteStorageType);
}
}
server.close();
}
} else {
LOG.info("OrientDB database not found, creating fresh db");
dbconn.create();
}
} catch (ODatabaseException e) {
throw new DBException(String.format("Error interfacing with %s", url), e);
}
}
if (dbconn == null) {
throw new DBException("Could not establish connection to: " + url);
}
dbChecked = true;
}
LOG.info("OrientDB connection created with " + url);
return dbconn;
}
if (db.isClosed()) {
db.open(user, password);
}
@Override
public void init() throws DBException {
Properties props = getProperties();
if (!db.getMetadata().getSchema().existsClass(CLASS)) {
db.getMetadata().getSchema().createClass(CLASS);
}
String intent = props.getProperty(INTENT_PROPERTY, INTENT_PROPERTY_DEFAULT);
db.close();
db = initDB(props);
if (databasePool == null) {
databasePool = new OPartitionedDatabasePool(url, user, password);
}
if (db.getURL().startsWith(OEngineRemote.NAME)) {
isRemote = true;
initialized = true;
}
} catch (Exception e) {
LOG.error("Could not initialize OrientDB connection pool for Loader: " + e.toString());
e.printStackTrace();
} finally {
INIT_LOCK.unlock();
}
dictionary = db.getMetadata().getIndexManager().getDictionary();
if (!db.getMetadata().getSchema().existsClass(CLASS)) {
db.getMetadata().getSchema().createClass(CLASS);
}
}
if (intent.equals(ORIENTDB_MASSIVEINSERT)) {
LOG.info("Declaring intent of MassiveInsert.");
db.declareIntent(new OIntentMassiveInsert());
} else if (intent.equals(ORIENTDB_MASSIVEREAD)) {
LOG.info("Declaring intent of MassiveRead.");
db.declareIntent(new OIntentMassiveRead());
} else if (intent.equals(ORIENTDB_NOCACHE)) {
LOG.info("Declaring intent of NoCache.");
db.declareIntent(new OIntentNoCache());
}
OPartitionedDatabasePool getDatabasePool() {
return databasePool;
}
@Override
public void cleanup() throws DBException {
// Set this thread's db reference (needed for thread safety in testing)
ODatabaseRecordThreadLocal.INSTANCE.set(db);
INIT_LOCK.lock();
try {
clientCounter--;
if (clientCounter == 0) {
databasePool.close();
}
if (db != null) {
db.close();
db = null;
databasePool = null;
initialized = false;
} finally {
INIT_LOCK.unlock();
}
}
@Override
public Status insert(String table, String key, HashMap<String, ByteIterator> values) {
try {
try (ODatabaseDocumentTx db = databasePool.acquire()) {
final ODocument document = new ODocument(CLASS);
for (Entry<String, String> entry : StringByteIterator.getStringMap(values).entrySet()) {
for (Map.Entry<String, String> entry : StringByteIterator.getStringMap(values).entrySet()) {
document.field(entry.getKey(), entry.getValue());
}
document.save();
final ODictionary<ORecord> dictionary = db.getMetadata().getIndexManager().getDictionary();
dictionary.put(key, document);
return Status.OK;
......@@ -219,18 +213,24 @@ public class OrientDBClient extends DB {
@Override
public Status delete(String table, String key) {
try {
dictionary.remove(key);
return Status.OK;
} catch (Exception e) {
e.printStackTrace();
while (true) {
try (ODatabaseDocumentTx db = databasePool.acquire()) {
final ODictionary<ORecord> dictionary = db.getMetadata().getIndexManager().getDictionary();
dictionary.remove(key);
return Status.OK;
} catch (OConcurrentModificationException cme) {
continue;
} catch (Exception e) {
e.printStackTrace();
return Status.ERROR;
}
}
return Status.ERROR;
}
@Override
public Status read(String table, String key, Set<String> fields, HashMap<String, ByteIterator> result) {
try {
try (ODatabaseDocumentTx db = databasePool.acquire()) {
final ODictionary<ORecord> dictionary = db.getMetadata().getIndexManager().getDictionary();
final ODocument document = dictionary.get(key);
if (document != null) {
if (fields != null) {
......@@ -252,48 +252,63 @@ public class OrientDBClient extends DB {
@Override
public Status update(String table, String key, HashMap<String, ByteIterator> values) {
try {
final ODocument document = dictionary.get(key);
if (document != null) {
for (Entry<String, String> entry : StringByteIterator.getStringMap(values).entrySet()) {
document.field(entry.getKey(), entry.getValue());
while (true) {
try (ODatabaseDocumentTx db = databasePool.acquire()) {
final ODictionary<ORecord> dictionary = db.getMetadata().getIndexManager().getDictionary();
final ODocument document = dictionary.get(key);
if (document != null) {
for (Map.Entry<String, String> entry : StringByteIterator.getStringMap(values).entrySet()) {
document.field(entry.getKey(), entry.getValue());
}
document.save();
return Status.OK;
}
document.save();
return Status.OK;
} catch (OConcurrentModificationException cme) {
continue;
} catch (Exception e) {
e.printStackTrace();
return Status.ERROR;
}
} catch (Exception e) {
e.printStackTrace();
}
return Status.ERROR;
}
@Override
public Status scan(String table, String startkey, int recordcount, Set<String> fields,
Vector<HashMap<String, ByteIterator>> result) {
Vector<HashMap<String, ByteIterator>> result) {
if (isRemote) {
// Iterator methods needed for scanning are Unsupported for remote database connections.
LOG.warn("OrientDB scan operation is not implemented for remote database connections.");
return Status.NOT_IMPLEMENTED;
}
try {
int entrycount = 0;
try (ODatabaseDocumentTx db = databasePool.acquire()) {
final ODictionary<ORecord> dictionary = db.getMetadata().getIndexManager().getDictionary();
final OIndexCursor entries = dictionary.getIndex().iterateEntriesMajor(startkey, true, true);
while (entries.hasNext() && entrycount < recordcount) {
final OIdentifiable entry = entries.next();
final ODocument document = entry.getRecord();
int currentCount = 0;
while (entries.hasNext()) {
final ODocument document = entries.next().getRecord();
final HashMap<String, ByteIterator> map = new HashMap<String, ByteIterator>();
final HashMap<String, ByteIterator> map = new HashMap<>();
result.add(map);
if (fields != null && !fields.isEmpty()) {
if (fields != null) {
for (String field : fields) {
map.put(field, new StringByteIterator((String) document.field(field)));
}
} else {
for (String field : document.fieldNames()) {
map.put(field, new StringByteIterator((String) document.field(field)));
}
}
entrycount++;
currentCount++;
if (currentCount >= recordcount) {
break;
}
}
return Status.OK;
......@@ -302,11 +317,4 @@ public class OrientDBClient extends DB {
}
return Status.ERROR;
}
/**
* Access method to db variable for unit testing.
**/
ODatabaseDocumentTx getDB() {
return db;
}
}
......@@ -17,6 +17,8 @@
package com.yahoo.ycsb.db;
import com.orientechnologies.orient.core.db.OPartitionedDatabasePool;
import com.orientechnologies.orient.core.db.document.ODatabaseDocumentTx;
import com.orientechnologies.orient.core.dictionary.ODictionary;
import com.orientechnologies.orient.core.record.ORecord;
import com.orientechnologies.orient.core.record.impl.ODocument;
......@@ -25,6 +27,7 @@ import com.yahoo.ycsb.DBException;
import com.yahoo.ycsb.StringByteIterator;
import org.junit.*;
import java.util.*;
import static org.junit.Assert.*;
......@@ -34,14 +37,13 @@ import static org.junit.Assert.*;
*/
public class OrientDBClientTest {
// TODO: This must be copied because it is private in OrientDBClient, but this should defer to table property.
private static final String CLASS = "usertable";
private static final int FIELD_LENGTH = 32;
private static final String CLASS = "usertable";
private static final int FIELD_LENGTH = 32;
private static final String FIELD_PREFIX = "FIELD";
private static final String KEY_PREFIX = "user";
private static final int NUM_FIELDS = 3;
private static final String TEST_DB_URL = "memory:test";
private static final String KEY_PREFIX = "user";
private static final int NUM_FIELDS = 3;
private static final String TEST_DB_URL = "memory:test";
private static ODictionary<ORecord> orientDBDictionary;
private static OrientDBClient orientDBClient = null;
@Before
......@@ -54,7 +56,6 @@ public class OrientDBClientTest {
orientDBClient.setProperties(p);
orientDBClient.init();
orientDBDictionary = orientDBClient.getDB().getDictionary();
}
@After
......@@ -101,12 +102,17 @@ public class OrientDBClientTest {
String insertKey = "user0";
Map<String, ByteIterator> insertMap = insertRow(insertKey);
ODocument result = orientDBDictionary.get(insertKey);
OPartitionedDatabasePool pool = orientDBClient.getDatabasePool();
try(ODatabaseDocumentTx db = pool.acquire()) {
ODictionary<ORecord> dictionary = db.getDictionary();
ODocument result = dictionary.get(insertKey);
assertTrue("Assert a row was inserted.", result != null);
assertTrue("Assert a row was inserted.", result != null);
for (int i = 0; i < NUM_FIELDS; i++) {
assertEquals("Assert all inserted columns have correct values.", result.field(FIELD_PREFIX + i), insertMap.get(FIELD_PREFIX + i).toString());
for (int i = 0; i < NUM_FIELDS; i++) {
assertEquals("Assert all inserted columns have correct values.", result.field(FIELD_PREFIX + i),
insertMap.get(FIELD_PREFIX + i).toString());
}
}
}
......@@ -117,14 +123,19 @@ public class OrientDBClientTest {
String user1 = "user1";
String user2 = "user2";
// Manually insert three documents
for(String key: Arrays.asList(user0, user1, user2)) {
ODocument doc = new ODocument(CLASS);
for (int i = 0; i < NUM_FIELDS; i++) {
doc.field(FIELD_PREFIX + i, preupdateString);
OPartitionedDatabasePool pool = orientDBClient.getDatabasePool();
try(ODatabaseDocumentTx db = pool.acquire()) {
// Manually insert three documents
for (String key : Arrays.asList(user0, user1, user2)) {
ODocument doc = new ODocument(CLASS);
for (int i = 0; i < NUM_FIELDS; i++) {
doc.field(FIELD_PREFIX + i, preupdateString);
}
doc.save();
ODictionary<ORecord> dictionary = db.getDictionary();
dictionary.put(key, doc);
}
doc.save();
orientDBDictionary.put(key, doc);
}
HashMap<String, ByteIterator> updateMap = new HashMap<>();
......@@ -134,22 +145,26 @@ public class OrientDBClientTest {
orientDBClient.update(CLASS, user1, updateMap);
// Ensure that user0 record was not changed
ODocument result = orientDBDictionary.get(user0);
for (int i = 0; i < NUM_FIELDS; i++) {
assertEquals("Assert first row fields contain preupdateString", result.field(FIELD_PREFIX + i), preupdateString);
}
try(ODatabaseDocumentTx db = pool.acquire()) {
ODictionary<ORecord> dictionary = db.getDictionary();
// Ensure that user0 record was not changed
ODocument result = dictionary.get(user0);
for (int i = 0; i < NUM_FIELDS; i++) {
assertEquals("Assert first row fields contain preupdateString", result.field(FIELD_PREFIX + i), preupdateString);
}
// Check that all the columns have expected values for user1 record
result = orientDBDictionary.get(user1);
for (int i = 0; i < NUM_FIELDS; i++) {
assertEquals("Assert updated row fields are correct", result.field(FIELD_PREFIX + i), updateMap.get(FIELD_PREFIX + i).toString());
}
// Check that all the columns have expected values for user1 record
result = dictionary.get(user1);
for (int i = 0; i < NUM_FIELDS; i++) {
assertEquals("Assert updated row fields are correct", result.field(FIELD_PREFIX + i),
updateMap.get(FIELD_PREFIX + i).toString());
}
// Ensure that user2 record was not changed
result = orientDBDictionary.get(user2);
for (int i = 0; i < NUM_FIELDS; i++) {
assertEquals("Assert third row fields contain preupdateString", result.field(FIELD_PREFIX + i), preupdateString);
// Ensure that user2 record was not changed
result = dictionary.get(user2);
for (int i = 0; i < NUM_FIELDS; i++) {
assertEquals("Assert third row fields contain preupdateString", result.field(FIELD_PREFIX + i), preupdateString);
}
}
}
......@@ -164,7 +179,7 @@ public class OrientDBClientTest {
readFields.add("FIELD0");
orientDBClient.read(CLASS, insertKey, readFields, readResultMap);
assertEquals("Assert that result has correct number of fields", readFields.size(), readResultMap.size());
for (String field: readFields) {
for (String field : readFields) {
assertEquals("Assert " + field + " was read correctly", insertMap.get(field).toString(), readResultMap.get(field).toString());
}
......@@ -175,7 +190,7 @@ public class OrientDBClientTest {
readFields.add("FIELD2");
orientDBClient.read(CLASS, insertKey, readFields, readResultMap);
assertEquals("Assert that result has correct number of fields", readFields.size(), readResultMap.size());
for (String field: readFields) {
for (String field : readFields) {
assertEquals("Assert " + field + " was read correctly", insertMap.get(field).toString(), readResultMap.get(field).toString());
}
}
......@@ -192,9 +207,14 @@ public class OrientDBClientTest {
orientDBClient.delete(CLASS, user1);
assertNotNull("Assert user0 still exists", orientDBDictionary.get(user0));
assertNull("Assert user1 does not exist", orientDBDictionary.get(user1));
assertNotNull("Assert user2 still exists", orientDBDictionary.get(user2));
OPartitionedDatabasePool pool = orientDBClient.getDatabasePool();
try(ODatabaseDocumentTx db = pool.acquire()) {
ODictionary<ORecord> dictionary = db.getDictionary();
assertNotNull("Assert user0 still exists", dictionary.get(user0));
assertNull("Assert user1 does not exist", dictionary.get(user1));
assertNotNull("Assert user2 still exists", dictionary.get(user2));
}
}
@Test
......@@ -208,7 +228,7 @@ public class OrientDBClientTest {
Set<String> fieldSet = new HashSet<>();
fieldSet.add("FIELD0");
fieldSet.add("FIELD1");
int startIndex = 1;
int startIndex = 0;
int resultRows = 3;
Vector<HashMap<String, ByteIterator>> resultVector = new Vector<>();
......@@ -217,20 +237,14 @@ public class OrientDBClientTest {
// Check the resultVector is the correct size
assertEquals("Assert the correct number of results rows were returned", resultRows, resultVector.size());
/**
* Part of the known issue about the broken iterator in orientdb is that the iterator
* starts at index 1 instead of index 0. Because of this, to test it we must increment
* the start index. When that known issue has been fixed, remove the increment below.
* Track the issue here: https://github.com/orientechnologies/orientdb/issues/5541
* This fix was implemented for orientechnologies:orientdb-client:2.1.8
*/
int testIndex = startIndex;
// Check each vector row to make sure we have the correct fields
for (HashMap<String, ByteIterator> result: resultVector) {
for (HashMap<String, ByteIterator> result : resultVector) {
assertEquals("Assert that this row has the correct number of fields", fieldSet.size(), result.size());
for (String field: fieldSet) {
assertEquals("Assert this field is correct in this row", keyMap.get(KEY_PREFIX + testIndex).get(field).toString(), result.get(field).toString());
for (String field : fieldSet) {
assertEquals("Assert this field is correct in this row", keyMap.get(KEY_PREFIX + testIndex).get(field).toString(),
result.get(field).toString());
}
testIndex++;
}
......
......@@ -82,7 +82,7 @@ LICENSE file.
<!--<mapkeeper.version>1.0</mapkeeper.version>-->
<mongodb.version>3.0.3</mongodb.version>
<mongodb.async.version>2.0.1</mongodb.async.version>
<orientdb.version>2.1.8</orientdb.version>
<orientdb.version>2.2.10</orientdb.version>
<redis.version>2.0.0</redis.version>
<s3.version>1.10.20</s3.version>
<voldemort.version>0.81</voldemort.version>
......
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment