diff --git a/README.md b/README.md index 05c0e47..bbe928f 100644 --- a/README.md +++ b/README.md @@ -1,2 +1,42 @@ # KVIndex -A simple B+Tree based clustered Index implementation for Key-Value storage. + +[![MIT Licence](https://badges.frapsoft.com/os/mit/mit.svg?v=103)](https://opensource.org/licenses/mit-license.php) + +A simple hash index implementation for random-read-only key-value storage. + +## Usage + +1. Call `KVIndex.initialize(filename)` to create index and initialize. +2. Concurrently call `KVIndex.get()` to query. + +## Benchmark + +Platform: 2.4GHz 2-core CPU, 16 GB RAM, 512 GB APPLE SSD + +![](doc/benchmark.png) + +The benchmark shows that the number of `N` has little effect on query performance. However, due to the bottleneck of disk I/O, multithreading can hardly increase query performance. + + +## Implementation + +The project implements a hash index for query-only key-value storage. + +All indexes are stored in the disk. There are totally (by default) 512 index files. + +An index file consists of several slots, each slot is (by default) 13 bytes, containing key_size, address, value_size, and next_slot_id. Collisions are handled with linked lists, where next_slot_id is used. + +A query first calculates the hashcode of the key. Secondly, the address of the corresponding record is retrieved from the index file. At last, read the value from the data file and return it. The second and third steps may repeat some times if there are hash collisions. The amortized number of disk accesses is 2. + +## Future work + +There are a few major factors that can be optimized to improve performance. + +1. [ ] better hash function +2. [ ] memory buffer +3. [ ] parallel initialization +4. [ ] I/O optimization + +## Contact + +Please e-mail me to ekexium@gmail.com for any suggestions. \ No newline at end of file diff --git a/build.gradle b/build.gradle index 4a5eb26..925a87d 100644 --- a/build.gradle +++ b/build.gradle @@ -14,6 +14,7 @@ repositories { dependencies { // testCompile group: 'junit', name: 'junit', version: '4.12' testCompile 'org.junit.jupiter:junit-jupiter:5.5.2' + testCompile 'org.apache.commons:commons-lang3:3.0' } test { diff --git a/doc/benchmark.png b/doc/benchmark.png new file mode 100644 index 0000000..d0e9b72 Binary files /dev/null and b/doc/benchmark.png differ diff --git a/src/main/java/HashFunc.java b/src/main/java/HashFunc.java index bb78674..68af243 100644 --- a/src/main/java/HashFunc.java +++ b/src/main/java/HashFunc.java @@ -1,12 +1,11 @@ import java.nio.ByteBuffer; +/** + * A DJB hash function that maps key(bytes[], <= 4096 bytes) to an address(long). + * + * The capacity is set to the nearest upper 2^k to (N / preferred_load_factor). + */ class HashFunc { - /** - * A DJB hash function that maps key(bytes[], <= 4096 bytes) to an address(long). - * - * The capacity is set to the nearest upper 2^k to (N / preferred_load_factor). - */ - long N; // size of the set of keys long capacity; // capacity of slots int loadFactorInv = 2; // the reciprocal of preferred load factor @@ -34,6 +33,14 @@ class HashFunc { Log.logi("Hash capacity = " + capacity); } + /** + * DJB hash function. + * Thread-safe. + * + * @param key + * Key of hash function + * @return The hashcode of key + */ long hash(byte[] key) { long hash = 5381; diff --git a/src/main/java/KVIndex.java b/src/main/java/KVIndex.java index 3e971b0..dae724e 100644 --- a/src/main/java/KVIndex.java +++ b/src/main/java/KVIndex.java @@ -5,30 +5,51 @@ import java.nio.ByteBuffer; import java.util.Arrays; +/** + * The main class of KVIndex using hash indexing. + * + * Input: + * A binary data file consisting of records. + * Each record is of format (key_size, key, value_size, value). + * + * Output: + * get(key) returns the corresponding value. + * + * Implementation: + * During initialization, create index files for all records. + * Each files consists of several slots. + * + * Default slot structure: + * | key_size | address | value_size | next_slot_id | + * | 2 | 5 | 2 | 4 | + * + * address indicates the address of the original record in the data file. + * + * key_size can be used to reduce unnecessary checks. + * + * key_size and value_size can be used to reduce unnecessary disk accesses. + * + * Use linked list to handle collisions. + * next_slot_id indicates the id of the next slot in the linked list, + * whose address = slot_size(11) * next_slot_id. + * + * Indexing: + * hash() : key -> hashCode + * hashCode: h bits + * First (h - f) bits are used for in-file index. + * Last f bits are used for file index, i.e. there are 2^f index files. + * + * Example: + * h = 24, f = 8 + * fileIdMask: 0x000000ff + * infileIndexMask: 0x00ffff00 + */ public class KVIndex { - /** - * hash() : key -> hashCode - * hashCode: h bits - * First (h - f) bits are used for in-file index. - * Last f bits are used for file index, i.e. there are 2^f index files. - *

- * Example: - * h = 24, f = 8 - * fileIdMask: 0x000000ff - * infileIndexMask: 0x00ffff00 - */ - final int f = 8; - long fileIdMask; - long infileIndexMask; - - /** - * size of each slot in index files - * next_index_id use 4 bytes, since h - f = 40 - 8 = 32 bits - * i.e. there should be at most 2^31 -1 indexes in the file - * key_size | key_pos | next_index_id - * 2 | 5 | 4 - */ + final int f = 8; // # of bits used for file id + long fileIdMask; // bitwise mask for file id + long infileIndexMask; // bitwise mask for in-file index + // constants used to specify the format of index slots private static final int addrLength = 5; private static final int infilePointerLength = 4; static int slotSize = Record.keySizeLength + addrLength @@ -43,20 +64,27 @@ public class KVIndex { // original data file RandomAccessFile dataFile; - long N; // number of key-value pairs - - long collisionCount = 0; + // number of key-value pairs + long N; + // hash function HashFunc hasher; - public static void main(String[] args) { - System.out.println("Hello PingCAP"); - } - KVIndex() { System.out.println("Hello PingCAP"); } + /** + * Creates index to get ready for queries. + * + * @param filename + * The filename of data. + * + * @throws IOException + * If I/O errors occur. + * @throws InvalidDataFormatException + * If the data file has invalid format. + */ public void initialize(String filename) throws IOException, InvalidDataFormatException { N = countEntry(filename); @@ -68,6 +96,17 @@ public void initialize(String filename) dataFile = new RandomAccessFile(filename, "r"); } + /** + * Thread-safe query function that returns the value corresponding to the given key. + * + * @param key + * Key of the query. + * + * @return The value. + * + * @throws UninitializedException + * If the KVIndex object has not been initialized. + */ synchronized public byte[] get(byte[] key) throws UninitializedException { if (hasher == null) throw new UninitializedException("KVIndex has not been initialized"); @@ -85,7 +124,8 @@ synchronized public byte[] get(byte[] key) throws UninitializedException { while (true) { if (slotSize * infileIndex < 0) { - Log.loge("seek offset < 0"); + Log.logi("seek offset < 0"); + return null; } indexFile.seek(slotSize * infileIndex); indexFile.read(slotArr); @@ -112,7 +152,6 @@ synchronized public byte[] get(byte[] key) throws UninitializedException { // find the key-value // retrieve and return value short valueSize = buf.getShort(Record.keySizeLength + addrLength); - Log.logd("[get] value size = " + valueSize); byte[] value = new byte[valueSize]; dataFile.seek(address + Record.keySizeLength + keySize + Record.valueSizeLength); @@ -126,8 +165,9 @@ synchronized public byte[] get(byte[] key) throws UninitializedException { infileIndex = buf.getInt(Record.keySizeLength + addrLength + Record.valueSizeLength); - if (infileIndex < 0) - Log.loge("infileIndex < 0"); + if (infileIndex <= 0) { + return null; + } } } } catch (IOException e) { @@ -136,6 +176,19 @@ synchronized public byte[] get(byte[] key) throws UninitializedException { } } + /** + * Counts the total number of records. + * + * @param filename + * The filename of data. + * + * @return The number of records in the data + * + * @throws IOException + * If I/O errors occur + * @throws InvalidDataFormatException + * If the data file has invalid format. + */ long countEntry(String filename) throws IOException, InvalidDataFormatException { long rt = 0; RecordReader reader = new RecordReader(filename); @@ -147,7 +200,7 @@ long countEntry(String filename) throws IOException, InvalidDataFormatException } /** - * Create empty index files. + * Creates empty index files. */ void createIndexFile() { try { @@ -174,9 +227,19 @@ void createIndexFile() { } } + /** + * Creates index for every record. + * + * @param filename The filename of data + */ private void createIndex(String filename) { try { + Log.logi("Begin creating index."); + long startTime = System.currentTimeMillis(); RecordReader reader = new RecordReader(filename); + + // open data file to check replicated key + while (reader.hasNextRecord()) { // get a record Record record = reader.getNextRecord(true); @@ -201,15 +264,14 @@ private void createIndex(String filename) { // hash collision, need to add new slot // temporarily store the address of next slot // skip the key_position and value_size field - collisionCount++; - indexFile.skipBytes(addrLength + Record.valueSizeLength); byte[] nextPos = new byte[infilePointerLength]; indexFile.read(nextPos); // set the pointer to the next slot to the end, where new record is written indexFile.seek(slotSize * infileIndex - + Record.keySizeLength + addrLength + Record.valueSizeLength); + + Record.keySizeLength + addrLength + + Record.valueSizeLength); indexFile.writeInt((int) (indexFile.length() / slotSize)); // append the file @@ -218,25 +280,50 @@ private void createIndex(String filename) { } indexFile.close(); } + Log.logi("Index created, used " + (System.currentTimeMillis() - startTime) + "ms."); } catch (IOException | InvalidDataFormatException e) { Log.loge("Failed to create index: " + e.getMessage()); e.printStackTrace(); } } + /** + * Calculates the masks for file id and infile index. + */ void calculateMask() { fileIdMask = (1 << f) - 1; infileIndexMask = (hasher.capacity - 1) ^ fileIdMask; } + /** + * Returns the index file name + * + * @param fileId + * The id of the index file + * + * @return The filename + */ private String getIndexFilePath(int fileId) { return indexPath + File.separator + indexFilenamePrefix + fileId + indexFilenamePostfix; } - private void writeSlot(RandomAccessFile indexFile, Record record, byte[] nextPos) + /** + * Writes a record to the current position of indexFile. + * + * @param indexFile + * File with position + * @param record + * The record to be written + * @param nextSlotId + * The next slot id in the linked list + * + * @throws IOException + * If I/O errors occur. + */ + private void writeSlot(RandomAccessFile indexFile, Record record, byte[] nextSlotId) throws IOException { - if (nextPos.length != infilePointerLength) - throw new IllegalArgumentException("Length of nextPos must be 4"); + if (nextSlotId.length != infilePointerLength) + throw new IllegalArgumentException("Length of nextSlotId must be 4"); Log.logd("--------writeslot--------"); Log.logd("key = " + Arrays.toString(record.key)); @@ -259,11 +346,14 @@ private void writeSlot(RandomAccessFile indexFile, Record record, byte[] nextPos indexFile.writeShort(record.valueSize); // position of next slot if there is hash collision - indexFile.write(nextPos); + indexFile.write(nextSlotId); Log.logd("-------\\writeslot--------"); } } +/** + * The exception class for queries before initialization. + */ class UninitializedException extends Exception { UninitializedException() { super(); diff --git a/src/main/java/Log.java b/src/main/java/Log.java index 2fee214..f74a106 100644 --- a/src/main/java/Log.java +++ b/src/main/java/Log.java @@ -1,3 +1,6 @@ +/** + * A utility class for logging + */ public class Log { private static final boolean useDebug = false; private static final boolean useWarning = true; diff --git a/src/main/java/Record.java b/src/main/java/Record.java index 340e388..757bd57 100644 --- a/src/main/java/Record.java +++ b/src/main/java/Record.java @@ -1,5 +1,10 @@ +/** + * The record class that stores info of a record and some metadata about records. + * + * By default, assume key size and value size <= 4KB + */ public class Record { - // key size and value size <= 4KB + static int keySizeLength = 2; // length of the field key_size, by default 2 bytes static int valueSizeLength = 2; // length of the field value_size, by default 2 bytes diff --git a/src/main/java/RecordReader.java b/src/main/java/RecordReader.java index 89832dd..adb5082 100644 --- a/src/main/java/RecordReader.java +++ b/src/main/java/RecordReader.java @@ -6,27 +6,43 @@ import java.nio.ByteOrder; import java.util.Arrays; -class InvalidDataFormatException extends Exception { - InvalidDataFormatException() { - super(); - } - - InvalidDataFormatException(String message) { - super(message); - } -} - +/** + * A Reader that reads the data file, with record format (key_size, key, value_size, value). + * Each getNextRecord() call returns a record with {key_size, key, value_size, value, address}. + */ class RecordReader { private FileInputStream inputStream; private boolean closed = false; long pos = 0; + /** + * Constructs the input stream which reads the input file. + * + * @param filename + * The filename of the data. + * + * @throws FileNotFoundException + * If data file is not found. + */ RecordReader(String filename) throws FileNotFoundException { inputStream = new FileInputStream(filename); } + /** + * Constructs the input stream and specify length of key_size and value_size fields. + * + * @param filename + * The filename of the data. + * @param keySizeLength + * The length of the field key_size. + * @param valueSizeLength + * The length of the field value_size. + * + * @throws FileNotFoundException + * If data file is not found. + */ RecordReader(String filename, int keySizeLength, int valueSizeLength) throws FileNotFoundException { this(filename); @@ -34,12 +50,35 @@ class RecordReader { Record.valueSizeLength = valueSizeLength; } + /** + * Checks if the reader can read next record, or has reached the end of file. + * + * @return Whether there is another record that can be read. + * + * @throws IOException + * If this file input stream has been closed by calling or I/O error occurs. + */ boolean hasNextRecord() throws IOException { if (closed) return false; return inputStream.available() > 0; } + /** + * Reads the next record and returns it. + * + * @param needData + * Whether need to read key and value + * + * @return The read record + * + * @throws IOException + * If I/O errors occur + * @throws InvalidDataFormatException + * If the data format is invalid, e.g. invalid key_size + * @throws BufferUnderflowException + * If there are fewer bytes than required to get a number from a byte array + */ Record getNextRecord(boolean needData) throws IOException, InvalidDataFormatException, BufferUnderflowException { Record record = new Record(); @@ -93,11 +132,22 @@ Record getNextRecord(boolean needData) Log.logd("addr = " + record.address); Log.logd("------\\reader------"); - return record; } - + /** + * Read an array of bytes from inputStream + * + * @param inputStream + * The file input stream + * @param arr + * The destination array + * + * @throws InvalidDataFormatException + * If read fewer bytes than required + * @throws IOException + * If I/O errors occur + */ private void readArray(FileInputStream inputStream, byte[] arr) throws InvalidDataFormatException, IOException { int read = inputStream.read(arr); @@ -106,7 +156,31 @@ private void readArray(FileInputStream inputStream, byte[] arr) throw new InvalidDataFormatException("End of file: no enough data to read, read = " + read); } + /** + * close the input stream + * + * @throws IOException + * If I/O errors occur + */ void close() throws IOException { inputStream.close(); } } + +/** + * The exception class for invalid data format. + * + * Examples: + * Less data than required in the data file, + * invalid key_size, + * invalid value_size. + */ +class InvalidDataFormatException extends Exception { + InvalidDataFormatException() { + super(); + } + + InvalidDataFormatException(String message) { + super(message); + } +} \ No newline at end of file diff --git a/src/test/java/ConcurrentTest.java b/src/test/java/ConcurrentTest.java new file mode 100644 index 0000000..8b63c2d --- /dev/null +++ b/src/test/java/ConcurrentTest.java @@ -0,0 +1,110 @@ +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Random; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.atomic.AtomicLong; + +public class ConcurrentTest { + + KVIndex index = new KVIndex(); + + final int queryCount = 20000; + + /** + * A thread used to test the correctness of KVIndex. + */ + class TestThread extends Thread { + + int threadId; // id of the thread + ArrayList keyArr, valueArr; // key and true value list + Random random; // initialized by different seeds + int n; // size of the set of keys + int queryCount; // number of queries + boolean needVerify; // need to verify the result of the query + CountDownLatch barrier; // used to start threads simultaneously + CountDownLatch stopLatch; // count the # of finished threads + AtomicLong totalTime; // used to sum up time consumption of queries + + TestThread(int threadId, int queryCount, ArrayList keyArr, + ArrayList valueArr, int seed, boolean needVerify, + CountDownLatch barrier, CountDownLatch stopLatch, AtomicLong totalTime) { + this.threadId = threadId; + this.keyArr = keyArr; + this.valueArr = valueArr; + this.n = keyArr.size(); + this.queryCount = queryCount; + random = new Random(seed); + this.needVerify = needVerify; + this.barrier = barrier; + this.stopLatch = stopLatch; + this.totalTime = totalTime; + } + + @Override + public void run() { + try { + barrier.await(); + long startTime = System.currentTimeMillis(); + Log.logi("Thread " + threadId + " begins at " + startTime); + for (int i = 0; i < queryCount; i++) { + int k = random.nextInt(n); + byte[] value = index.get(keyArr.get(k)); + if (needVerify && Arrays.compare(value, valueArr.get(k)) != 0) { + Log.loge("Test failed: query(" + Arrays.toString(keyArr.get(k)) + ") gets" + + Arrays.toString(value) + ".\nWhile true value is " + + Arrays.toString(valueArr.get(k))); + } + } + long usedTime = System.currentTimeMillis() - startTime; + totalTime.getAndAdd(usedTime); + Log.logi("Thread " + threadId + " test succeeded, used " + + usedTime + " ms. avg = " + ((double) usedTime / queryCount + "ms.")); + } catch (Exception e) { + e.printStackTrace(); + } finally { + stopLatch.countDown(); + } + } + } + + void testConcurrentCorrectness(boolean benchmark) { + try { + int[] Ns = {10000, 20000, 50000, 100000}; + int[] threadCounts = {1, 2, 4, 8}; + for (int N : Ns) { + Log.logi("N = " + N); + KVIndexTest.makeData(N); + index.initialize(KVIndexTest.filename); + for (int threadCount : threadCounts) { + Log.logi("N = " + N + ", threadCount = " + threadCount); + CountDownLatch barrier = new CountDownLatch(1); + CountDownLatch stopLatch = new CountDownLatch(threadCount); + Random random = new Random(System.currentTimeMillis()); + AtomicLong totalTime = new AtomicLong(); + + for (int i = 0; i < threadCount; i++) { + new TestThread(i, queryCount, KVIndexTest.keys, KVIndexTest.values, + random.nextInt(), !benchmark, barrier, stopLatch, + totalTime).start(); + } + + // start threads + barrier.countDown(); + + // wait for all threads to finish + stopLatch.await(); + + Log.logi("All test threads finished, avg query time = " + + (double) (totalTime.get()) / (threadCount * queryCount)); + } + } + } catch (Exception e) { + e.printStackTrace(); + } + } + + public static void main(String[] agrs) { + ConcurrentTest tester = new ConcurrentTest(); + tester.testConcurrentCorrectness(true); + } +} diff --git a/src/test/java/HashFuncTest.java b/src/test/java/HashFuncTest.java index 819c152..126bd7e 100644 --- a/src/test/java/HashFuncTest.java +++ b/src/test/java/HashFuncTest.java @@ -10,7 +10,6 @@ public class HashFuncTest { void hashFuncCorrectness() { Random random = new Random(System.currentTimeMillis()); int N = random.nextInt(1000000); - N = 12; HashFunc hasher = new HashFunc(N); for (int i = 0; i < N; i++) { byte[] arr = new byte[random.nextInt(4096)]; diff --git a/src/test/java/KVIndexTest.java b/src/test/java/KVIndexTest.java index 9911b86..27372d7 100644 --- a/src/test/java/KVIndexTest.java +++ b/src/test/java/KVIndexTest.java @@ -1,71 +1,93 @@ +import org.apache.commons.lang3.ArrayUtils; +import org.junit.jupiter.api.BeforeAll; import org.junit.jupiter.api.Test; import java.io.File; import java.io.FileOutputStream; -import java.io.IOException; import java.nio.ByteBuffer; -import java.util.ArrayList; -import java.util.Arrays; -import java.util.Random; +import java.util.*; -import static org.junit.jupiter.api.Assertions.assertEquals; -import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.junit.jupiter.api.Assertions.*; public class KVIndexTest { private final KVIndex index = new KVIndex(); - ArrayList keys = new ArrayList(); - ArrayList values = new ArrayList(); + static ArrayList keys = new ArrayList(); + static ArrayList values = new ArrayList(); + static long N; + static String filename = "data" + File.separator + "data"; + static HashSet> set = new HashSet<>(); - private long makeData(String filename, boolean testCorrectness) throws IOException { - // when testing correctness, value = [key key] - - FileOutputStream out = new FileOutputStream(filename); - long seed = System.currentTimeMillis(); - Log.logd("Seed = " + seed); - Random random = new Random(seed); + @BeforeAll + static void makeData() { + Random random = new Random(System.currentTimeMillis()); + int n = random.nextInt(200000) + 1; + makeData(n); + } - // generate the number of k-v pairs - int n = random.nextInt(10000) + 1; + static void makeData(int n) { try { - for (int i = 0; i < n; i++) { - Log.logd("[gen] " + i); - // generate key size and value size - int keySize = random.nextInt(4096) + 1; - int valueSize = random.nextInt(4096) + 1; - - // generate key and value - byte[] key = new byte[keySize]; - random.nextBytes(key); - byte[] value = new byte[valueSize]; - random.nextBytes(value); - if (testCorrectness) { + keys.clear(); + values.clear(); + + Log.logi("Begin generating data."); + long startTime = System.currentTimeMillis(); + FileOutputStream out = new FileOutputStream(filename); + long seed = System.currentTimeMillis(); + Log.logi("Seed = " + seed); + Random random = new Random(seed); + + // generate the number of k-v pairs + try { + for (int i = 0; i < n; i++) { + Log.logd("[gen] " + i); + // generate key size and value size + int keySize = random.nextInt(4096) + 1; + int valueSize = random.nextInt(4096) + 1; + + // for benchmark + keySize = 4096; + valueSize = 4096; + + // generate key and value + byte[] key = new byte[keySize]; + Byte[] bkey = new Byte[keySize]; + do { + random.nextBytes(key); + bkey = ArrayUtils.toObject(key); + } while (set.contains(Arrays.asList(bkey))); + byte[] value = new byte[valueSize]; + random.nextBytes(value); + set.add(Arrays.asList(bkey)); keys.add(key); values.add(value); + Log.logd("key = " + Arrays.toString(key)); + Log.logd("value = " + Arrays.toString(value)); + + // write to file + out.write(ByteBuffer.allocate(2).putShort((short) key.length).array()); + out.write(key); + out.write(ByteBuffer.allocate(2).putShort((short) value.length).array()); + out.write(value); } - Log.logd("key = " + Arrays.toString(key)); - Log.logd("value = " + Arrays.toString(value)); - - // write to file - out.write(ByteBuffer.allocate(2).putShort((short) key.length).array()); - out.write(key); - out.write(ByteBuffer.allocate(2).putShort((short) value.length).array()); - out.write(value); + Log.logi("Data generated, used " + + (System.currentTimeMillis() - startTime) + " " + "ms."); + } finally { + out.close(); } - } finally { - out.close(); + N = n; + } catch (Exception e) { + e.printStackTrace(); } - return n; } @Test void testCountEntry() { String filename = "data" + File.separator + "data"; try { - long n = makeData(filename, false); long count = index.countEntry(filename); - assertEquals(count, n); + assertEquals(count, N); } catch (Exception e) { System.out.println(e.getMessage()); e.printStackTrace(); @@ -88,8 +110,6 @@ void testCreateIndexFile() { @Test void testInitialization() { try { - String filename = "data" + File.separator + "data"; - makeData(filename, false); index.initialize(filename); } catch (Exception e) { e.printStackTrace(); @@ -100,17 +120,28 @@ void testInitialization() { void testCorrectness() { try { // initialization - String filename = "data" + File.separator + "data"; - makeData(filename, true); index.initialize(filename); // construct queries for (int i = 0; i < keys.size(); i++) { byte[] value = index.get(keys.get(i)); - Log.logd("value = " + Arrays.toString(value)); - Log.logd("true value = " + Arrays.toString(values.get(i))); + if (Arrays.compare(value, values.get(i)) != 0) { + Log.logd("value = " + Arrays.toString(value)); + Log.logd("true value = " + Arrays.toString(values.get(i))); + } assertEquals(Arrays.compare(value, values.get(i)), 0); } + + // invalid queries + Random random = new Random(System.currentTimeMillis()); + for (int i = 0; i < 10; ) { + byte[] nkey = new byte[Record.MAX_KEY_SIZE]; + for (byte[] key : keys) { + if (Arrays.compare(key, nkey) == 0) continue; + } + i++; + assertNull(index.get(nkey)); + } } catch (Exception e) { e.printStackTrace(); }