proper tree merging beginnings

This commit is contained in:
2024-08-03 14:46:50 +02:00
parent 582ca87dd2
commit 824a33ae31
22 changed files with 581 additions and 0 deletions

View File

@@ -0,0 +1,35 @@
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>com.usatiuk.dhfs</groupId>
<artifactId>parent</artifactId>
<version>1.0-SNAPSHOT</version>
</parent>
<groupId>com.usatiuk</groupId>
<artifactId>kleppmanntree</artifactId>
<properties>
<maven.compiler.source>21</maven.compiler.source>
<maven.compiler.target>21</maven.compiler.target>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
</properties>
<dependencies>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>1.18.32</version>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>org.junit.jupiter</groupId>
<artifactId>junit-jupiter-engine</artifactId>
<version>5.9.1</version>
<scope>test</scope>
</dependency>
</dependencies>
</project>

View File

@@ -0,0 +1 @@
lombok.accessors.prefix += _

View File

@@ -0,0 +1,7 @@
package com.usatiuk.kleppmanntree;
public interface Clock<TimestampT extends Comparable<TimestampT>> {
TimestampT getTimestamp();
void updateTimestamp(TimestampT receivedTimestamp);
}

View File

@@ -0,0 +1,15 @@
package com.usatiuk.kleppmanntree;
import java.util.Comparator;
public record CombinedTimestamp<TimestampT extends Comparable<TimestampT>, PeerIdT extends Comparable<PeerIdT>>
(TimestampT timestamp, PeerIdT nodeId) implements Comparable<CombinedTimestamp<TimestampT, PeerIdT>> {
@Override
public int compareTo(CombinedTimestamp<TimestampT, PeerIdT> o) {
return Comparator.comparing((CombinedTimestamp<TimestampT, PeerIdT> t) -> t.timestamp)
.thenComparing((CombinedTimestamp<TimestampT, PeerIdT> t) -> t.nodeId)
.compare(this, o);
}
}

View File

@@ -0,0 +1,173 @@
package com.usatiuk.kleppmanntree;
import java.util.List;
import java.util.Map;
import java.util.Objects;
public class KleppmannTree<TimestampT extends Comparable<TimestampT>, PeerIdT extends Comparable<PeerIdT>, NameT, MetaT extends NodeMeta<NameT>, NodeIdT, WrapperT extends TreeNodeWrapper<NameT, MetaT, NodeIdT>> {
private final StorageInterface<TimestampT, PeerIdT, NameT, MetaT, NodeIdT, WrapperT> _storage;
private final PeerInterface<PeerIdT> _peers;
private final Clock<TimestampT> _clock;
public KleppmannTree(StorageInterface<TimestampT, PeerIdT, NameT, MetaT, NodeIdT, WrapperT> storage,
PeerInterface<PeerIdT> peers,
Clock<TimestampT> clock) {
_storage = storage;
_peers = peers;
_clock = clock;
}
public NodeIdT traverse(NodeIdT fromId, List<NameT> names) {
if (names.isEmpty()) return fromId;
var from = _storage.getById(fromId);
from.rLock();
NodeIdT childId;
try {
childId = from.getNode().getChildren().get(names.getFirst());
} finally {
from.rwUnlock();
}
if (childId == null)
return null;
return traverse(childId, names.subList(1, names.size()));
}
public NodeIdT traverse(List<NameT> names) {
return traverse(_storage.getRootId(), names);
}
private void undoOp(LogOpMove<TimestampT, PeerIdT, NameT, MetaT, NodeIdT> op) {
if (op.oldInfo() != null) {
var node = _storage.getById(op.op().childId());
var oldParent = _storage.getById(op.oldInfo().oldParent());
var curParent = _storage.getById(op.op().newParentId());
curParent.rwLock();
oldParent.rwLock();
node.rwLock();
try {
curParent.getNode().getChildren().remove(node.getNode().getMeta().getName());
node.getNode().setMeta(op.oldInfo().oldMeta());
node.getNode().setParent(oldParent.getNode().getId());
oldParent.getNode().getChildren().put(node.getNode().getMeta().getName(), node.getNode().getId());
} finally {
node.rwUnlock();
oldParent.rwUnlock();
curParent.rwUnlock();
}
} else {
var node = _storage.getById(op.op().childId());
var curParent = _storage.getById(op.op().newParentId());
curParent.rwLock();
node.rwLock();
try {
curParent.getNode().getChildren().remove(node.getNode().getMeta().getName());
_storage.removeNode(node.getNode().getId());
} finally {
node.rwUnlock();
curParent.rwUnlock();
}
}
}
private void redoOp(Map.Entry<CombinedTimestamp<TimestampT, PeerIdT>, LogOpMove<TimestampT, PeerIdT, NameT, MetaT, NodeIdT>> entry) {
entry.setValue(doOp(entry.getValue().op()));
}
public void applyOp(OpMove<TimestampT, PeerIdT, NameT, MetaT, NodeIdT> op) {
_clock.updateTimestamp(op.timestamp().timestamp());
var log = _storage.getLog();
if (log.isEmpty()) {
log.put(op.timestamp(), doOp(op));
return;
}
var cmp = op.timestamp().compareTo(log.lastEntry().getKey());
assert cmp != 0;
if (cmp < 0) {
_storage.globalLock();
try {
var toUndo = log.tailMap(op.timestamp(), false);
for (var entry : toUndo.reversed().entrySet()) {
undoOp(entry.getValue());
}
log.put(op.timestamp(), doOp(op));
for (var entry : toUndo.entrySet()) {
redoOp(entry);
}
} finally {
_storage.globalUnlock();
}
} else {
log.put(op.timestamp(), doOp(op));
}
}
private LogOpMove<TimestampT, PeerIdT, NameT, MetaT, NodeIdT> doOp(OpMove<TimestampT, PeerIdT, NameT, MetaT, NodeIdT> op) {
var node = _storage.getById(op.childId());
if (node == null) {
node = _storage.createNewNode(op.childId());
}
var oldParent = node.getNode().getParent() != null ? _storage.getById(node.getNode().getParent()) : null;
var newParent = _storage.getById(op.newParentId());
if (newParent == null) {
throw new IllegalArgumentException("New parent not found");
}
if (oldParent == null) {
newParent.rwLock();
try {
node.rwLock();
try {
node.getNode().setMeta(op.newMeta());
node.getNode().setParent(op.newParentId());
newParent.getNode().getChildren().put(node.getNode().getMeta().getName(), node.getNode().getId());
return new LogOpMove<>(null, op);
} finally {
node.rwUnlock();
}
} finally {
newParent.rwUnlock();
}
}
// FIXME:
_storage.globalLock();
try {
if (op.childId() == op.newParentId() || isAncestor(op.childId(), op.newParentId()))
return new LogOpMove<>(null, op);
newParent.rwLock();
oldParent.rwLock();
node.rwLock();
try {
oldParent.getNode().getChildren().remove(node.getNode().getMeta().getName());
node.getNode().setMeta(op.newMeta());
node.getNode().setParent(newParent.getNode().getId());
newParent.getNode().getChildren().put(op.newMeta().getName(), node.getNode().getId());
return new LogOpMove<>(new LogOpMoveOld<>(oldParent.getNode().getId(), node.getNode().getMeta()), op);
} finally {
node.rwUnlock();
oldParent.rwUnlock();
newParent.rwUnlock();
}
} finally {
_storage.globalUnlock();
}
}
private boolean isAncestor(NodeIdT child, NodeIdT parent) {
var node = _storage.getById(parent);
NodeIdT curParent = null;
while ((curParent = node.getNode().getParent()) != null) {
if (Objects.equals(child, curParent)) return true;
node = _storage.getById(curParent);
}
return false;
}
}

View File

@@ -0,0 +1,5 @@
package com.usatiuk.kleppmanntree;
public record LogOpMove<TimestampT extends Comparable<TimestampT>, PeerIdT extends Comparable<PeerIdT>, NameT, MetaT extends NodeMeta<NameT>, NodeIdT>
(LogOpMoveOld<NameT, MetaT, NodeIdT> oldInfo,
OpMove<TimestampT, PeerIdT, NameT, MetaT, NodeIdT> op) {}

View File

@@ -0,0 +1,4 @@
package com.usatiuk.kleppmanntree;
public record LogOpMoveOld<NameT, MetaT extends NodeMeta<NameT>, NodeIdT>
(NodeIdT oldParent, MetaT oldMeta) {}

View File

@@ -0,0 +1,5 @@
package com.usatiuk.kleppmanntree;
public interface NodeMeta<NameT> {
public NameT getName();
}

View File

@@ -0,0 +1,4 @@
package com.usatiuk.kleppmanntree;
public record OpMove<TimestampT extends Comparable<TimestampT>, PeerIdT extends Comparable<PeerIdT>, NameT, MetaT extends NodeMeta<NameT>, NodeIdT>
(CombinedTimestamp<TimestampT, PeerIdT> timestamp, NodeIdT newParentId, MetaT newMeta, NodeIdT childId) {}

View File

@@ -0,0 +1,8 @@
package com.usatiuk.kleppmanntree;
import java.util.Collection;
public interface PeerInterface<PeerIdT extends Comparable<PeerIdT>> {
public PeerIdT getSelfId();
public Collection<PeerIdT> getAllPeers();
}

View File

@@ -0,0 +1,34 @@
package com.usatiuk.kleppmanntree;
import java.util.Collection;
import java.util.NavigableMap;
public interface StorageInterface<
TimestampT extends Comparable<TimestampT>,
PeerIdT extends Comparable<PeerIdT>,
NameT,
MetaT extends NodeMeta<NameT>,
NodeIdT,
WrapperT extends TreeNodeWrapper<NameT, MetaT, NodeIdT>> {
NodeIdT getRootId();
NodeIdT getTrashId();
NodeIdT getNewNodeId();
WrapperT getById(NodeIdT id);
WrapperT createNewNode(NodeIdT id);
void removeNode(NodeIdT id);
void lockSet(Collection<WrapperT> nodes);
// It is expected that the map allows concurrent additions at the end
NavigableMap<CombinedTimestamp<TimestampT, PeerIdT>, LogOpMove<TimestampT, PeerIdT, NameT, MetaT, NodeIdT>> getLog();
// Locks all the objects from being changed
void globalLock();
void globalUnlock();
}

View File

@@ -0,0 +1,18 @@
package com.usatiuk.kleppmanntree;
import lombok.Getter;
import lombok.Setter;
import java.util.HashMap;
import java.util.Map;
@Getter
@Setter
public class TreeNode<NameT, MetaT extends NodeMeta<NameT>, NodeIdT> {
private NodeIdT _parent = null;
private final NodeIdT _id;
private MetaT _meta = null;
private Map<NameT, NodeIdT> _children = new HashMap<>();
public TreeNode(NodeIdT id) {_id = id;}
}

View File

@@ -0,0 +1,13 @@
package com.usatiuk.kleppmanntree;
public interface TreeNodeWrapper<NameT, MetaT extends NodeMeta<NameT>, NodeIdT> {
void rLock();
void rUnlock();
void rwLock();
void rwUnlock();
TreeNode<NameT, MetaT, NodeIdT> getNode();
}

View File

@@ -0,0 +1,67 @@
package com.usatiuk.kleppmanntree;
import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.Test;
import java.util.List;
public class KleppmanTreeSimpleTest {
private final TestNode testNode1 = new TestNode(1);
private final TestNode testNode2 = new TestNode(2);
private final TestNode testNode3 = new TestNode(3);
private final TestNode testNode4 = new TestNode(4);
@Test
void circularTest() {
var d1id = testNode1._storageInterface.getNewNodeId();
var d2id = testNode2._storageInterface.getNewNodeId();
var op1 = new OpMove<>(new CombinedTimestamp<>(testNode1._clock.getTimestamp(), 1L),
testNode1._storageInterface.getRootId(),
new TestNodeMetaDir("Test1"),
d1id);
var op2 = new OpMove<>(new CombinedTimestamp<>(testNode2._clock.getTimestamp(), 2L),
testNode2._storageInterface.getRootId(),
new TestNodeMetaDir("Test2"),
d2id);
testNode1._tree.applyOp(op1);
testNode2._tree.applyOp(op2);
testNode1._tree.applyOp(op2);
testNode2._tree.applyOp(op1);
Assertions.assertEquals(d1id, testNode1._tree.traverse(List.of("Test1")));
Assertions.assertEquals(d2id, testNode1._tree.traverse(List.of("Test2")));
Assertions.assertEquals(d1id, testNode2._tree.traverse(List.of("Test1")));
Assertions.assertEquals(d2id, testNode2._tree.traverse(List.of("Test2")));
Assertions.assertIterableEquals(List.of("Test1", "Test2"), testNode1._storageInterface.getById(testNode2._storageInterface.getRootId()).getNode().getChildren().keySet());
Assertions.assertIterableEquals(List.of("Test1", "Test2"), testNode2._storageInterface.getById(testNode2._storageInterface.getRootId()).getNode().getChildren().keySet());
var cop1 = new OpMove<>(new CombinedTimestamp<>(testNode1._clock.getTimestamp(), 1L),
d1id,
new TestNodeMetaDir("Test2"),
d2id);
testNode1._tree.applyOp(cop1);
Assertions.assertEquals(d1id, testNode1._tree.traverse(List.of("Test1")));
Assertions.assertEquals(d2id, testNode1._tree.traverse(List.of("Test1", "Test2")));
Assertions.assertIterableEquals(List.of("Test1"), testNode1._storageInterface.getById(testNode2._storageInterface.getRootId()).getNode().getChildren().keySet());
var cop2 = new OpMove<>(new CombinedTimestamp<>(testNode2._clock.getTimestamp(), 2L),
d2id,
new TestNodeMetaDir("Test1"),
d1id);
testNode2._tree.applyOp(cop2);
Assertions.assertIterableEquals(List.of("Test2"), testNode2._storageInterface.getById(testNode2._storageInterface.getRootId()).getNode().getChildren().keySet());
Assertions.assertEquals(d2id, testNode2._tree.traverse(List.of("Test2")));
Assertions.assertEquals(d1id, testNode2._tree.traverse(List.of("Test2", "Test1")));
testNode1._tree.applyOp(cop2);
testNode2._tree.applyOp(cop1);
// First node wins as it has smaller timestamp
Assertions.assertIterableEquals(List.of("Test1"), testNode1._storageInterface.getById(testNode2._storageInterface.getRootId()).getNode().getChildren().keySet());
Assertions.assertIterableEquals(List.of("Test2"), testNode1._storageInterface.getById(d1id).getNode().getChildren().keySet());
Assertions.assertEquals(d1id, testNode1._tree.traverse(List.of("Test1")));
Assertions.assertEquals(d2id, testNode1._tree.traverse(List.of("Test1", "Test2")));
}
}

View File

@@ -0,0 +1,15 @@
package com.usatiuk.kleppmanntree;
public class TestClock implements Clock<Long> {
private long max = 0;
@Override
public Long getTimestamp() {
return max++;
}
@Override
public void updateTimestamp(Long receivedTimestamp) {
max = Math.max(max, receivedTimestamp) + 1;
}
}

View File

@@ -0,0 +1,18 @@
package com.usatiuk.kleppmanntree;
public class TestNode {
protected final long _id;
protected final TestClock _clock;
protected final TestPeerInterface _peerInterface;
protected final TestStorageInterface _storageInterface;
protected final KleppmannTree<Long, Long, String, TestNodeMetaDir, Long, TestNodeWrapper> _tree;
public TestNode(long id) {
_id = id;
_clock = new TestClock();
_peerInterface = new TestPeerInterface(_id);
_storageInterface = new TestStorageInterface(_id);
_tree = new KleppmannTree<>(_storageInterface, _peerInterface, _clock);
}
}

View File

@@ -0,0 +1,12 @@
package com.usatiuk.kleppmanntree;
public class TestNodeMetaDir implements NodeMeta<String> {
private final String name;
public TestNodeMetaDir(String name) {this.name = name;}
@Override
public String getName() {
return name;
}
}

View File

@@ -0,0 +1,19 @@
package com.usatiuk.kleppmanntree;
import lombok.Getter;
public class TestNodeMetaFile implements NodeMeta<String> {
private final String name;
@Getter
private final long inode;
public TestNodeMetaFile(String name, long inode) {
this.name = name;
this.inode = inode;
}
@Override
public String getName() {
return name;
}
}

View File

@@ -0,0 +1,32 @@
package com.usatiuk.kleppmanntree;
public class TestNodeWrapper implements TreeNodeWrapper<String, TestNodeMetaDir, Long> {
private final TreeNode<String, TestNodeMetaDir, Long> _backingNode;
public TestNodeWrapper(TreeNode<String, TestNodeMetaDir, Long> backingNode) {_backingNode = backingNode;}
@Override
public void rLock() {
}
@Override
public void rUnlock() {
}
@Override
public void rwLock() {
}
@Override
public void rwUnlock() {
}
@Override
public TreeNode<String, TestNodeMetaDir, Long> getNode() {
return _backingNode;
}
}

View File

@@ -0,0 +1,20 @@
package com.usatiuk.kleppmanntree;
import java.util.Collection;
import java.util.List;
public class TestPeerInterface implements PeerInterface<Long> {
private final long selfId;
public TestPeerInterface(long selfId) {this.selfId = selfId;}
@Override
public Long getSelfId() {
return selfId;
}
@Override
public Collection<Long> getAllPeers() {
return List.of(1L, 2L, 3L, 4L);
}
}

View File

@@ -0,0 +1,75 @@
package com.usatiuk.kleppmanntree;
import java.util.*;
public class TestStorageInterface implements StorageInterface<Long, Long, String, TestNodeMetaDir, Long, TestNodeWrapper> {
private long _curId = 1;
private final long _peerId;
private final Map<Long, TreeNode<String, TestNodeMetaDir, Long>> _nodes = new HashMap<>();
private final NavigableMap<CombinedTimestamp<Long, Long>, LogOpMove<Long, Long, String, TestNodeMetaDir, Long>> _log = new TreeMap<>();
public TestStorageInterface(long peerId) {
_peerId = peerId;
_nodes.put(getRootId(), new TreeNode<>(getRootId()));
_nodes.put(getTrashId(), new TreeNode<>(getTrashId()));
}
@Override
public Long getRootId() {
return 0L;
}
@Override
public Long getTrashId() {
return -1L;
}
@Override
public Long getNewNodeId() {
return _curId++ | _peerId << 32;
}
@Override
public TestNodeWrapper getById(Long id) {
var node = _nodes.get(id);
return node == null ? null : new TestNodeWrapper(node);
}
@Override
public TestNodeWrapper createNewNode(Long id) {
if (!_nodes.containsKey(id)) {
var newNode = new TreeNode<String, TestNodeMetaDir, Long>(id);
_nodes.put(id, newNode);
return new TestNodeWrapper(newNode);
}
throw new IllegalStateException("Node with id " + id + " already exists");
}
@Override
public void removeNode(Long id) {
if (!_nodes.containsKey(id))
throw new IllegalStateException("Node with id " + id + " doesn't exist");
_nodes.remove(id);
}
@Override
public void lockSet(Collection<TestNodeWrapper> nodes) {
}
@Override
public NavigableMap<CombinedTimestamp<Long, Long>, LogOpMove<Long, Long, String, TestNodeMetaDir, Long>> getLog() {
return _log;
}
@Override
public void globalLock() {
}
@Override
public void globalUnlock() {
}
}

View File

@@ -12,6 +12,7 @@
<packaging>pom</packaging> <packaging>pom</packaging>
<modules> <modules>
<module>server</module> <module>server</module>
<module>kleppmanntree</module>
</modules> </modules>
<properties> <properties>