PersistentMap can store data in multiple nodes
This commit is contained in:
@@ -190,4 +190,10 @@ public class VariableByteEncoder {
|
|||||||
}
|
}
|
||||||
return offset - offsetInBuffer;
|
return offset - offsetInBuffer;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public static int neededBytes(final long value) {
|
||||||
|
final byte[] buffer = SINGLE_VALUE_BUFFER.get();
|
||||||
|
final int usedBytes = encodeInto(value, buffer, 0);
|
||||||
|
return usedBytes;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -60,8 +60,11 @@ class NodeEntry {
|
|||||||
|
|
||||||
@Override
|
@Override
|
||||||
public String toString() {
|
public String toString() {
|
||||||
|
final String valueAsString = isInnerNode() ? String.valueOf(VariableByteEncoder.decodeFirstValue(value))
|
||||||
|
: new String(value, StandardCharsets.UTF_8);
|
||||||
|
|
||||||
return "NodeEntry [type=" + type + ", key=" + new String(key, StandardCharsets.UTF_8) + ", value="
|
return "NodeEntry [type=" + type + ", key=" + new String(key, StandardCharsets.UTF_8) + ", value="
|
||||||
+ new String(value, StandardCharsets.UTF_8) + "]";
|
+ valueAsString + "]";
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
@@ -92,10 +95,6 @@ class NodeEntry {
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
public static int neededBytes(final List<NodeEntry> entries) {
|
|
||||||
return entries.stream().mapToInt(NodeEntry::size).sum();
|
|
||||||
}
|
|
||||||
|
|
||||||
public static List<NodeEntry> deserialize(final byte[] buffer) {
|
public static List<NodeEntry> deserialize(final byte[] buffer) {
|
||||||
final List<NodeEntry> entries = new ArrayList<>();
|
final List<NodeEntry> entries = new ArrayList<>();
|
||||||
final LongList keyLengths = VariableByteEncoder.decode(buffer);
|
final LongList keyLengths = VariableByteEncoder.decode(buffer);
|
||||||
@@ -128,22 +127,39 @@ class NodeEntry {
|
|||||||
return entries;
|
return entries;
|
||||||
}
|
}
|
||||||
|
|
||||||
public static byte[] serialize(final List<NodeEntry> entries) {
|
public static int neededBytes(final List<NodeEntry> entries) {
|
||||||
final var keyLengths = new LongList();
|
return entries.stream().mapToInt(NodeEntry::size).sum();
|
||||||
|
}
|
||||||
|
|
||||||
|
public static int neededBytesTotal(final List<NodeEntry> entries) {
|
||||||
|
final byte[] buffer = new byte[PersistentMap.BLOCK_SIZE];
|
||||||
|
|
||||||
|
final int usedBytes = serializeKeyLengths(entries, buffer);
|
||||||
|
|
||||||
|
return usedBytes + NodeEntry.neededBytes(entries);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static byte[] serialize(final List<NodeEntry> entries) {
|
||||||
|
final byte[] buffer = new byte[PersistentMap.BLOCK_SIZE];
|
||||||
|
|
||||||
|
final int usedBytes = serializeKeyLengths(entries, buffer);
|
||||||
|
|
||||||
|
Preconditions.checkGreater(PersistentMap.BLOCK_SIZE, usedBytes + NodeEntry.neededBytes(entries), "");
|
||||||
|
|
||||||
|
NodeEntry.serializeIntoFromTail(entries, buffer);
|
||||||
|
return buffer;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static int serializeKeyLengths(final List<NodeEntry> entries, final byte[] buffer) {
|
||||||
|
final var keyLengths = new LongList();
|
||||||
keyLengths.add(entries.size());
|
keyLengths.add(entries.size());
|
||||||
for (final NodeEntry nodeEntry : entries) {
|
for (final NodeEntry nodeEntry : entries) {
|
||||||
keyLengths.add(nodeEntry.getKey().length);
|
keyLengths.add(nodeEntry.getKey().length);
|
||||||
keyLengths.add(nodeEntry.getValue().length);
|
keyLengths.add(nodeEntry.getValue().length);
|
||||||
}
|
}
|
||||||
|
|
||||||
final byte[] buffer = new byte[PersistentMap.BLOCK_SIZE];
|
|
||||||
final int usedBytes = VariableByteEncoder.encodeInto(keyLengths, buffer, 0);
|
final int usedBytes = VariableByteEncoder.encodeInto(keyLengths, buffer, 0);
|
||||||
|
return usedBytes;
|
||||||
Preconditions.checkGreater(PersistentMap.BLOCK_SIZE, usedBytes + NodeEntry.neededBytes(entries), "");
|
|
||||||
|
|
||||||
NodeEntry.serializeIntoFromTail(entries, buffer);
|
|
||||||
return buffer;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private static void serializeIntoFromTail(final List<NodeEntry> entries, final byte[] buffer) {
|
private static void serializeIntoFromTail(final List<NodeEntry> entries, final byte[] buffer) {
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ package org.lucares.pdb.map;
|
|||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
import java.nio.charset.Charset;
|
import java.nio.charset.Charset;
|
||||||
import java.nio.charset.StandardCharsets;
|
import java.nio.charset.StandardCharsets;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
import org.lucares.pdb.blockstorage.intsequence.VariableByteEncoder;
|
import org.lucares.pdb.blockstorage.intsequence.VariableByteEncoder;
|
||||||
import org.lucares.pdb.diskstorage.DiskBlock;
|
import org.lucares.pdb.diskstorage.DiskBlock;
|
||||||
@@ -11,6 +12,10 @@ import org.lucares.utils.Preconditions;
|
|||||||
|
|
||||||
public class PersistentMap {
|
public class PersistentMap {
|
||||||
|
|
||||||
|
interface VisitorCallback {
|
||||||
|
void visit(NodeEntry nodeEntry, int depth);
|
||||||
|
}
|
||||||
|
|
||||||
private static final Charset UTF8 = StandardCharsets.UTF_8;
|
private static final Charset UTF8 = StandardCharsets.UTF_8;
|
||||||
static final int BLOCK_SIZE = 4096;
|
static final int BLOCK_SIZE = 4096;
|
||||||
private static final int ROOT_NODE_OFFEST = 4096;
|
private static final int ROOT_NODE_OFFEST = 4096;
|
||||||
@@ -70,18 +75,33 @@ public class PersistentMap {
|
|||||||
final PersistentMapDiskNode node = getNode(nodeOffest);
|
final PersistentMapDiskNode node = getNode(nodeOffest);
|
||||||
|
|
||||||
final var entry = node.getNodeEntryTo(key);
|
final var entry = node.getNodeEntryTo(key);
|
||||||
|
if (entry == null || entry.isDataNode()) {
|
||||||
|
|
||||||
|
final byte[] oldValue;
|
||||||
if (entry == null) {
|
if (entry == null) {
|
||||||
node.addKeyValue(key, value);
|
oldValue = null;
|
||||||
writeNode(nodeOffest, node);
|
|
||||||
return null;
|
|
||||||
} else if (entry.isDataNode()) {
|
|
||||||
if (entry.equal(key)) {
|
|
||||||
return entry.getValue();
|
|
||||||
} else {
|
} else {
|
||||||
|
final boolean entryIsForKey = entry.equal(key);
|
||||||
|
|
||||||
|
oldValue = entryIsForKey ? entry.getValue() : null;
|
||||||
|
|
||||||
|
if (entryIsForKey) {
|
||||||
node.removeKey(key);
|
node.removeKey(key);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (node.canAdd(key, value)) {
|
||||||
|
// insert in existing node
|
||||||
node.addKeyValue(key, value);
|
node.addKeyValue(key, value);
|
||||||
writeNode(nodeOffest, node);
|
writeNode(nodeOffest, node);
|
||||||
return null;
|
return oldValue;
|
||||||
|
} else {
|
||||||
|
// add new node
|
||||||
|
// 1. split current node into A and B
|
||||||
|
splitNode(nodeOffest, node);
|
||||||
|
|
||||||
|
// 2. insert the value
|
||||||
|
return insert(nodeOffest, key, value);
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
final long childNodeOffset = toNodeOffset(entry);
|
final long childNodeOffset = toNodeOffset(entry);
|
||||||
@@ -89,6 +109,31 @@ public class PersistentMap {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private void splitNode(final long nodeOffest, final PersistentMapDiskNode node) throws IOException {
|
||||||
|
|
||||||
|
final long newLeftBlockOffset = diskStore.allocateBlock(BLOCK_SIZE);
|
||||||
|
final long newRightBlockOffset = diskStore.allocateBlock(BLOCK_SIZE);
|
||||||
|
|
||||||
|
final List<NodeEntry> entries = node.getEntries();
|
||||||
|
final var leftEntries = entries.subList(0, entries.size() / 2);
|
||||||
|
final var rightEntries = entries.subList(entries.size() / 2, entries.size());
|
||||||
|
|
||||||
|
final PersistentMapDiskNode leftNode = new PersistentMapDiskNode(leftEntries);
|
||||||
|
final PersistentMapDiskNode rightNode = new PersistentMapDiskNode(rightEntries);
|
||||||
|
|
||||||
|
node.clear();
|
||||||
|
|
||||||
|
final NodeEntry lastLeftEntry = leftEntries.get(leftEntries.size() - 1);
|
||||||
|
final NodeEntry lastRightEntry = rightEntries.get(rightEntries.size() - 1);
|
||||||
|
|
||||||
|
node.addKeyNodePointer(lastLeftEntry.getKey(), newLeftBlockOffset);
|
||||||
|
node.addKeyNodePointer(lastRightEntry.getKey(), newRightBlockOffset);
|
||||||
|
|
||||||
|
writeNode(newLeftBlockOffset, leftNode);
|
||||||
|
writeNode(newRightBlockOffset, rightNode);
|
||||||
|
writeNode(nodeOffest, node);
|
||||||
|
}
|
||||||
|
|
||||||
private NodeEntry findNodeEntry(final long nodeOffest, final byte[] key) throws IOException {
|
private NodeEntry findNodeEntry(final long nodeOffest, final byte[] key) throws IOException {
|
||||||
final PersistentMapDiskNode node = getNode(nodeOffest);
|
final PersistentMapDiskNode node = getNode(nodeOffest);
|
||||||
|
|
||||||
@@ -127,4 +172,28 @@ public class PersistentMap {
|
|||||||
diskBlock.writeAsync();
|
diskBlock.writeAsync();
|
||||||
diskBlock.force();
|
diskBlock.force();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public void print() throws IOException {
|
||||||
|
|
||||||
|
visitPreOrder((nodeEntry, depth) -> System.out.println(" ".repeat(depth) + nodeEntry));
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
public void visitPreOrder(final VisitorCallback visitor) throws IOException {
|
||||||
|
visitPreOrderRecursively(ROOT_NODE_OFFEST, visitor, 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void visitPreOrderRecursively(final long nodeOffset, final VisitorCallback visitor, final int depth)
|
||||||
|
throws IOException {
|
||||||
|
final PersistentMapDiskNode node = getNode(nodeOffset);
|
||||||
|
|
||||||
|
for (final NodeEntry child : node.getEntries()) {
|
||||||
|
|
||||||
|
visitor.visit(child, depth);
|
||||||
|
if (child.isInnerNode()) {
|
||||||
|
final long childNodeOffset = VariableByteEncoder.decodeFirstValue(child.getValue());
|
||||||
|
visitPreOrderRecursively(childNodeOffset, visitor, depth + 1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,8 +1,11 @@
|
|||||||
package org.lucares.pdb.map;
|
package org.lucares.pdb.map;
|
||||||
|
|
||||||
|
import java.util.ArrayList;
|
||||||
import java.util.Collections;
|
import java.util.Collections;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
import java.util.stream.Collectors;
|
||||||
|
|
||||||
|
import org.lucares.pdb.blockstorage.intsequence.VariableByteEncoder;
|
||||||
import org.lucares.pdb.map.NodeEntry.ValueType;
|
import org.lucares.pdb.map.NodeEntry.ValueType;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -38,25 +41,56 @@ public class PersistentMapDiskNode {
|
|||||||
|
|
||||||
public NodeEntry getNodeEntryTo(final byte[] key) {
|
public NodeEntry getNodeEntryTo(final byte[] key) {
|
||||||
|
|
||||||
NodeEntry result = null;
|
final NodeEntry result = null;
|
||||||
for (final NodeEntry entry : entries) {
|
for (final NodeEntry entry : entries) {
|
||||||
if (entry.compare(key) <= 0) {
|
// if (entry.compare(key) <= 0) {
|
||||||
result = entry;
|
if (entry.compare(key) >= 0) {
|
||||||
|
return entry;
|
||||||
} else {
|
} else {
|
||||||
break;
|
// break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
public void addKeyValue(final byte[] key, final byte[] value) {
|
public void addKeyValue(final byte[] key, final byte[] value) {
|
||||||
final NodeEntry entry = new NodeEntry(ValueType.VALUE_INLINE, key, value);
|
addNode(ValueType.VALUE_INLINE, key, value);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void addKeyNodePointer(final byte[] key, final long nodePointer) {
|
||||||
|
final byte[] value = VariableByteEncoder.encode(nodePointer);
|
||||||
|
addNode(ValueType.NODE_POINTER, key, value);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void addNode(final ValueType valueType, final byte[] key, final byte[] value) {
|
||||||
|
final NodeEntry entry = new NodeEntry(valueType, key, value);
|
||||||
entries.add(entry);
|
entries.add(entry);
|
||||||
Collections.sort(entries, NodeEntry.SORT_BY_KEY);
|
Collections.sort(entries, NodeEntry.SORT_BY_KEY);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public boolean canAdd(final byte[] key, final byte[] value) {
|
||||||
|
final NodeEntry entry = new NodeEntry(ValueType.VALUE_INLINE, key, value);
|
||||||
|
final List<NodeEntry> tmp = new ArrayList<>(entries.size() + 1);
|
||||||
|
tmp.addAll(entries);
|
||||||
|
tmp.add(entry);
|
||||||
|
return NodeEntry.neededBytesTotal(tmp) <= PersistentMap.BLOCK_SIZE;
|
||||||
|
}
|
||||||
|
|
||||||
public void removeKey(final byte[] key) {
|
public void removeKey(final byte[] key) {
|
||||||
entries.removeIf(entry -> entry.compare(key) == 0);
|
entries.removeIf(entry -> entry.compare(key) == 0);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public List<NodeEntry> getEntries() {
|
||||||
|
return new ArrayList<>(entries);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void clear() {
|
||||||
|
entries.clear();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String toString() {
|
||||||
|
return String.join("\n", entries.stream().map(NodeEntry::toString).collect(Collectors.toList()));
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -81,4 +81,29 @@ public class VariableByteEncoderTest {
|
|||||||
final LongList decodedValues = VariableByteEncoder.decode(buffer);
|
final LongList decodedValues = VariableByteEncoder.decode(buffer);
|
||||||
Assert.assertEquals(decodedValues, originalValues);
|
Assert.assertEquals(decodedValues, originalValues);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@DataProvider
|
||||||
|
public Object[][] providerNededBytes() {
|
||||||
|
return new Object[][] { //
|
||||||
|
{ 0, 1 }, //
|
||||||
|
{ -10, 1 }, //
|
||||||
|
{ 10, 1 }, //
|
||||||
|
{ -63, 1 }, //
|
||||||
|
{ 63, 1 }, //
|
||||||
|
{ -64, 2 }, //
|
||||||
|
{ 64, 2 }, //
|
||||||
|
{ -8191, 2 }, //
|
||||||
|
{ 8191, 2 }, //
|
||||||
|
{ -8192, 3 }, //
|
||||||
|
{ 8192, 3 }, //
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test(dataProvider = "providerNededBytes")
|
||||||
|
public void testNeededBytes(final long value, final int expectedNeededBytes) {
|
||||||
|
|
||||||
|
final int neededBytes = VariableByteEncoder.neededBytes(value);
|
||||||
|
final byte[] encoded = VariableByteEncoder.encode(value);
|
||||||
|
Assert.assertEquals(encoded.length, neededBytes);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,6 +3,8 @@ package org.lucares.pdb.map;
|
|||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
import java.nio.file.Files;
|
import java.nio.file.Files;
|
||||||
import java.nio.file.Path;
|
import java.nio.file.Path;
|
||||||
|
import java.util.HashMap;
|
||||||
|
import java.util.UUID;
|
||||||
|
|
||||||
import org.lucares.pdb.diskstorage.DiskStorage;
|
import org.lucares.pdb.diskstorage.DiskStorage;
|
||||||
import org.lucares.utils.file.FileUtils;
|
import org.lucares.utils.file.FileUtils;
|
||||||
@@ -38,9 +40,51 @@ public class PersistentMapTest {
|
|||||||
Assert.assertNull(map.getAsString(key));
|
Assert.assertNull(map.getAsString(key));
|
||||||
|
|
||||||
Assert.assertNull(map.put(key, value));
|
Assert.assertNull(map.put(key, value));
|
||||||
final String actualValue = map.getAsString(key);
|
|
||||||
|
|
||||||
Assert.assertEquals(actualValue, value);
|
Assert.assertEquals(map.getAsString(key), value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public void testManyValues() throws Exception {
|
||||||
|
final Path file = dataDirectory.resolve("map.db");
|
||||||
|
final var insertedValues = new HashMap<String, String>();
|
||||||
|
|
||||||
|
try (final DiskStorage ds = new DiskStorage(file)) {
|
||||||
|
final PersistentMap map = new PersistentMap(ds);
|
||||||
|
|
||||||
|
for (int i = 0; i < 100; i++) {
|
||||||
|
|
||||||
|
final String key = UUID.randomUUID().toString() + "__" + i;
|
||||||
|
final String value = "long value to waste some bytes " + i;
|
||||||
|
Assert.assertNull(map.getAsString(key));
|
||||||
|
|
||||||
|
Assert.assertNull(map.put(key, value));
|
||||||
|
|
||||||
|
insertedValues.put(key, value);
|
||||||
|
|
||||||
|
for (final var entry : insertedValues.entrySet()) {
|
||||||
|
final String actualValue = map.getAsString(entry.getKey());
|
||||||
|
Assert.assertEquals(actualValue, entry.getValue(),
|
||||||
|
"value for key " + entry.getKey() + " in the " + i + "th iteration");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
try (final DiskStorage ds = new DiskStorage(file)) {
|
||||||
|
final PersistentMap map = new PersistentMap(ds);
|
||||||
|
map.visitPreOrder((nodeEntry, depth) -> {
|
||||||
|
if (nodeEntry.isInnerNode()) {
|
||||||
|
System.out.println(" ".repeat(depth) + nodeEntry);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
for (final var entry : insertedValues.entrySet()) {
|
||||||
|
final String actualValue = map.getAsString(entry.getKey());
|
||||||
|
Assert.assertEquals(actualValue, entry.getValue(),
|
||||||
|
"value for key " + entry.getKey() + " after all iterations");
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user