blob: bd89733cec25ea49a97f8892a583b04c9549e90e [file] [log] [blame]
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.apache.bookkeeper.metastore;
import static org.apache.bookkeeper.metastore.MetastoreScannableTable.EMPTY_END_KEY;
import static org.apache.bookkeeper.metastore.MetastoreScannableTable.EMPTY_START_KEY;
import static org.apache.bookkeeper.metastore.MetastoreTable.ALL_FIELDS;
import static org.apache.bookkeeper.metastore.MetastoreTable.NON_FIELDS;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.assertNull;
import static org.junit.Assert.assertTrue;
import static org.junit.Assert.fail;
import com.google.common.collect.MapDifference;
import com.google.common.collect.Maps;
import com.google.common.collect.Sets;
import java.util.Collections;
import java.util.HashSet;
import java.util.Iterator;
import java.util.Map;
import java.util.Set;
import java.util.TreeMap;
import org.apache.bookkeeper.metastore.InMemoryMetastoreTable.MetadataVersion;
import org.apache.bookkeeper.metastore.MSException.Code;
import org.apache.bookkeeper.metastore.MetastoreScannableTable.Order;
import org.apache.bookkeeper.versioning.Version;
import org.apache.bookkeeper.versioning.Versioned;
import org.apache.commons.configuration.CompositeConfiguration;
import org.apache.commons.configuration.Configuration;
import org.junit.After;
import org.junit.Before;
import org.junit.Test;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* Test the metastore.
*/
public class TestMetaStore {
private static final Logger logger = LoggerFactory.getLogger(TestMetaStore.class);
protected static final String TABLE = "myTable";
protected static final String RECORDID = "test";
protected static final String FIELD_NAME = "name";
protected static final String FIELD_COUNTER = "counter";
protected String getFieldFromValue(Value value, String field) {
byte[] v = value.getField(field);
return v == null ? null : new String(v);
}
protected static Value makeValue(String name, Integer counter) {
Value data = new Value();
if (name != null) {
data.setField(FIELD_NAME, name.getBytes());
}
if (counter != null) {
data.setField(FIELD_COUNTER, counter.toString().getBytes());
}
return data;
}
/**
* A metastore record.
*/
protected class Record {
String name;
Integer counter;
Version version;
public Record() {
}
public Record(String name, Integer counter, Version version) {
this.name = name;
this.counter = counter;
this.version = version;
}
public Record(Versioned<Value> vv) {
version = vv.getVersion();
Value value = vv.getValue();
if (value == null) {
return;
}
name = getFieldFromValue(value, FIELD_NAME);
String c = getFieldFromValue(value, FIELD_COUNTER);
if (c != null) {
counter = Integer.parseInt(c);
}
}
public Version getVersion() {
return version;
}
public Value getValue() {
return TestMetaStore.makeValue(name, counter);
}
public Versioned<Value> getVersionedValue() {
return new Versioned<Value>(getValue(), version);
}
public void merge(String name, Integer counter, Version version) {
if (name != null) {
this.name = name;
}
if (counter != null) {
this.counter = counter;
}
if (version != null) {
this.version = version;
}
}
public void merge(Record record) {
merge(record.name, record.counter, record.version);
}
public void checkEqual(Versioned<Value> vv) {
Version v = vv.getVersion();
Value value = vv.getValue();
assertEquals(name, getFieldFromValue(value, FIELD_NAME));
String c = getFieldFromValue(value, FIELD_COUNTER);
if (counter == null) {
assertNull(c);
} else {
assertEquals(counter.toString(), c);
}
assertTrue(isEqualVersion(version, v));
}
}
protected MetaStore metastore;
protected MetastoreScannableTable myActualTable;
protected MetastoreScannableTableAsyncToSyncConverter myTable;
protected String getMetaStoreName() {
return InMemoryMetaStore.class.getName();
}
protected Configuration getConfiguration() {
return new CompositeConfiguration();
}
protected Version newBadVersion() {
return new MetadataVersion(-1);
}
protected Version nextVersion(Version version) {
if (Version.NEW == version) {
return new MetadataVersion(0);
}
if (Version.ANY == version) {
return Version.ANY;
}
assertTrue(version instanceof MetadataVersion);
return new MetadataVersion(((MetadataVersion) version).incrementVersion());
}
private void checkVersion(Version v) {
assertNotNull(v);
if (v != Version.NEW && v != Version.ANY) {
assertTrue(v instanceof MetadataVersion);
}
}
protected boolean isEqualVersion(Version v1, Version v2) {
checkVersion(v1);
checkVersion(v2);
return v1.compare(v2) == Version.Occurred.CONCURRENTLY;
}
@Before
public void setUp() throws Exception {
metastore = MetastoreFactory.createMetaStore(getMetaStoreName());
Configuration config = getConfiguration();
metastore.init(config, metastore.getVersion());
myActualTable = metastore.createScannableTable(TABLE);
myTable = new MetastoreScannableTableAsyncToSyncConverter(myActualTable);
// setup a clean environment
clearTable();
}
@After
public void tearDown() throws Exception {
// also clear table after test
clearTable();
myActualTable.close();
metastore.close();
}
void checkExpectedValue(Versioned<Value> vv, String expectedName,
Integer expectedCounter, Version expectedVersion) {
Record expected = new Record(expectedName, expectedCounter, expectedVersion);
expected.checkEqual(vv);
}
protected Integer getRandom() {
return (int) (Math.random() * 65536);
}
protected Versioned<Value> getRecord(String recordId) throws Exception {
try {
return myTable.get(recordId);
} catch (MSException.NoKeyException nke) {
return null;
}
}
/**
* get record with specific fields, assume record EXIST!
*/
protected Versioned<Value> getExistRecordFields(String recordId, Set<String> fields)
throws Exception {
Versioned<Value> retValue = myTable.get(recordId, fields);
return retValue;
}
/**
* put and check fields.
*/
protected void putAndCheck(String recordId, String name,
Integer counter, Version version,
Record expected, Code expectedCode)
throws Exception {
Version retVersion = null;
Code code = Code.OperationFailure;
try {
retVersion = myTable.put(recordId, makeValue(name, counter), version);
code = Code.OK;
} catch (MSException.BadVersionException bve) {
code = Code.BadVersion;
} catch (MSException.NoKeyException nke) {
code = Code.NoKey;
} catch (MSException.KeyExistsException kee) {
code = Code.KeyExists;
}
assertEquals(expectedCode, code);
// get and check all fields of record
if (Code.OK == code) {
assertTrue(isEqualVersion(retVersion, nextVersion(version)));
expected.merge(name, counter, retVersion);
}
Versioned<Value> existedVV = getRecord(recordId);
if (null == expected) {
assertNull(existedVV);
} else {
expected.checkEqual(existedVV);
}
}
protected void clearTable() throws Exception {
MetastoreCursor cursor = myTable.openCursor();
if (!cursor.hasMoreEntries()) {
return;
}
while (cursor.hasMoreEntries()) {
Iterator<MetastoreTableItem> iter = cursor.readEntries(99);
while (iter.hasNext()) {
MetastoreTableItem item = iter.next();
String key = item.getKey();
myTable.remove(key, Version.ANY);
}
}
cursor.close();
}
/**
* Test (get, get partial field, remove) on non-existent element.
*/
@Test
public void testNonExistent() throws Exception {
// get
try {
myTable.get(RECORDID);
fail("Should fail to get a non-existent key");
} catch (MSException.NoKeyException nke) {
}
// get partial field
Set<String> fields =
new HashSet<String>(Collections.singletonList(FIELD_COUNTER));
try {
myTable.get(RECORDID, fields);
fail("Should fail to get a non-existent key with specified fields");
} catch (MSException.NoKeyException nke) {
}
// remove
try {
myTable.remove(RECORDID, Version.ANY);
fail("Should fail to delete a non-existent key");
} catch (MSException.NoKeyException nke) {
}
}
/**
* Test usage of get operation on (full and partial) fields.
*/
@Test
public void testGet() throws Exception {
Versioned<Value> vv;
final Set<String> fields =
new HashSet<String>(Collections.singletonList(FIELD_NAME));
final String name = "get";
final Integer counter = getRandom();
// put test item
Version version = myTable.put(RECORDID, makeValue(name, counter), Version.NEW);
assertNotNull(version);
// fetch with all fields
vv = getExistRecordFields(RECORDID, ALL_FIELDS);
checkExpectedValue(vv, name, counter, version);
// partial get name
vv = getExistRecordFields(RECORDID, fields);
checkExpectedValue(vv, name, null, version);
// non fields
vv = getExistRecordFields(RECORDID, NON_FIELDS);
checkExpectedValue(vv, null, null, version);
// get null key should fail
try {
getExistRecordFields(null, NON_FIELDS);
fail("Should fail to get null key with NON fields");
} catch (MSException.IllegalOpException ioe) {
}
try {
getExistRecordFields(null, ALL_FIELDS);
fail("Should fail to get null key with ALL fields.");
} catch (MSException.IllegalOpException ioe) {
}
try {
getExistRecordFields(null, fields);
fail("Should fail to get null key with fields " + fields);
} catch (MSException.IllegalOpException ioe) {
}
}
/**
* Test usage of put operation with (full and partial) fields.
*/
@Test
public void testPut() throws Exception {
final Integer counter = getRandom();
final String name = "put";
Version version;
/**
* test correct version put
*/
// put test item
version = myTable.put(RECORDID, makeValue(name, counter), Version.NEW);
assertNotNull(version);
Record expected = new Record(name, counter, version);
// correct version put with only name field changed
putAndCheck(RECORDID, "name1", null, expected.getVersion(), expected, Code.OK);
// correct version put with only counter field changed
putAndCheck(RECORDID, null, counter + 1, expected.getVersion(), expected, Code.OK);
// correct version put with all fields filled
putAndCheck(RECORDID, "name2", counter + 2, expected.getVersion(), expected, Code.OK);
// test put exist entry with Version.ANY
checkPartialPut("put exist entry with Version.ANY", Version.ANY, expected, Code.OK);
/**
* test bad version put
*/
// put to existed entry with Version.NEW
badVersionedPut(Version.NEW, Code.KeyExists);
// put to existed entry with bad version
badVersionedPut(newBadVersion(), Code.BadVersion);
// remove the entry
myTable.remove(RECORDID, Version.ANY);
// put to non-existent entry with bad version
badVersionedPut(newBadVersion(), Code.NoKey);
// put to non-existent entry with Version.ANY
badVersionedPut(Version.ANY, Code.NoKey);
/**
* test illegal arguments
*/
illegalPut(null, Version.NEW);
illegalPut(makeValue("illegal value", getRandom()), null);
illegalPut(null, null);
}
protected void badVersionedPut(Version badVersion, Code expectedCode) throws Exception {
Versioned<Value> vv = getRecord(RECORDID);
Record expected = null;
if (expectedCode != Code.NoKey) {
assertNotNull(vv);
expected = new Record(vv);
}
checkPartialPut("badVersionedPut", badVersion, expected, expectedCode);
}
protected void checkPartialPut(String name, Version version, Record expected, Code expectedCode)
throws Exception {
Integer counter;
// bad version put with all fields filled
counter = getRandom();
putAndCheck(RECORDID, name + counter, counter, version, expected, expectedCode);
// bad version put with only name field changed
counter = getRandom();
putAndCheck(RECORDID, name + counter, null, version, expected, expectedCode);
// bad version put with only counter field changed
putAndCheck(RECORDID, null, counter, version, expected, expectedCode);
}
protected void illegalPut(Value value, Version version) throws MSException {
try {
myTable.put(RECORDID, value, version);
fail("Should fail to do versioned put with illegal arguments");
} catch (MSException.IllegalOpException ioe) {
}
}
/**
* Test usage of (unconditional remove, BadVersion remove, CorrectVersion
* remove) operation.
*/
@Test
public void testRemove() throws Exception {
final Integer counter = getRandom();
final String name = "remove";
Version version;
// insert test item
version = myTable.put(RECORDID, makeValue(name, counter), Version.NEW);
assertNotNull(version);
// test unconditional remove
myTable.remove(RECORDID, Version.ANY);
// insert test item
version = myTable.put(RECORDID, makeValue(name, counter), Version.NEW);
assertNotNull(version);
// test remove with bad version
try {
myTable.remove(RECORDID, Version.NEW);
fail("Should fail to remove a given key with bad version");
} catch (MSException.BadVersionException bve) {
}
try {
myTable.remove(RECORDID, newBadVersion());
fail("Should fail to remove a given key with bad version");
} catch (MSException.BadVersionException bve) {
}
// test remove with correct version
myTable.remove(RECORDID, version);
}
protected void openCursorTest(MetastoreCursor cursor, Map<String, Value> expectedValues,
int numEntriesPerScan) throws Exception {
try {
Map<String, Value> entries = Maps.newHashMap();
while (cursor.hasMoreEntries()) {
Iterator<MetastoreTableItem> iter = cursor.readEntries(numEntriesPerScan);
while (iter.hasNext()) {
MetastoreTableItem item = iter.next();
entries.put(item.getKey(), item.getValue().getValue());
}
}
MapDifference<String, Value> diff = Maps.difference(expectedValues, entries);
assertTrue(diff.areEqual());
} finally {
cursor.close();
}
}
void openRangeCursorTest(String firstKey, boolean firstInclusive,
String lastKey, boolean lastInclusive,
Order order, Set<String> fields,
Iterator<Map.Entry<String, Value>> expectedValues,
int numEntriesPerScan) throws Exception {
try (MetastoreCursor cursor =
myTable.openCursor(firstKey, firstInclusive, lastKey, lastInclusive, order, fields)) {
while (cursor.hasMoreEntries()) {
Iterator<MetastoreTableItem> iter = cursor.readEntries(numEntriesPerScan);
while (iter.hasNext()) {
assertTrue(expectedValues.hasNext());
MetastoreTableItem item = iter.next();
Map.Entry<String, Value> expectedItem = expectedValues.next();
assertEquals(expectedItem.getKey(), item.getKey());
assertEquals(expectedItem.getValue(), item.getValue().getValue());
}
}
assertFalse(expectedValues.hasNext());
}
}
/**
* Test usage of (scan) operation on (full and partial) fields.
*/
@Test
public void testOpenCursor() throws Exception {
TreeMap<String, Value> allValues = Maps.newTreeMap();
TreeMap<String, Value> partialValues = Maps.newTreeMap();
TreeMap<String, Value> nonValues = Maps.newTreeMap();
Set<String> counterFields = Sets.newHashSet(FIELD_COUNTER);
for (int i = 5; i < 24; i++) {
char c = (char) ('a' + i);
String key = String.valueOf(c);
Value v = makeValue("value" + i, i);
Value cv = v.project(counterFields);
Value nv = v.project(NON_FIELDS);
myTable.put(key, new Value(v), Version.NEW);
allValues.put(key, v);
partialValues.put(key, cv);
nonValues.put(key, nv);
}
// test open cursor
MetastoreCursor cursor = myTable.openCursor(ALL_FIELDS);
openCursorTest(cursor, allValues, 7);
cursor = myTable.openCursor(counterFields);
openCursorTest(cursor, partialValues, 7);
cursor = myTable.openCursor(NON_FIELDS);
openCursorTest(cursor, nonValues, 7);
// test order inclusive exclusive
Iterator<Map.Entry<String, Value>> expectedIterator;
expectedIterator = allValues.subMap("l", true, "u", true).entrySet().iterator();
openRangeCursorTest("l", true, "u", true, Order.ASC, ALL_FIELDS, expectedIterator, 7);
expectedIterator = allValues.descendingMap().subMap("u", true, "l", true)
.entrySet().iterator();
openRangeCursorTest("u", true, "l", true, Order.DESC, ALL_FIELDS, expectedIterator, 7);
expectedIterator = allValues.subMap("l", false, "u", false).entrySet().iterator();
openRangeCursorTest("l", false, "u", false, Order.ASC, ALL_FIELDS, expectedIterator, 7);
expectedIterator = allValues.descendingMap().subMap("u", false, "l", false)
.entrySet().iterator();
openRangeCursorTest("u", false, "l", false, Order.DESC, ALL_FIELDS, expectedIterator, 7);
expectedIterator = allValues.subMap("l", true, "u", false).entrySet().iterator();
openRangeCursorTest("l", true, "u", false, Order.ASC, ALL_FIELDS, expectedIterator, 7);
expectedIterator = allValues.descendingMap().subMap("u", true, "l", false)
.entrySet().iterator();
openRangeCursorTest("u", true, "l", false, Order.DESC, ALL_FIELDS, expectedIterator, 7);
expectedIterator = allValues.subMap("l", false, "u", true).entrySet().iterator();
openRangeCursorTest("l", false, "u", true, Order.ASC, ALL_FIELDS, expectedIterator, 7);
expectedIterator = allValues.descendingMap().subMap("u", false, "l", true)
.entrySet().iterator();
openRangeCursorTest("u", false, "l", true, Order.DESC, ALL_FIELDS, expectedIterator, 7);
// test out of range
String firstKey = "f";
String lastKey = "x";
expectedIterator = allValues.subMap(firstKey, true, lastKey, true)
.entrySet().iterator();
openRangeCursorTest("a", true, "z", true, Order.ASC, ALL_FIELDS, expectedIterator, 7);
expectedIterator = allValues.subMap("l", true, lastKey, true).entrySet().iterator();
openRangeCursorTest("l", true, "z", true, Order.ASC, ALL_FIELDS, expectedIterator, 7);
expectedIterator = allValues.subMap(firstKey, true, "u", true).entrySet().iterator();
openRangeCursorTest("a", true, "u", true, Order.ASC, ALL_FIELDS, expectedIterator, 7);
// test EMPTY_START_KEY and EMPTY_END_KEY
expectedIterator = allValues.subMap(firstKey, true, "u", true).entrySet().iterator();
openRangeCursorTest(EMPTY_START_KEY, true, "u", true, Order.ASC, ALL_FIELDS,
expectedIterator, 7);
expectedIterator = allValues.descendingMap().subMap(lastKey, true, "l", true)
.entrySet().iterator();
openRangeCursorTest(EMPTY_END_KEY, true, "l", true, Order.DESC, ALL_FIELDS,
expectedIterator, 7);
// test illegal arguments
try {
myTable.openCursor("a", true, "z", true, Order.DESC, ALL_FIELDS);
fail("Should fail with wrong range");
} catch (MSException.IllegalOpException ioe) {
}
try {
myTable.openCursor("z", true, "a", true, Order.ASC, ALL_FIELDS);
fail("Should fail with wrong range");
} catch (MSException.IllegalOpException ioe) {
}
try {
myTable.openCursor("a", true, "z", true, null, ALL_FIELDS);
fail("Should fail with null order");
} catch (MSException.IllegalOpException ioe) {
}
}
}