71 Commits

Author SHA1 Message Date
fe20db9b3b dump 2025-03-30 13:45:09 +02:00
8b3c0a6f2c Server: snapshot interface key fix 2025-03-29 12:29:28 +01:00
4a19f69c38 Revert "Server: proxying"
This reverts commit 6a8394852e.
2025-03-28 17:26:55 +01:00
8d40019f2c Revert "Server: hopefully working reverse proxy connection"
This reverts commit 312cf18b27.
2025-03-28 17:22:06 +01:00
312cf18b27 Server: hopefully working reverse proxy connection 2025-03-28 12:02:20 +01:00
6a8394852e Server: proxying 2025-03-27 21:42:21 +01:00
3e69e5dfb9 Update Quarkus to 3.20 2025-03-27 15:55:55 +01:00
8fdbaf5aa7 Objects: move objects to com.usatiuk.objects 2025-03-27 15:41:16 +01:00
4d44e3541b Server: move server outside objects 2025-03-27 15:34:59 +01:00
18d5a7f90e Server: move server impl to a separate bean 2025-03-27 13:56:45 +01:00
adcc5f464f Server: fix OOM when truncating large files
use the same chunk for zero fill
2025-03-27 12:23:13 +01:00
d9ded36891 Utils: fix a race in hash set queue test 2025-03-27 12:13:04 +01:00
038b873364 Server: fix syncing when moving directories
as with log trimming we might be moving to folders that don't exist
2025-03-27 12:12:51 +01:00
8f7869d87a Server: 100 ops push 2025-03-24 23:04:44 +01:00
e0b4f97349 Server: tree op push ack (to motivate garbage collection) 2025-03-23 23:18:49 +01:00
035f64df5a Server: make DhfsFileServiceSimpleTestImpl abstract
so that IDEA doesn't try to run it
2025-03-23 22:36:15 +01:00
4c5fd91050 Server: some refcounting fixes 2025-03-23 22:32:32 +01:00
8559c9b984 Server: more logs and fix possible race when resyncing with more peers 2025-03-23 18:11:46 +01:00
e80e33568b Server: try to download all on start only when it's enabled, duh 2025-03-23 16:24:13 +01:00
03850d3522 Server: use linked list for autosync/sync lists 2025-03-23 16:05:29 +01:00
527395447c Server: fix autosync OOM 2025-03-23 16:04:19 +01:00
9108b27dd3 Auto-formatted field order fix 2025-03-23 15:34:01 +01:00
3bd0c4e2bb Remove server-old 2025-03-23 15:29:45 +01:00
c977b5f6c9 Run code cleanup 2025-03-23 15:29:32 +01:00
c5a875c27f Server: separate ref types
as we need to get the real "parent" object when sending some requests
2025-03-23 15:29:06 +01:00
ba6bb756bb Server: remove hackRefresh thing 2025-03-23 14:31:51 +01:00
a63e7e59b3 Utils: die on lock timeout 2025-03-23 14:27:36 +01:00
9a02a554a1 Server: hopefully working autosync 2025-03-23 14:27:20 +01:00
892e5ca9b7 run wrapper fixes 2025-03-23 14:03:18 +01:00
c12bff3ee7 Revert "CI: downgrade maven because of some locks bug"
This reverts commit 59a0b9a856.
2025-03-22 23:18:37 +01:00
59a0b9a856 CI: downgrade maven because of some locks bug 2025-03-22 23:15:09 +01:00
817d12a161 CI: parallel maven 2025-03-22 23:10:01 +01:00
258c257778 Server: increase manyFiles test timeout 2025-03-22 23:08:57 +01:00
b0bb9121e7 Server: re-enable parallel tests 2025-03-22 23:05:25 +01:00
a224c6bd51 Server: dumber deleteTest 2025-03-22 23:05:17 +01:00
13ecdd3106 Server: fix initial sync for normal objects, and fix ResyncIT 2025-03-22 22:53:33 +01:00
8a07f37566 Server: a bunch of fixes 2025-03-22 21:31:20 +01:00
dc0e73b1aa KleppmannTree: fix failed moves being recorded in op log 2025-03-22 14:57:01 +01:00
16eb1d28d9 Objects: add a "never lock option" to avoid deadlocks 2025-03-21 23:43:29 +01:00
4f397cd2d4 KleppmannTree: fix undo with rename 2025-03-21 23:31:59 +01:00
6a20550353 Server: better logging and (hopefully) fix ktree op forwarding messing up timestamps 2025-03-21 20:23:59 +01:00
92bca1e4e1 Server: fix peer sync not quite working with >2 nodes 2025-03-21 12:44:00 +01:00
4bfa93fca4 Server: handle PeerInfo conflicts (by ignoring them) 2025-03-20 22:26:28 +01:00
7d762c70fa Server: seemingly almost working file conflict resolution
though the removeAddHostTest doesn't seem to quite like it
2025-03-20 22:26:21 +01:00
20daa857e6 Server: slightly less broken peer removal 2025-03-17 23:36:34 +01:00
97c0f002fb Objects: don't run tx commit hooks inside the same transaction 2025-03-17 23:29:10 +01:00
f260bb0491 Server: improve run options 2025-03-17 08:44:11 +01:00
a2e75dbdc7 Server: some peer sync fixes 2025-03-16 20:07:45 +01:00
fa64dac9aa Server: peer connected/disconnected event listeners 2025-03-16 12:58:56 +01:00
8fbdf50732 Server: seemingly working file sync 2025-03-16 00:13:42 +01:00
be1f5d12c9 Server: fix logs in PeerManager
somehow it doesn't like the format of toString of some objects
2025-03-16 00:13:15 +01:00
1fd3b9e5e0 Server: don't always take tree lock in PeerInfoService 2025-03-16 00:12:45 +01:00
8499e20823 Server: fix for non-absolute WebUi paths 2025-03-16 00:11:52 +01:00
842bd49246 Objects: lmdb iterator find first fix 2025-03-16 00:08:05 +01:00
1b0af6e883 Objects: remove IterProdFn close() 2025-03-16 00:07:16 +01:00
667f8b3b42 Objects: a bunch of moving around 2025-03-14 22:39:54 +01:00
0aca2c5dbb Objects: move serializer stuff a little 2025-03-14 22:31:45 +01:00
223ba20418 Objects: run tx commit callbacks in empty transactions 2025-03-14 22:23:36 +01:00
ae17ab6ce9 Server: JMapHolder cleanup on delete 2025-03-14 16:22:22 +01:00
6e37320e7c Objects: seemingly more reasonably working tx hooks 2025-03-14 16:21:07 +01:00
d37dc944d0 Objects: remove findAllObjects 2025-03-14 15:03:11 +01:00
d483eba20d Objects: working cache for object reads without iterator 2025-03-14 00:01:21 +01:00
4cbb4ce2be Server: don't write-lock fs tree when opening files 2025-03-13 23:54:24 +01:00
5f85e944e3 Objects: fix read only transactions locking everything 2025-03-13 23:36:34 +01:00
4c90a74fea Server: don't keep file size separately 2025-03-13 23:10:43 +01:00
38ab6de85b Objects: fix tests not being run by maven 2025-03-13 21:15:53 +01:00
29fd2826a3 Objects: simplify snapshots 2025-03-13 19:35:19 +01:00
3faab4c324 less hardcoded paths in run config 2025-03-13 11:49:34 +01:00
5071cd908a add run configuration to repo 2025-03-13 10:10:04 +01:00
3470ce8690 Objects: lazier iterators 2025-03-13 10:07:49 +01:00
1d22465e4a Objects: don't recreate iterators when initializing in MergingKvIterator 2025-03-12 21:21:10 +01:00
371 changed files with 5069 additions and 14787 deletions

View File

@@ -49,7 +49,7 @@ jobs:
cache: maven
- name: Test with Maven
run: cd dhfs-parent && mvn --batch-mode --update-snapshots package verify
run: cd dhfs-parent && mvn -T $(nproc) --batch-mode --update-snapshots package verify
# - name: Build with Maven
# run: cd dhfs-parent && mvn --batch-mode --update-snapshots package # -Dquarkus.log.category.\"com.usatiuk.dhfs\".min-level=DEBUG

View File

@@ -0,0 +1,17 @@
<component name="ProjectRunConfigurationManager">
<configuration default="false" name="Main 2" type="QsApplicationConfigurationType" factoryName="QuarkusApplication">
<option name="MAIN_CLASS_NAME" value="com.usatiuk.dhfs.Main"/>
<module name="server"/>
<option name="VM_PARAMETERS"
value="-XX:+UnlockDiagnosticVMOptions -XX:+DebugNonSafepoints --add-exports java.base/sun.nio.ch=ALL-UNNAMED --add-opens=java.base/java.nio=ALL-UNNAMED --add-exports java.base/jdk.internal.access=ALL-UNNAMED -ea -Dcom.usatiuk.dhfs.supportlib.native-path=$ProjectFileDir$/target/classes/native -Xmx2G -Ddhfs.webui.root=$ProjectFileDir$/../webui/dist -Ddhfs.fuse.root=${HOME}/dhfs_test/2/fuse -Ddhfs.objects.persistence.files.root=${HOME}/dhfs_test/2/data -Ddhfs.objects.persistence.stuff.root=${HOME}/dhfs_test/2/data/stuff -Ddhfs.objects.peerdiscovery.broadcast=false -Dquarkus.http.port=9020 -Dquarkus.http.ssl-port=9021 -Ddhfs.peerdiscovery.preset-uuid=22000000-0000-0000-0000-000000000000 -Ddhfs.peerdiscovery.static-peers=11000000-0000-0000-0000-000000000000:127.0.0.1:9010:9011"/>
<extension name="coverage">
<pattern>
<option name="PATTERN" value="com.usatiuk.dhfs.*"/>
<option name="ENABLED" value="true"/>
</pattern>
</extension>
<method v="2">
<option name="Make" enabled="true"/>
</method>
</configuration>
</component>

View File

@@ -0,0 +1,18 @@
<component name="ProjectRunConfigurationManager">
<configuration default="false" name="Main" type="QsApplicationConfigurationType" factoryName="QuarkusApplication"
nameIsGenerated="true">
<option name="MAIN_CLASS_NAME" value="com.usatiuk.dhfs.Main"/>
<module name="server"/>
<option name="VM_PARAMETERS"
value="-XX:+UnlockDiagnosticVMOptions -XX:+DebugNonSafepoints --add-exports java.base/sun.nio.ch=ALL-UNNAMED --add-opens=java.base/java.nio=ALL-UNNAMED --add-exports java.base/jdk.internal.access=ALL-UNNAMED -ea -Dcom.usatiuk.dhfs.supportlib.native-path=$ProjectFileDir$/target/classes/native -Xmx2G -Ddhfs.webui.root=$ProjectFileDir$/../webui/dist -Ddhfs.fuse.root=${HOME}/dhfs_test/1/fuse -Ddhfs.objects.persistence.files.root=${HOME}/dhfs_test/1/data -Ddhfs.objects.persistence.stuff.root=${HOME}/dhfs_test/1/data/stuff -Ddhfs.objects.peerdiscovery.broadcast=false -Dquarkus.http.port=9010 -Dquarkus.http.ssl-port=9011 -Ddhfs.peerdiscovery.preset-uuid=11000000-0000-0000-0000-000000000000 -Ddhfs.peerdiscovery.static-peers=22000000-0000-0000-0000-000000000000:127.0.0.1:9020:9021"/>
<extension name="coverage">
<pattern>
<option name="PATTERN" value="com.usatiuk.dhfs.*"/>
<option name="ENABLED" value="true"/>
</pattern>
</extension>
<method v="2">
<option name="Make" enabled="true"/>
</method>
</configuration>
</component>

View File

@@ -6,8 +6,8 @@ import com.usatiuk.autoprotomap.runtime.ProtoSerializer;
import io.quarkus.gizmo.*;
import jakarta.inject.Inject;
import org.apache.commons.lang3.tuple.Pair;
import org.jboss.jandex.Type;
import org.jboss.jandex.*;
import org.jboss.jandex.Type;
import org.objectweb.asm.Opcodes;
import java.util.ArrayList;

View File

@@ -82,7 +82,7 @@
<profiles>
<profile>
<id>native-image</id>
<id>native</id>
<activation>
<property>
<name>native</name>

View File

@@ -1 +0,0 @@
quarkus.package.jar.decompiler.enabled=true

View File

@@ -18,6 +18,11 @@
<artifactId>junit-jupiter-engine</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.junit.jupiter</groupId>
<artifactId>junit-jupiter</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-collections4</artifactId>
@@ -30,5 +35,9 @@
<groupId>org.pcollections</groupId>
<artifactId>pcollections</artifactId>
</dependency>
<dependency>
<groupId>jakarta.annotation</groupId>
<artifactId>jakarta.annotation-api</artifactId>
</dependency>
</dependencies>
</project>

View File

@@ -1,5 +1,7 @@
package com.usatiuk.kleppmanntree;
import jakarta.annotation.Nonnull;
import jakarta.annotation.Nullable;
import org.apache.commons.lang3.tuple.Pair;
import java.util.*;
@@ -53,18 +55,20 @@ public class KleppmannTree<TimestampT extends Comparable<TimestampT>, PeerIdT ex
var node = _storage.getById(effect.childId());
var curParent = _storage.getById(effect.newParentId());
{
var newCurParentChildren = curParent.children().minus(node.meta().getName());
var newCurParentChildren = curParent.children().minus(node.name());
curParent = curParent.withChildren(newCurParentChildren);
_storage.putNode(curParent);
}
if (!node.meta().getClass().equals(effect.oldInfo().oldMeta().getClass()))
if (effect.oldInfo().oldMeta() != null
&& node.meta() != null
&& !node.meta().getClass().equals(effect.oldInfo().oldMeta().getClass()))
throw new IllegalArgumentException("Class mismatch for meta for node " + node.key());
// Needs to be read after changing curParent, as it might be the same node
var oldParent = _storage.getById(effect.oldInfo().oldParent());
{
var newOldParentChildren = oldParent.children().plus(node.meta().getName(), node.key());
var newOldParentChildren = oldParent.children().plus(effect.oldName(), node.key());
oldParent = oldParent.withChildren(newOldParentChildren);
_storage.putNode(oldParent);
}
@@ -77,7 +81,7 @@ public class KleppmannTree<TimestampT extends Comparable<TimestampT>, PeerIdT ex
var node = _storage.getById(effect.childId());
var curParent = _storage.getById(effect.newParentId());
{
var newCurParentChildren = curParent.children().minus(node.meta().getName());
var newCurParentChildren = curParent.children().minus(node.name());
curParent = curParent.withChildren(newCurParentChildren);
_storage.putNode(curParent);
}
@@ -90,6 +94,7 @@ public class KleppmannTree<TimestampT extends Comparable<TimestampT>, PeerIdT ex
}
private void undoOp(LogRecord<TimestampT, PeerIdT, MetaT, NodeIdT> op) {
LOGGER.finer(() -> "Will undo op: " + op);
if (op.effects() != null)
for (var e : op.effects().reversed())
undoEffect(e);
@@ -140,8 +145,8 @@ public class KleppmannTree<TimestampT extends Comparable<TimestampT>, PeerIdT ex
}
}
}
}
if (!inTrash.isEmpty()) {
var trash = _storage.getById(_storage.getTrashId());
for (var n : inTrash) {
@@ -166,8 +171,8 @@ public class KleppmannTree<TimestampT extends Comparable<TimestampT>, PeerIdT ex
public void move(NodeIdT newParent, MetaT newMeta, NodeIdT child, boolean failCreatingIfExists) {
var createdMove = createMove(newParent, newMeta, child);
_opRecorder.recordOp(createdMove);
applyOp(_peers.getSelfId(), createdMove, failCreatingIfExists);
_opRecorder.recordOp(createdMove);
}
public void applyExternalOp(PeerIdT from, OpMove<TimestampT, PeerIdT, MetaT, NodeIdT> op) {
@@ -178,7 +183,7 @@ public class KleppmannTree<TimestampT extends Comparable<TimestampT>, PeerIdT ex
// Returns true if the timestamp is newer than what's seen, false otherwise
private boolean updateTimestampImpl(PeerIdT from, TimestampT newTimestamp) {
TimestampT oldRef = _storage.getPeerTimestampLog().getForPeer(from);
if (oldRef != null && oldRef.compareTo(newTimestamp) > 0) { // FIXME?
if (oldRef != null && oldRef.compareTo(newTimestamp) >= 0) { // FIXME?
LOGGER.warning("Wrong op order: received older than known from " + from.toString());
return false;
}
@@ -186,20 +191,20 @@ public class KleppmannTree<TimestampT extends Comparable<TimestampT>, PeerIdT ex
return true;
}
public boolean updateExternalTimestamp(PeerIdT from, TimestampT timestamp) {
// TODO: Ideally no point in this separate locking?
public void updateExternalTimestamp(PeerIdT from, TimestampT timestamp) {
var gotExt = _storage.getPeerTimestampLog().getForPeer(from);
var gotSelf = _storage.getPeerTimestampLog().getForPeer(_peers.getSelfId());
if ((gotExt != null && gotExt.compareTo(timestamp) >= 0)
&& (gotSelf != null && gotSelf.compareTo(_clock.peekTimestamp()) >= 0)) return false;
updateTimestampImpl(_peers.getSelfId(), _clock.peekTimestamp()); // FIXME:? Kind of a hack?
updateTimestampImpl(from, timestamp);
if (!(gotExt != null && gotExt.compareTo(timestamp) >= 0))
updateTimestampImpl(from, timestamp);
if (!(gotSelf != null && gotSelf.compareTo(_clock.peekTimestamp()) >= 0))
updateTimestampImpl(_peers.getSelfId(), _clock.peekTimestamp()); // FIXME:? Kind of a hack?
tryTrimLog();
return true;
}
private void applyOp(PeerIdT from, OpMove<TimestampT, PeerIdT, MetaT, NodeIdT> op, boolean failCreatingIfExists) {
if (!updateTimestampImpl(from, op.timestamp().timestamp())) return;
if (!updateTimestampImpl(op.timestamp().nodeId(), op.timestamp().timestamp())) return;
LOGGER.finer(() -> "Will apply op: " + op + " from " + from);
var log = _storage.getLog();
@@ -252,6 +257,7 @@ public class KleppmannTree<TimestampT extends Comparable<TimestampT>, PeerIdT ex
}
private LogRecord<TimestampT, PeerIdT, MetaT, NodeIdT> doOp(OpMove<TimestampT, PeerIdT, MetaT, NodeIdT> op, boolean failCreatingIfExists) {
LOGGER.finer(() -> "Doing op: " + op);
LogRecord<TimestampT, PeerIdT, MetaT, NodeIdT> computed;
try {
computed = computeEffects(op, failCreatingIfExists);
@@ -291,6 +297,7 @@ public class KleppmannTree<TimestampT extends Comparable<TimestampT>, PeerIdT ex
private void applyEffects(OpMove<TimestampT, PeerIdT, MetaT, NodeIdT> sourceOp, List<LogEffect<TimestampT, PeerIdT, MetaT, NodeIdT>> effects) {
for (var effect : effects) {
LOGGER.finer(() -> "Applying effect: " + effect + " from op " + sourceOp);
TreeNode<TimestampT, PeerIdT, MetaT, NodeIdT> oldParentNode = null;
TreeNode<TimestampT, PeerIdT, MetaT, NodeIdT> newParentNode;
TreeNode<TimestampT, PeerIdT, MetaT, NodeIdT> node;
@@ -304,7 +311,7 @@ public class KleppmannTree<TimestampT extends Comparable<TimestampT>, PeerIdT ex
node = _storage.getById(effect.childId());
}
if (oldParentNode != null) {
var newOldParentChildren = oldParentNode.children().minus(effect.oldInfo().oldMeta().getName());
var newOldParentChildren = oldParentNode.children().minus(effect.oldName());
oldParentNode = oldParentNode.withChildren(newOldParentChildren);
_storage.putNode(oldParentNode);
}
@@ -313,12 +320,12 @@ public class KleppmannTree<TimestampT extends Comparable<TimestampT>, PeerIdT ex
newParentNode = _storage.getById(effect.newParentId());
{
var newNewParentChildren = newParentNode.children().plus(effect.newMeta().getName(), effect.childId());
var newNewParentChildren = newParentNode.children().plus(effect.newName(), effect.childId());
newParentNode = newParentNode.withChildren(newNewParentChildren);
_storage.putNode(newParentNode);
}
if (effect.newParentId().equals(_storage.getTrashId()) &&
!Objects.equals(effect.newMeta().getName(), effect.childId().toString()))
!Objects.equals(effect.newName(), effect.childId().toString()))
throw new IllegalArgumentException("Move to trash should have id of node as name");
_storage.putNode(
node.withParent(effect.newParentId())
@@ -335,17 +342,32 @@ public class KleppmannTree<TimestampT extends Comparable<TimestampT>, PeerIdT ex
NodeIdT newParentId = op.newParentId();
TreeNode<TimestampT, PeerIdT, MetaT, NodeIdT> newParent = _storage.getById(newParentId);
if (newParent == null) {
LOGGER.log(Level.SEVERE, "New parent not found " + op.newMeta().getName() + " " + op.childId());
return new LogRecord<>(op, null);
LOGGER.log(Level.SEVERE, "New parent not found " + op.newName() + " " + op.childId());
// Creation
if (oldParentId == null) {
LOGGER.severe(() -> "Creating both dummy parent and child node");
return new LogRecord<>(op, List.of(
new LogEffect<>(null, op, _storage.getLostFoundId(), null, newParentId),
new LogEffect<>(null, op, newParentId, op.newMeta(), op.childId())
));
} else {
LOGGER.severe(() -> "Moving child node to dummy parent");
return new LogRecord<>(op, List.of(
new LogEffect<>(null, op, _storage.getLostFoundId(), null, newParentId),
new LogEffect<>(new LogEffectOld<>(node.lastEffectiveOp(), oldParentId, node.meta()), op, op.newParentId(), op.newMeta(), op.childId())
));
}
}
if (oldParentId == null) {
var conflictNodeId = newParent.children().get(op.newMeta().getName());
var conflictNodeId = newParent.children().get(op.newName());
if (conflictNodeId != null) {
if (failCreatingIfExists)
throw new AlreadyExistsException("Already exists: " + op.newMeta().getName() + ": " + conflictNodeId);
throw new AlreadyExistsException("Already exists: " + op.newName() + ": " + conflictNodeId);
var conflictNode = _storage.getById(conflictNodeId);
MetaT conflictNodeMeta = conflictNode.meta();
@@ -354,13 +376,16 @@ public class KleppmannTree<TimestampT extends Comparable<TimestampT>, PeerIdT ex
return new LogRecord<>(op, null);
}
String newConflictNodeName = conflictNodeMeta.getName() + ".conflict." + conflictNode.key();
String newOursName = op.newMeta().getName() + ".conflict." + op.childId();
LOGGER.finer(() -> "Node creation conflict: " + conflictNode);
String newConflictNodeName = op.newName() + ".conflict." + conflictNode.key();
String newOursName = op.newName() + ".conflict." + op.childId();
return new LogRecord<>(op, List.of(
new LogEffect<>(new LogEffectOld<>(conflictNode.lastEffectiveOp(), newParentId, conflictNodeMeta), conflictNode.lastEffectiveOp(), newParentId, (MetaT) conflictNodeMeta.withName(newConflictNodeName), conflictNodeId),
new LogEffect<>(null, op, op.newParentId(), (MetaT) op.newMeta().withName(newOursName), op.childId())
));
} else {
LOGGER.finer(() -> "Simple node creation");
return new LogRecord<>(op, List.of(
new LogEffect<>(null, op, newParentId, op.newMeta(), op.childId())
));
@@ -372,11 +397,13 @@ public class KleppmannTree<TimestampT extends Comparable<TimestampT>, PeerIdT ex
}
MetaT oldMeta = node.meta();
if (!oldMeta.getClass().equals(op.newMeta().getClass())) {
if (oldMeta != null
&& op.newMeta() != null
&& !oldMeta.getClass().equals(op.newMeta().getClass())) {
LOGGER.log(Level.SEVERE, "Class mismatch for meta for node " + node.key());
return new LogRecord<>(op, null);
}
var replaceNodeId = newParent.children().get(op.newMeta().getName());
var replaceNodeId = newParent.children().get(op.newName());
if (replaceNodeId != null) {
var replaceNode = _storage.getById(replaceNodeId);
var replaceNodeMeta = replaceNode.meta();
@@ -385,11 +412,15 @@ public class KleppmannTree<TimestampT extends Comparable<TimestampT>, PeerIdT ex
return new LogRecord<>(op, null);
}
LOGGER.finer(() -> "Node replacement: " + replaceNode);
return new LogRecord<>(op, List.of(
new LogEffect<>(new LogEffectOld<>(replaceNode.lastEffectiveOp(), newParentId, replaceNodeMeta), replaceNode.lastEffectiveOp(), _storage.getTrashId(), (MetaT) replaceNodeMeta.withName(replaceNodeId.toString()), replaceNodeId),
new LogEffect<>(new LogEffectOld<>(node.lastEffectiveOp(), oldParentId, oldMeta), op, op.newParentId(), op.newMeta(), op.childId())
));
}
LOGGER.finer(() -> "Simple node move");
return new LogRecord<>(op, List.of(
new LogEffect<>(new LogEffectOld<>(node.lastEffectiveOp(), oldParentId, oldMeta), op, op.newParentId(), op.newMeta(), op.childId())
));
@@ -444,18 +475,18 @@ public class KleppmannTree<TimestampT extends Comparable<TimestampT>, PeerIdT ex
walkTree(node -> {
var op = node.lastEffectiveOp();
if (node.lastEffectiveOp() == null) return;
LOGGER.info("visited bootstrap op for " + host + ": " + op.timestamp().toString() + " " + op.newMeta().getName() + " " + op.childId() + "->" + op.newParentId());
LOGGER.info("visited bootstrap op for " + host + ": " + op.timestamp().toString() + " " + op.newName() + " " + op.childId() + "->" + op.newParentId());
result.put(node.lastEffectiveOp().timestamp(), node.lastEffectiveOp());
});
for (var le : _storage.getLog().getAll()) {
var op = le.getValue().op();
LOGGER.info("bootstrap op from log for " + host + ": " + op.timestamp().toString() + " " + op.newMeta().getName() + " " + op.childId() + "->" + op.newParentId());
LOGGER.info("bootstrap op from log for " + host + ": " + op.timestamp().toString() + " " + op.newName() + " " + op.childId() + "->" + op.newParentId());
result.put(le.getKey(), le.getValue().op());
}
for (var op : result.values()) {
LOGGER.info("Recording bootstrap op for " + host + ": " + op.timestamp().toString() + " " + op.newMeta().getName() + " " + op.childId() + "->" + op.newParentId());
LOGGER.info("Recording bootstrap op for " + host + ": " + op.timestamp().toString() + " " + op.newName() + " " + op.childId() + "->" + op.newParentId());
_opRecorder.recordOpForPeer(host, op);
}
}

View File

@@ -8,4 +8,17 @@ public record LogEffect<TimestampT extends Comparable<TimestampT>, PeerIdT exten
NodeIdT newParentId,
MetaT newMeta,
NodeIdT childId) implements Serializable {
public String oldName() {
if (oldInfo.oldMeta() != null) {
return oldInfo.oldMeta().getName();
}
return childId.toString();
}
public String newName() {
if (newMeta != null) {
return newMeta.getName();
}
return childId.toString();
}
}

View File

@@ -5,4 +5,9 @@ import java.io.Serializable;
public record OpMove<TimestampT extends Comparable<TimestampT>, PeerIdT extends Comparable<PeerIdT>, MetaT extends NodeMeta, NodeIdT>
(CombinedTimestamp<TimestampT, PeerIdT> timestamp, NodeIdT newParentId, MetaT newMeta,
NodeIdT childId) implements Serializable {
public String newName() {
if (newMeta != null)
return newMeta.getName();
return childId.toString();
}
}

View File

@@ -9,6 +9,8 @@ public interface StorageInterface<
NodeIdT getTrashId();
NodeIdT getLostFoundId();
NodeIdT getNewNodeId();
TreeNode<TimestampT, PeerIdT, MetaT, NodeIdT> getById(NodeIdT id);

View File

@@ -1,9 +1,9 @@
package com.usatiuk.kleppmanntree;
import jakarta.annotation.Nullable;
import org.pcollections.PMap;
import java.io.Serializable;
import java.util.Map;
public interface TreeNode<TimestampT extends Comparable<TimestampT>, PeerIdT extends Comparable<PeerIdT>, MetaT extends NodeMeta, NodeIdT> extends Serializable {
NodeIdT key();
@@ -12,8 +12,15 @@ public interface TreeNode<TimestampT extends Comparable<TimestampT>, PeerIdT ext
OpMove<TimestampT, PeerIdT, MetaT, NodeIdT> lastEffectiveOp();
@Nullable
MetaT meta();
default String name() {
var meta = meta();
if (meta != null) return meta.getName();
return key().toString();
}
PMap<String, NodeIdT> children();
TreeNode<TimestampT, PeerIdT, MetaT, NodeIdT> withParent(NodeIdT parent);

View File

@@ -2,13 +2,15 @@ package com.usatiuk.kleppmanntree;
import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.ValueSource;
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);
@Test
void circularTest() {
@@ -89,4 +91,75 @@ public class KleppmanTreeSimpleTest {
Assertions.assertTrue(testNode2._storageInterface.getLog().size() <= 1);
}
@ParameterizedTest
@ValueSource(booleans = {true, false})
void undoWithRenameTest(boolean opOrder) {
var d1id = testNode1._storageInterface.getNewNodeId();
var d2id = testNode2._storageInterface.getNewNodeId();
var d3id = testNode3._storageInterface.getNewNodeId();
testNode1._tree.move(testNode1._storageInterface.getRootId(), new TestNodeMetaDir("Test1"), d1id);
testNode2._tree.move(testNode1._storageInterface.getRootId(), new TestNodeMetaDir("Test1"), d2id);
testNode3._tree.move(testNode1._storageInterface.getRootId(), new TestNodeMetaDir("Test1"), d3id);
var r1 = testNode1.getRecorded();
var r2 = testNode2.getRecorded();
var r3 = testNode3.getRecorded();
Assertions.assertEquals(1, r1.size());
Assertions.assertEquals(1, r2.size());
Assertions.assertEquals(1, r3.size());
if (opOrder) {
testNode2._tree.applyExternalOp(3L, r3.getFirst());
testNode2._tree.applyExternalOp(1L, r1.getFirst());
} else {
testNode2._tree.applyExternalOp(1L, r1.getFirst());
testNode2._tree.applyExternalOp(3L, r3.getFirst());
}
Assertions.assertIterableEquals(List.of("Test1", "Test1.conflict." + d1id, "Test1.conflict." + d2id), testNode2._storageInterface.getById(testNode2._storageInterface.getRootId()).children().keySet());
if (opOrder) {
testNode1._tree.applyExternalOp(3L, r3.getFirst());
testNode1._tree.applyExternalOp(2L, r2.getFirst());
} else {
testNode1._tree.applyExternalOp(2L, r2.getFirst());
testNode1._tree.applyExternalOp(3L, r3.getFirst());
}
Assertions.assertIterableEquals(List.of("Test1", "Test1.conflict." + d1id, "Test1.conflict." + d2id), testNode1._storageInterface.getById(testNode1._storageInterface.getRootId()).children().keySet());
if (opOrder) {
testNode3._tree.applyExternalOp(2L, r2.getFirst());
testNode3._tree.applyExternalOp(1L, r1.getFirst());
} else {
testNode3._tree.applyExternalOp(1L, r1.getFirst());
testNode3._tree.applyExternalOp(2L, r2.getFirst());
}
Assertions.assertIterableEquals(List.of("Test1", "Test1.conflict." + d1id, "Test1.conflict." + d2id), testNode3._storageInterface.getById(testNode3._storageInterface.getRootId()).children().keySet());
}
@Test
void noFailedOpRecordTest() {
var d1id = testNode1._storageInterface.getNewNodeId();
var d2id = testNode1._storageInterface.getNewNodeId();
testNode1._tree.move(testNode1._storageInterface.getRootId(), new TestNodeMetaDir("Test1"), d1id);
Assertions.assertThrows(AlreadyExistsException.class, () -> testNode1._tree.move(testNode1._storageInterface.getRootId(), new TestNodeMetaDir("Test1"), d2id));
var r1 = testNode1.getRecorded();
Assertions.assertEquals(1, r1.size());
}
@Test
void externalOpWithDummy() {
Long d1id = testNode1._storageInterface.getNewNodeId();
Long f1id = testNode1._storageInterface.getNewNodeId();
testNode1._tree.applyExternalOp(2L, new OpMove<>(
new CombinedTimestamp<>(2L, 2L), d1id, new TestNodeMetaFile("Hi", 123), f1id
));
testNode1._tree.applyExternalOp(2L, new OpMove<>(
new CombinedTimestamp<>(3L, 2L), testNode1._storageInterface.getRootId(), new TestNodeMetaDir("HiDir"), d1id
));
Assertions.assertEquals(f1id, testNode1._tree.traverse(List.of("HiDir", "Hi")));
}
}

View File

@@ -14,6 +14,7 @@ public class TestStorageInterface implements StorageInterface<Long, Long, TestNo
_peerId = peerId;
_nodes.put(getRootId(), new TestTreeNode(getRootId(), null, null));
_nodes.put(getTrashId(), new TestTreeNode(getTrashId(), null, null));
_nodes.put(getLostFoundId(), new TestTreeNode(getLostFoundId(), null, null));
}
@Override
@@ -26,6 +27,11 @@ public class TestStorageInterface implements StorageInterface<Long, Long, TestNo
return -1L;
}
@Override
public Long getLostFoundId() {
return -2L;
}
@Override
public Long getNewNodeId() {
return _curId++ | _peerId << 32;

View File

@@ -1,44 +0,0 @@
package com.usatiuk.dhfs.objects;
import com.google.protobuf.ByteString;
import com.usatiuk.dhfs.utils.SerializationHelper;
public class JDataVersionedWrapperLazy implements JDataVersionedWrapper {
private final long _version;
private ByteString _rawData;
private JData _data;
public JDataVersionedWrapperLazy(long version, ByteString rawData) {
_version = version;
_rawData = rawData;
}
public JData data() {
if (_data != null)
return _data;
synchronized (this) {
if (_data != null)
return _data;
try (var is = _rawData.newInput()) {
_data = SerializationHelper.deserialize(is);
} catch (Exception e) {
throw new RuntimeException(e);
}
_rawData = null;
return _data;
}
}
public long version() {
return _version;
}
@Override
public int estimateSize() {
if (_data != null)
return _data.estimateSize();
return _rawData.size();
}
}

View File

@@ -1,192 +0,0 @@
package com.usatiuk.dhfs.objects;
import com.usatiuk.dhfs.objects.persistence.IteratorStart;
import io.quarkus.logging.Log;
import org.apache.commons.lang3.tuple.Pair;
import java.util.*;
import java.util.stream.Collectors;
public class MergingKvIterator<K extends Comparable<K>, V> extends ReversibleKvIterator<K, V> {
private final Map<CloseableKvIterator<K, V>, Integer> _iterators;
private final NavigableMap<K, CloseableKvIterator<K, V>> _sortedIterators = new TreeMap<>();
private final String _name;
public MergingKvIterator(String name, IteratorStart startType, K startKey, List<IterProdFn<K, V>> iterators) {
_goingForward = true;
_name = name;
IteratorStart initialStartType = startType;
K initialStartKey = startKey;
boolean fail = false;
if (startType == IteratorStart.LT || startType == IteratorStart.LE) {
// Starting at a greatest key less than/less or equal than:
// We have a bunch of iterators that have given us theirs "greatest LT/LE key"
// now we need to pick the greatest of those to start with
// But if some of them don't have a lesser key, we need to pick the smallest of those
var initialIterators = iterators.stream().map(p -> p.get(initialStartType, initialStartKey)).toList();
try {
IteratorStart finalStartType = startType;
var found = initialIterators.stream()
.filter(CloseableKvIterator::hasNext)
.map((i) -> {
var peeked = i.peekNextKey();
// Log.warnv("peeked: {0}, from {1}", peeked, i.getClass());
return peeked;
}).distinct().collect(Collectors.partitioningBy(e -> finalStartType == IteratorStart.LE ? e.compareTo(initialStartKey) <= 0 : e.compareTo(initialStartKey) < 0));
K initialMaxValue;
if (!found.get(true).isEmpty())
initialMaxValue = found.get(true).stream().max(Comparator.naturalOrder()).orElse(null);
else
initialMaxValue = found.get(false).stream().min(Comparator.naturalOrder()).orElse(null);
if (initialMaxValue == null) {
fail = true;
}
startKey = initialMaxValue;
startType = IteratorStart.GE;
} finally {
initialIterators.forEach(CloseableKvIterator::close);
}
}
if (fail) {
_iterators = Map.of();
return;
}
int counter = 0;
var iteratorsTmp = new HashMap<CloseableKvIterator<K, V>, Integer>();
for (var iteratorFn : iterators) {
var iterator = iteratorFn.get(startType, startKey);
iteratorsTmp.put(iterator, counter++);
}
_iterators = Map.copyOf(iteratorsTmp);
for (CloseableKvIterator<K, V> iterator : _iterators.keySet()) {
advanceIterator(iterator);
}
Log.tracev("{0} Created: {1}", _name, _sortedIterators);
switch (initialStartType) {
// case LT -> {
// assert _sortedIterators.isEmpty() || _sortedIterators.firstKey().compareTo(initialStartKey) < 0;
// }
// case LE -> {
// assert _sortedIterators.isEmpty() || _sortedIterators.firstKey().compareTo(initialStartKey) <= 0;
// }
case GT -> {
assert _sortedIterators.isEmpty() || _sortedIterators.firstKey().compareTo(initialStartKey) > 0;
}
case GE -> {
assert _sortedIterators.isEmpty() || _sortedIterators.firstKey().compareTo(initialStartKey) >= 0;
}
}
}
@SafeVarargs
public MergingKvIterator(String name, IteratorStart startType, K startKey, IterProdFn<K, V>... iterators) {
this(name, startType, startKey, List.of(iterators));
}
private void advanceIterator(CloseableKvIterator<K, V> iterator) {
if (!iterator.hasNext()) {
return;
}
K key = iterator.peekNextKey();
Log.tracev("{0} Advance peeked: {1}-{2}", _name, iterator, key);
if (!_sortedIterators.containsKey(key)) {
_sortedIterators.put(key, iterator);
return;
}
// Expects that reversed iterator returns itself when reversed again
var oursPrio = _iterators.get(_goingForward ? iterator : iterator.reversed());
var them = _sortedIterators.get(key);
var theirsPrio = _iterators.get(_goingForward ? them : them.reversed());
if (oursPrio < theirsPrio) {
_sortedIterators.put(key, iterator);
advanceIterator(them);
} else {
Log.tracev("{0} Skipped: {1}", _name, iterator.peekNextKey());
iterator.skip();
advanceIterator(iterator);
}
}
@Override
protected void reverse() {
var cur = _goingForward ? _sortedIterators.pollFirstEntry() : _sortedIterators.pollLastEntry();
Log.tracev("{0} Reversing from {1}", _name, cur);
_goingForward = !_goingForward;
_sortedIterators.clear();
for (CloseableKvIterator<K, V> iterator : _iterators.keySet()) {
// _goingForward inverted already
advanceIterator(!_goingForward ? iterator.reversed() : iterator);
}
if (_sortedIterators.isEmpty() || cur == null) {
return;
}
// Advance to the expected key, as we might have brought back some iterators
// that were at their ends
while (!_sortedIterators.isEmpty()
&& ((_goingForward && peekImpl().compareTo(cur.getKey()) <= 0)
|| (!_goingForward && peekImpl().compareTo(cur.getKey()) >= 0))) {
skipImpl();
}
Log.tracev("{0} Reversed to {1}", _name, _sortedIterators);
}
@Override
protected K peekImpl() {
if (_sortedIterators.isEmpty())
throw new NoSuchElementException();
return _goingForward ? _sortedIterators.firstKey() : _sortedIterators.lastKey();
}
@Override
protected void skipImpl() {
var cur = _goingForward ? _sortedIterators.pollFirstEntry() : _sortedIterators.pollLastEntry();
if (cur == null) {
throw new NoSuchElementException();
}
cur.getValue().skip();
advanceIterator(cur.getValue());
Log.tracev("{0} Skip: {1}, next: {2}", _name, cur, _sortedIterators);
}
@Override
protected boolean hasImpl() {
return !_sortedIterators.isEmpty();
}
@Override
protected Pair<K, V> nextImpl() {
var cur = _goingForward ? _sortedIterators.pollFirstEntry() : _sortedIterators.pollLastEntry();
if (cur == null) {
throw new NoSuchElementException();
}
var curVal = cur.getValue().next();
advanceIterator(cur.getValue());
// Log.tracev("{0} Read from {1}: {2}, next: {3}", _name, cur.getValue(), curVal, _sortedIterators.keySet());
return curVal;
}
@Override
public void close() {
for (CloseableKvIterator<K, V> iterator : _iterators.keySet()) {
iterator.close();
}
}
@Override
public String toString() {
return "MergingKvIterator{" +
"_name='" + _name + '\'' +
", _sortedIterators=" + _sortedIterators.keySet() +
", _iterators=" + _iterators +
'}';
}
}

View File

@@ -1,230 +0,0 @@
package com.usatiuk.dhfs.objects.persistence;
import com.usatiuk.dhfs.objects.*;
import com.usatiuk.dhfs.utils.DataLocker;
import io.quarkus.logging.Log;
import io.quarkus.runtime.Startup;
import jakarta.enterprise.context.ApplicationScoped;
import jakarta.inject.Inject;
import org.apache.commons.lang3.tuple.Pair;
import org.eclipse.microprofile.config.inject.ConfigProperty;
import org.pcollections.TreePMap;
import javax.annotation.Nonnull;
import java.util.LinkedHashMap;
import java.util.Optional;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.locks.ReentrantReadWriteLock;
@ApplicationScoped
public class CachingObjectPersistentStore {
private final LinkedHashMap<JObjectKey, CacheEntry> _cache = new LinkedHashMap<>();
private TreePMap<JObjectKey, CacheEntry> _sortedCache = TreePMap.empty();
private long _cacheVersion = 0;
private final ReentrantReadWriteLock _lock = new ReentrantReadWriteLock();
private final DataLocker _readerLocker = new DataLocker();
@Inject
SerializingObjectPersistentStore delegate;
@ConfigProperty(name = "dhfs.objects.lru.limit")
long sizeLimit;
@ConfigProperty(name = "dhfs.objects.lru.print-stats")
boolean printStats;
private long _curSize = 0;
private long _evict = 0;
private ExecutorService _statusExecutor = null;
@Startup
void init() {
if (printStats) {
_statusExecutor = Executors.newSingleThreadExecutor();
_statusExecutor.submit(() -> {
try {
while (true) {
Thread.sleep(10000);
if (_curSize > 0)
Log.info("Cache status: size=" + _curSize / 1024 / 1024 + "MB" + " evicted=" + _evict);
_evict = 0;
}
} catch (InterruptedException ignored) {
}
});
}
}
private void put(JObjectKey key, Optional<JDataVersionedWrapper> obj) {
// Log.tracev("Adding {0} to cache: {1}", key, obj);
_lock.writeLock().lock();
try {
int size = obj.map(JDataVersionedWrapper::estimateSize).orElse(16);
_curSize += size;
var entry = new CacheEntry(obj.<MaybeTombstone<JDataVersionedWrapper>>map(Data::new).orElse(new Tombstone<>()), size);
var old = _cache.putLast(key, entry);
_sortedCache = _sortedCache.plus(key, entry);
if (old != null)
_curSize -= old.size();
while (_curSize >= sizeLimit) {
var del = _cache.pollFirstEntry();
_sortedCache = _sortedCache.minus(del.getKey());
_curSize -= del.getValue().size();
_evict++;
}
} finally {
_lock.writeLock().unlock();
}
}
@Nonnull
public Optional<JDataVersionedWrapper> readObject(JObjectKey name) {
_lock.readLock().lock();
try {
var got = _cache.get(name);
if (got != null) {
return got.object().opt();
}
} finally {
_lock.readLock().unlock();
}
try (var lock = _readerLocker.lock(name)) {
// TODO: This is possibly racy
// var got = delegate.readObject(name);
// put(name, got);
return delegate.readObject(name);
}
}
public void commitTx(TxManifestObj<? extends JDataVersionedWrapper> names, long txId) {
var serialized = delegate.prepareManifest(names);
Log.tracev("Committing: {0} writes, {1} deletes", names.written().size(), names.deleted().size());
delegate.commitTx(serialized, txId, (commit) -> {
_lock.writeLock().lock();
try {
// Make the changes visible atomically both in cache and in the underlying store
for (var write : names.written()) {
put(write.getLeft(), Optional.of(write.getRight()));
}
for (var del : names.deleted()) {
put(del, Optional.empty());
}
++_cacheVersion;
commit.run();
} finally {
_lock.writeLock().unlock();
}
});
Log.tracev("Committed: {0} writes, {1} deletes", names.written().size(), names.deleted().size());
}
private class CachingKvIterator implements CloseableKvIterator<JObjectKey, JDataVersionedWrapper> {
private final CloseableKvIterator<JObjectKey, JDataVersionedWrapper> _delegate;
// This should be created under lock
private final long _curCacheVersion = _cacheVersion;
private CachingKvIterator(CloseableKvIterator<JObjectKey, JDataVersionedWrapper> delegate) {
_delegate = delegate;
}
@Override
public JObjectKey peekNextKey() {
return _delegate.peekNextKey();
}
@Override
public void skip() {
_delegate.skip();
}
@Override
public void close() {
_delegate.close();
}
@Override
public boolean hasNext() {
return _delegate.hasNext();
}
@Override
public JObjectKey peekPrevKey() {
return _delegate.peekPrevKey();
}
private void maybeCache(Pair<JObjectKey, JDataVersionedWrapper> prev) {
_lock.writeLock().lock();
try {
if (_cacheVersion != _curCacheVersion) {
Log.tracev("Not caching: {0}", prev);
} else {
Log.tracev("Caching: {0}", prev);
put(prev.getKey(), Optional.of(prev.getValue()));
}
} finally {
_lock.writeLock().unlock();
}
}
@Override
public Pair<JObjectKey, JDataVersionedWrapper> prev() {
var prev = _delegate.prev();
maybeCache(prev);
return prev;
}
@Override
public boolean hasPrev() {
return _delegate.hasPrev();
}
@Override
public void skipPrev() {
_delegate.skipPrev();
}
@Override
public Pair<JObjectKey, JDataVersionedWrapper> next() {
var next = _delegate.next();
maybeCache(next);
return next;
}
}
// Returns an iterator with a view of all commited objects
// Does not have to guarantee consistent view, snapshots are handled by upper layers
// Warning: it has a nasty side effect of global caching, so in this case don't even call next on it,
// if some objects are still in writeback
public CloseableKvIterator<JObjectKey, MaybeTombstone<JDataVersionedWrapper>> getIterator(IteratorStart start, JObjectKey key) {
_lock.readLock().lock();
try {
Log.tracev("Getting cache iterator: {0}, {1}", start, key);
var curSortedCache = _sortedCache;
return new MergingKvIterator<>("cache", start, key,
(mS, mK)
-> new MappingKvIterator<>(
new NavigableMapKvIterator<>(curSortedCache, mS, mK),
e -> {
Log.tracev("Taken from cache: {0}", e);
return e.object();
}
),
(mS, mK)
-> new MappingKvIterator<>(new CachingKvIterator(delegate.getIterator(mS, mK)), Data::new));
} finally {
_lock.readLock().unlock();
}
}
private record CacheEntry(MaybeTombstone<JDataVersionedWrapper> object, long size) {
}
public long getLastTxId() {
return delegate.getLastCommitId();
}
}

View File

@@ -1,56 +0,0 @@
package com.usatiuk.dhfs.objects.persistence;
import com.usatiuk.dhfs.objects.*;
import jakarta.enterprise.context.ApplicationScoped;
import jakarta.inject.Inject;
import org.apache.commons.lang3.tuple.Pair;
import javax.annotation.Nonnull;
import java.util.Collection;
import java.util.Optional;
import java.util.function.Consumer;
@ApplicationScoped
public class SerializingObjectPersistentStore {
@Inject
ObjectSerializer<JDataVersionedWrapper> serializer;
@Inject
ObjectPersistentStore delegateStore;
@Nonnull
Collection<JObjectKey> findAllObjects() {
return delegateStore.findAllObjects();
}
@Nonnull
Optional<JDataVersionedWrapper> readObject(JObjectKey name) {
return delegateStore.readObject(name).map(serializer::deserialize);
}
// Returns an iterator with a view of all commited objects
// Does not have to guarantee consistent view, snapshots are handled by upper layers
public CloseableKvIterator<JObjectKey, JDataVersionedWrapper> getIterator(IteratorStart start, JObjectKey key) {
return new MappingKvIterator<>(delegateStore.getIterator(start, key), d -> serializer.deserialize(d));
}
public TxManifestRaw prepareManifest(TxManifestObj<? extends JDataVersionedWrapper> names) {
return new TxManifestRaw(
names.written().stream()
.map(e -> Pair.of(e.getKey(), serializer.serialize(e.getValue())))
.toList()
, names.deleted());
}
// void commitTx(TxManifestObj<? extends JDataVersionedWrapper> names, Consumer<Runnable> commitLocked) {
// delegateStore.commitTx(prepareManifest(names), commitLocked);
// }
void commitTx(TxManifestRaw names, long txId, Consumer<Runnable> commitLocked) {
delegateStore.commitTx(names, txId, commitLocked);
}
long getLastCommitId() {
return delegateStore.getLastCommitId();
}
}

View File

@@ -1,342 +0,0 @@
package com.usatiuk.dhfs.objects.snapshot;
import com.usatiuk.dhfs.objects.*;
import com.usatiuk.dhfs.objects.persistence.IteratorStart;
import com.usatiuk.dhfs.objects.transaction.TxRecord;
import com.usatiuk.dhfs.utils.AutoCloseableNoThrow;
import io.quarkus.logging.Log;
import jakarta.enterprise.context.ApplicationScoped;
import jakarta.inject.Inject;
import org.apache.commons.lang3.mutable.MutableObject;
import org.apache.commons.lang3.tuple.Pair;
import org.eclipse.microprofile.config.inject.ConfigProperty;
import org.pcollections.TreePMap;
import javax.annotation.Nonnull;
import java.lang.ref.Cleaner;
import java.util.*;
import java.util.concurrent.locks.ReentrantReadWriteLock;
import java.util.function.Consumer;
@ApplicationScoped
public class SnapshotManager {
@Inject
WritebackObjectPersistentStore writebackStore;
private final ReentrantReadWriteLock _lock = new ReentrantReadWriteLock();
@ConfigProperty(name = "dhfs.objects.persistence.snapshot-extra-checks")
boolean extraChecks;
private long _lastSnapshotId = 0;
private long _lastAliveSnapshotId = -1;
private final Queue<Long> _snapshotIds = new ArrayDeque<>();
private TreePMap<SnapshotKey, SnapshotEntry> _objects = TreePMap.empty();
private final TreeMap<Long, ArrayDeque<SnapshotKey>> _snapshotBounds = new TreeMap<>();
private final HashMap<Long, Long> _snapshotRefCounts = new HashMap<>();
private void verify() {
assert _snapshotIds.isEmpty() == (_lastAliveSnapshotId == -1);
assert _snapshotIds.isEmpty() || _snapshotIds.peek() == _lastAliveSnapshotId;
}
// This should not be called for the same objects concurrently
public Consumer<Runnable> commitTx(Collection<TxRecord.TxObjectRecord<?>> writes) {
// _lock.writeLock().lock();
// try {
// if (!_snapshotIds.isEmpty()) {
// verify();
HashMap<SnapshotKey, SnapshotEntry> newEntries = new HashMap<>();
for (var action : writes) {
var current = writebackStore.readObjectVerbose(action.key());
// Add to snapshot the previous visible version of the replaced object
// I.e. should be visible to all transactions with id <= id
// and at least as its corresponding version
Pair<SnapshotKey, SnapshotEntry> newSnapshotEntry = switch (current) {
case WritebackObjectPersistentStore.VerboseReadResultPersisted(
Optional<JDataVersionedWrapper> data
) -> Pair.of(new SnapshotKey(action.key(), data.map(JDataVersionedWrapper::version).orElse(-1L)),
data.<SnapshotEntry>map(o -> new SnapshotEntryObject(o, -1)).orElse(new SnapshotEntryDeleted(-1)));
case WritebackObjectPersistentStore.VerboseReadResultPending(
PendingWriteEntry pending
) -> {
yield switch (pending) {
case PendingWrite write ->
Pair.of(new SnapshotKey(action.key(), write.bundleId()), new SnapshotEntryObject(write.data(), -1));
case PendingDelete delete ->
Pair.of(new SnapshotKey(action.key(), delete.bundleId()), new SnapshotEntryDeleted(-1));
default -> throw new IllegalStateException("Unexpected value: " + pending);
};
}
default -> throw new IllegalStateException("Unexpected value: " + current);
};
Log.tracev("Adding snapshot entry {0}", newSnapshotEntry);
newEntries.put(newSnapshotEntry.getLeft(), newSnapshotEntry.getRight());
}
_lock.writeLock().lock();
try {
return writebackStore.commitTx(writes, (id, commit) -> {
if (!_snapshotIds.isEmpty()) {
assert id > _lastSnapshotId;
for (var newSnapshotEntry : newEntries.entrySet()) {
assert newSnapshotEntry.getKey().version() < id;
var realNewSnapshotEntry = newSnapshotEntry.getValue().withWhenToRemove(id);
if (realNewSnapshotEntry instanceof SnapshotEntryObject re) {
assert re.data().version() <= newSnapshotEntry.getKey().version();
}
_objects = _objects.plus(newSnapshotEntry.getKey(), realNewSnapshotEntry);
// assert val == null;
_snapshotBounds.merge(newSnapshotEntry.getKey().version(), new ArrayDeque<>(List.of(newSnapshotEntry.getKey())),
(a, b) -> {
a.addAll(b);
return a;
});
}
}
commit.run();
});
} finally {
_lock.writeLock().unlock();
}
// }
// verify();
// Commit under lock, iterators will see new version after the lock is released and writeback
// cache is updated
// TODO: Maybe writeback iterator being invalidated wouldn't be a problem?
// } finally {
// _lock.writeLock().unlock();
// }
}
private void unrefSnapshot(long id) {
Log.tracev("Unref snapshot {0}", id);
_lock.writeLock().lock();
try {
verify();
var refCount = _snapshotRefCounts.merge(id, -1L, (a, b) -> a + b == 0 ? null : a + b);
if (!(refCount == null && id == _lastAliveSnapshotId)) {
return;
}
long curCount;
long curId = id;
long nextId;
do {
Log.tracev("Removing snapshot {0}", curId);
_snapshotIds.poll();
nextId = _snapshotIds.isEmpty() ? -1 : _snapshotIds.peek();
while (nextId == curId) {
_snapshotIds.poll();
nextId = _snapshotIds.isEmpty() ? -1 : _snapshotIds.peek();
}
var keys = _snapshotBounds.headMap(curId, true);
long finalCurId = curId;
long finalNextId = nextId;
ArrayList<Pair<Long, SnapshotKey>> toReAdd = new ArrayList<>();
keys.values().stream().flatMap(Collection::stream).forEach(key -> {
var entry = _objects.get(key);
if (entry == null) {
// Log.warnv("Entry not found for key {0}", key);
return;
}
if (finalNextId == -1) {
Log.tracev("Could not find place to place entry {0}, curId={1}, nextId={2}, whenToRemove={3}, snapshotIds={4}",
entry, finalCurId, finalNextId, entry.whenToRemove(), _snapshotIds);
} else if (finalNextId < entry.whenToRemove()) {
_objects = _objects.plus(new SnapshotKey(key.key(), finalNextId), entry);
assert finalNextId > finalCurId;
toReAdd.add(Pair.of(finalNextId, new SnapshotKey(key.key(), finalNextId)));
}
_objects = _objects.minus(key);
});
toReAdd.forEach(p -> {
_snapshotBounds.merge(p.getLeft(), new ArrayDeque<>(List.of(p.getRight())),
(a, b) -> {
a.addAll(b);
return a;
});
});
keys.clear();
if (_snapshotIds.isEmpty()) {
_lastAliveSnapshotId = -1;
break;
}
curId = _snapshotIds.peek();
_lastAliveSnapshotId = curId;
curCount = _snapshotRefCounts.getOrDefault(curId, 0L);
} while (curCount == 0);
verify();
} finally {
_lock.writeLock().unlock();
}
}
public static class IllegalSnapshotIdException extends IllegalArgumentException {
public IllegalSnapshotIdException(String message) {
super(message);
}
@Override
public synchronized Throwable fillInStackTrace() {
return this;
}
}
public class Snapshot implements AutoCloseableNoThrow {
private final long _id;
private static final Cleaner CLEANER = Cleaner.create();
private final MutableObject<Boolean> _closed = new MutableObject<>(false);
public long id() {
return _id;
}
private Snapshot(long id) {
_id = id;
_lock.writeLock().lock();
try {
verify();
if (_lastSnapshotId > id)
throw new IllegalSnapshotIdException("Snapshot id " + id + " is less than last snapshot id " + _lastSnapshotId);
_lastSnapshotId = id;
if (_lastAliveSnapshotId == -1)
_lastAliveSnapshotId = id;
if (_snapshotRefCounts.merge(id, 1L, Long::sum) == 1) {
_snapshotIds.add(id);
}
verify();
} finally {
_lock.writeLock().unlock();
}
var closedRef = _closed;
var idRef = _id;
CLEANER.register(this, () -> {
if (!closedRef.getValue()) {
Log.error("Snapshot " + idRef + " was not closed before GC");
}
});
}
public class CheckingSnapshotKvIterator implements CloseableKvIterator<JObjectKey, JDataVersionedWrapper> {
private final CloseableKvIterator<JObjectKey, JDataVersionedWrapper> _backing;
public CheckingSnapshotKvIterator(CloseableKvIterator<JObjectKey, JDataVersionedWrapper> backing) {
_backing = backing;
}
@Override
public JObjectKey peekNextKey() {
return _backing.peekNextKey();
}
@Override
public void skip() {
_backing.skip();
}
@Override
public JObjectKey peekPrevKey() {
return _backing.peekPrevKey();
}
@Override
public Pair<JObjectKey, JDataVersionedWrapper> prev() {
var ret = _backing.prev();
assert ret.getValue().version() <= _id;
return ret;
}
@Override
public boolean hasPrev() {
return _backing.hasPrev();
}
@Override
public void skipPrev() {
_backing.skipPrev();
}
@Override
public void close() {
_backing.close();
}
@Override
public boolean hasNext() {
return _backing.hasNext();
}
@Override
public Pair<JObjectKey, JDataVersionedWrapper> next() {
var ret = _backing.next();
assert ret.getValue().version() <= _id;
return ret;
}
}
public CloseableKvIterator<JObjectKey, JDataVersionedWrapper> getIterator(IteratorStart start, JObjectKey key) {
_lock.readLock().lock();
try {
Log.tracev("Getting snapshot {0} iterator for {1} {2}\n" +
"objects in snapshots: {3}", _id, start, key, _objects);
return new CheckingSnapshotKvIterator(new TombstoneMergingKvIterator<>("snapshot", start, key,
(tS, tK) -> new SnapshotKvIterator(_objects, _id, tS, tK),
(tS, tK) -> new MappingKvIterator<>(
writebackStore.getIterator(tS, tK), d -> d.version() <= _id ? new Data<>(d) : new Tombstone<>())
));
} finally {
_lock.readLock().unlock();
}
}
@Nonnull
public Optional<JDataVersionedWrapper> readObject(JObjectKey name) {
try (var it = getIterator(IteratorStart.GE, name)) {
if (it.hasNext()) {
if (!it.peekNextKey().equals(name)) {
return Optional.empty();
}
return Optional.of(it.next().getValue());
}
}
return Optional.empty();
}
@Override
public void close() {
if (_closed.getValue()) {
return;
}
_closed.setValue(true);
unrefSnapshot(_id);
}
}
public Snapshot createSnapshot() {
_lock.writeLock().lock();
try {
return new Snapshot(writebackStore.getLastTxId());
} finally {
_lock.writeLock().unlock();
}
}
@Nonnull
public Optional<JDataVersionedWrapper> readObjectDirect(JObjectKey name) {
return writebackStore.readObject(name);
}
}

View File

@@ -1,10 +0,0 @@
package com.usatiuk.dhfs.objects.transaction;
import com.usatiuk.dhfs.objects.JData;
import com.usatiuk.dhfs.objects.JDataVersionedWrapper;
import java.util.Optional;
public interface TransactionObject<T extends JData> {
Optional<JDataVersionedWrapper> data();
}

View File

@@ -1,4 +1,4 @@
package com.usatiuk.dhfs.objects;
package com.usatiuk.objects;
import java.io.Serializable;

View File

@@ -1,4 +1,4 @@
package com.usatiuk.dhfs.objects;
package com.usatiuk.objects;
public interface JDataVersionedWrapper {
JData data();

View File

@@ -1,4 +1,4 @@
package com.usatiuk.dhfs.objects;
package com.usatiuk.objects;
import jakarta.annotation.Nonnull;

View File

@@ -0,0 +1,39 @@
package com.usatiuk.objects;
import java.util.function.Supplier;
public class JDataVersionedWrapperLazy implements JDataVersionedWrapper {
private final long _version;
private final int _estimatedSize;
private Supplier<JData> _producer;
private JData _data;
public JDataVersionedWrapperLazy(long version, int estimatedSize, Supplier<JData> producer) {
_version = version;
_estimatedSize = estimatedSize;
_producer = producer;
}
public JData data() {
if (_data != null)
return _data;
synchronized (this) {
if (_data != null)
return _data;
_data = _producer.get();
_producer = null;
return _data;
}
}
public long version() {
return _version;
}
@Override
public int estimateSize() {
return _estimatedSize;
}
}

View File

@@ -1,26 +1,29 @@
package com.usatiuk.dhfs.objects;
package com.usatiuk.objects;
import com.google.protobuf.ByteString;
import com.usatiuk.dhfs.utils.SerializationHelper;
import jakarta.enterprise.context.ApplicationScoped;
import jakarta.inject.Inject;
import java.nio.ByteBuffer;
@ApplicationScoped
public class JavaDataSerializer implements ObjectSerializer<JDataVersionedWrapper> {
public class JDataVersionedWrapperSerializer implements ObjectSerializer<JDataVersionedWrapper> {
@Inject
ObjectSerializer<JData> dataSerializer;
@Override
public ByteString serialize(JDataVersionedWrapper obj) {
ByteBuffer buffer = ByteBuffer.allocate(Long.BYTES);
buffer.putLong(obj.version());
buffer.flip();
return ByteString.copyFrom(buffer).concat(SerializationHelper.serialize(obj.data()));
return ByteString.copyFrom(buffer).concat(dataSerializer.serialize(obj.data()));
}
@Override
public JDataVersionedWrapper deserialize(ByteString data) {
var version = data.substring(0, Long.BYTES).asReadOnlyByteBuffer().getLong();
var rawData = data.substring(Long.BYTES);
return new JDataVersionedWrapperLazy(version, rawData);
return new JDataVersionedWrapperLazy(version, rawData.size(), () -> dataSerializer.deserialize(rawData));
}
}

View File

@@ -1,16 +1,33 @@
package com.usatiuk.dhfs.objects;
package com.usatiuk.objects;
import com.usatiuk.dhfs.supportlib.UninitializedByteBuffer;
import java.io.Serializable;
import java.nio.ByteBuffer;
import java.nio.charset.StandardCharsets;
import java.util.UUID;
public record JObjectKey(String name) implements Serializable, Comparable<JObjectKey> {
public static JObjectKey of(String name) {
return new JObjectKey(name);
}
public static JObjectKey random() {
return new JObjectKey(UUID.randomUUID().toString());
}
public static JObjectKey first() {
return new JObjectKey("");
}
public static JObjectKey fromBytes(byte[] bytes) {
return new JObjectKey(new String(bytes, StandardCharsets.UTF_8));
}
public static JObjectKey fromByteBuffer(ByteBuffer buff) {
return new JObjectKey(StandardCharsets.UTF_8.decode(buff).toString());
}
@Override
public int compareTo(JObjectKey o) {
return name.compareTo(o.name);
@@ -33,12 +50,4 @@ public record JObjectKey(String name) implements Serializable, Comparable<JObjec
directBb.flip();
return directBb;
}
public static JObjectKey fromBytes(byte[] bytes) {
return new JObjectKey(new String(bytes, StandardCharsets.UTF_8));
}
public static JObjectKey fromByteBuffer(ByteBuffer buff) {
return new JObjectKey(StandardCharsets.UTF_8.decode(buff).toString());
}
}

View File

@@ -0,0 +1,27 @@
package com.usatiuk.objects;
import com.google.protobuf.ByteString;
import com.usatiuk.dhfs.utils.SerializationHelper;
import io.quarkus.arc.DefaultBean;
import jakarta.enterprise.context.ApplicationScoped;
import java.io.IOException;
@ApplicationScoped
@DefaultBean
public class JavaDataSerializer implements ObjectSerializer<JData> {
@Override
public ByteString serialize(JData obj) {
return SerializationHelper.serialize(obj);
}
@Override
public JData deserialize(ByteString data) {
try (var is = data.newInput()) {
return SerializationHelper.deserialize(is);
} catch (IOException e) {
throw new RuntimeException(e);
}
}
}

View File

@@ -1,4 +1,4 @@
package com.usatiuk.dhfs.objects;
package com.usatiuk.objects;
import com.google.protobuf.ByteString;

View File

@@ -1,11 +1,11 @@
package com.usatiuk.dhfs.objects;
package com.usatiuk.objects.iterators;
import com.usatiuk.dhfs.utils.AutoCloseableNoThrow;
import org.apache.commons.lang3.tuple.Pair;
import java.util.Iterator;
public interface CloseableKvIterator<K extends Comparable<K>, V> extends Iterator<Pair<K, V>>, AutoCloseableNoThrow {
public interface CloseableKvIterator<K extends Comparable<? super K>, V> extends Iterator<Pair<K, V>>, AutoCloseableNoThrow {
K peekNextKey();
void skip();
@@ -19,6 +19,6 @@ public interface CloseableKvIterator<K extends Comparable<K>, V> extends Iterato
void skipPrev();
default CloseableKvIterator<K, V> reversed() {
return new ReversedKvIterator<>(this);
return new ReversedKvIterator<K, V>(this);
}
}

View File

@@ -1,4 +1,4 @@
package com.usatiuk.dhfs.objects;
package com.usatiuk.objects.iterators;
import java.util.Optional;

View File

@@ -1,6 +1,4 @@
package com.usatiuk.dhfs.objects;
import com.usatiuk.dhfs.objects.persistence.IteratorStart;
package com.usatiuk.objects.iterators;
@FunctionalInterface
public interface IterProdFn<K extends Comparable<K>, V> {

View File

@@ -1,4 +1,4 @@
package com.usatiuk.dhfs.objects.persistence;
package com.usatiuk.objects.iterators;
public enum IteratorStart {
LT,

View File

@@ -1,6 +1,5 @@
package com.usatiuk.dhfs.objects;
package com.usatiuk.objects.iterators;
import com.usatiuk.dhfs.objects.persistence.IteratorStart;
import org.apache.commons.lang3.tuple.Pair;
import java.util.NoSuchElementException;

View File

@@ -1,4 +1,4 @@
package com.usatiuk.dhfs.objects;
package com.usatiuk.objects.iterators;
import org.apache.commons.lang3.tuple.Pair;

View File

@@ -1,4 +1,4 @@
package com.usatiuk.dhfs.objects;
package com.usatiuk.objects.iterators;
import java.util.Optional;

View File

@@ -0,0 +1,307 @@
package com.usatiuk.objects.iterators;
import io.quarkus.logging.Log;
import org.apache.commons.lang3.tuple.Pair;
import java.util.*;
import java.util.stream.Collectors;
public class MergingKvIterator<K extends Comparable<K>, V> extends ReversibleKvIterator<K, V> {
private final NavigableMap<K, CloseableKvIterator<K, V>> _sortedIterators = new TreeMap<>();
private final String _name;
private final IteratorStart _initialStartType;
private final K _initialStartKey;
private final List<IterProdFn<K, V>> _pendingIterators;
private Map<CloseableKvIterator<K, V>, Integer> _iterators;
// Fast path for the first element
private FirstMatchState<K, V> _firstMatchState;
public MergingKvIterator(String name, IteratorStart startType, K startKey, List<IterProdFn<K, V>> iterators) {
_goingForward = true;
_name = name;
_initialStartType = startType;
_initialStartKey = startKey;
{
int counter = 0;
var iteratorsTmp = new HashMap<CloseableKvIterator<K, V>, Integer>();
for (var iteratorFn : iterators) {
var iterator = iteratorFn.get(startType, startKey);
if ((counter == 0) // Not really a requirement but simplifies some things for now
&& (startType == IteratorStart.GE || startType == IteratorStart.LE)
&& iterator.hasNext()
&& iterator.peekNextKey().equals(startKey)) {
_firstMatchState = new FirstMatchFound<>(iterator);
_pendingIterators = iterators;
Log.tracev("{0} Created fast match: {1}", _name, _firstMatchState);
return;
}
iteratorsTmp.put(iterator, counter++);
}
_iterators = Map.copyOf(iteratorsTmp);
_pendingIterators = null;
}
_firstMatchState = new FirstMatchNone<>();
doInitialAdvance();
}
@SafeVarargs
public MergingKvIterator(String name, IteratorStart startType, K startKey, IterProdFn<K, V>... iterators) {
this(name, startType, startKey, List.of(iterators));
}
private void doInitialAdvance() {
if (_initialStartType == IteratorStart.LT || _initialStartType == IteratorStart.LE) {
// Starting at a greatest key less than/less or equal than:
// We have a bunch of iterators that have given us theirs "greatest LT/LE key"
// now we need to pick the greatest of those to start with
// But if some of them don't have a lesser key, we need to pick the smallest of those
var found = _iterators.keySet().stream()
.filter(CloseableKvIterator::hasNext)
.map((i) -> {
var peeked = i.peekNextKey();
// Log.warnv("peeked: {0}, from {1}", peeked, i.getClass());
return peeked;
}).distinct().collect(Collectors.partitioningBy(e -> _initialStartType == IteratorStart.LE ? e.compareTo(_initialStartKey) <= 0 : e.compareTo(_initialStartKey) < 0));
K initialMaxValue;
if (!found.get(true).isEmpty())
initialMaxValue = found.get(true).stream().max(Comparator.naturalOrder()).orElse(null);
else
initialMaxValue = found.get(false).stream().min(Comparator.naturalOrder()).orElse(null);
for (var iterator : _iterators.keySet()) {
while (iterator.hasNext() && iterator.peekNextKey().compareTo(initialMaxValue) < 0) {
iterator.skip();
}
}
}
for (CloseableKvIterator<K, V> iterator : _iterators.keySet()) {
advanceIterator(iterator);
}
Log.tracev("{0} Initialized: {1}", _name, _sortedIterators);
switch (_initialStartType) {
// case LT -> {
// assert _sortedIterators.isEmpty() || _sortedIterators.firstKey().compareTo(initialStartKey) < 0;
// }
// case LE -> {
// assert _sortedIterators.isEmpty() || _sortedIterators.firstKey().compareTo(initialStartKey) <= 0;
// }
case GT -> {
assert _sortedIterators.isEmpty() || _sortedIterators.firstKey().compareTo(_initialStartKey) > 0;
}
case GE -> {
assert _sortedIterators.isEmpty() || _sortedIterators.firstKey().compareTo(_initialStartKey) >= 0;
}
}
}
private void doHydrate() {
if (_firstMatchState instanceof FirstMatchNone) {
return;
}
boolean consumed = _firstMatchState instanceof FirstMatchConsumed;
if (_firstMatchState instanceof FirstMatchFound(CloseableKvIterator iterator)) {
iterator.close();
}
_firstMatchState = new FirstMatchNone<>();
{
int counter = 0;
var iteratorsTmp = new HashMap<CloseableKvIterator<K, V>, Integer>();
for (var iteratorFn : _pendingIterators) {
var iterator = iteratorFn.get(consumed ? IteratorStart.GT : IteratorStart.GE, _initialStartKey);
iteratorsTmp.put(iterator, counter++);
}
_iterators = Map.copyOf(iteratorsTmp);
}
doInitialAdvance();
}
private void advanceIterator(CloseableKvIterator<K, V> iterator) {
if (!iterator.hasNext()) {
return;
}
K key = iterator.peekNextKey();
Log.tracev("{0} Advance peeked: {1}-{2}", _name, iterator, key);
if (!_sortedIterators.containsKey(key)) {
_sortedIterators.put(key, iterator);
return;
}
// Expects that reversed iterator returns itself when reversed again
var oursPrio = _iterators.get(_goingForward ? iterator : iterator.reversed());
var them = _sortedIterators.get(key);
var theirsPrio = _iterators.get(_goingForward ? them : them.reversed());
if (oursPrio < theirsPrio) {
_sortedIterators.put(key, iterator);
advanceIterator(them);
} else {
Log.tracev("{0} Skipped: {1}", _name, iterator.peekNextKey());
iterator.skip();
advanceIterator(iterator);
}
}
@Override
protected void reverse() {
switch (_firstMatchState) {
case FirstMatchFound<K, V> firstMatchFound -> {
doHydrate();
}
case FirstMatchConsumed<K, V> firstMatchConsumed -> {
doHydrate();
}
default -> {
}
}
var cur = _goingForward ? _sortedIterators.pollFirstEntry() : _sortedIterators.pollLastEntry();
Log.tracev("{0} Reversing from {1}", _name, cur);
_goingForward = !_goingForward;
_sortedIterators.clear();
for (CloseableKvIterator<K, V> iterator : _iterators.keySet()) {
// _goingForward inverted already
advanceIterator(!_goingForward ? iterator.reversed() : iterator);
}
if (_sortedIterators.isEmpty() || cur == null) {
return;
}
// Advance to the expected key, as we might have brought back some iterators
// that were at their ends
while (!_sortedIterators.isEmpty()
&& ((_goingForward && peekImpl().compareTo(cur.getKey()) <= 0)
|| (!_goingForward && peekImpl().compareTo(cur.getKey()) >= 0))) {
skipImpl();
}
Log.tracev("{0} Reversed to {1}", _name, _sortedIterators);
}
@Override
protected K peekImpl() {
switch (_firstMatchState) {
case FirstMatchFound<K, V> firstMatchFound -> {
return firstMatchFound.iterator.peekNextKey();
}
case FirstMatchConsumed<K, V> firstMatchConsumed -> {
doHydrate();
break;
}
default -> {
}
}
if (_sortedIterators.isEmpty())
throw new NoSuchElementException();
return _goingForward ? _sortedIterators.firstKey() : _sortedIterators.lastKey();
}
@Override
protected void skipImpl() {
switch (_firstMatchState) {
case FirstMatchFound<K, V> firstMatchFound -> {
var curVal = firstMatchFound.iterator.next();
firstMatchFound.iterator.close();
_firstMatchState = new FirstMatchConsumed<>();
// Log.tracev("{0} Read from {1}: {2}, next: {3}", _name, firstMatchFound.iterator, curVal, _sortedIterators.keySet());
return;
}
case FirstMatchConsumed<K, V> firstMatchConsumed -> {
doHydrate();
break;
}
default -> {
}
}
var cur = _goingForward ? _sortedIterators.pollFirstEntry() : _sortedIterators.pollLastEntry();
if (cur == null) {
throw new NoSuchElementException();
}
cur.getValue().skip();
advanceIterator(cur.getValue());
Log.tracev("{0} Skip: {1}, next: {2}", _name, cur, _sortedIterators);
}
@Override
protected boolean hasImpl() {
switch (_firstMatchState) {
case FirstMatchFound<K, V> firstMatchFound -> {
return true;
}
case FirstMatchConsumed<K, V> firstMatchConsumed -> {
doHydrate();
break;
}
default -> {
}
}
return !_sortedIterators.isEmpty();
}
@Override
protected Pair<K, V> nextImpl() {
switch (_firstMatchState) {
case FirstMatchFound<K, V> firstMatchFound -> {
var curVal = firstMatchFound.iterator.next();
firstMatchFound.iterator.close();
_firstMatchState = new FirstMatchConsumed<>();
// Log.tracev("{0} Read from {1}: {2}, next: {3}", _name, firstMatchFound.iterator, curVal, _sortedIterators.keySet());
return curVal;
}
case FirstMatchConsumed<K, V> firstMatchConsumed -> {
doHydrate();
break;
}
default -> {
}
}
var cur = _goingForward ? _sortedIterators.pollFirstEntry() : _sortedIterators.pollLastEntry();
if (cur == null) {
throw new NoSuchElementException();
}
var curVal = cur.getValue().next();
advanceIterator(cur.getValue());
// Log.tracev("{0} Read from {1}: {2}, next: {3}", _name, cur.getValue(), curVal, _sortedIterators.keySet());
return curVal;
}
@Override
public void close() {
if (_firstMatchState instanceof FirstMatchFound(CloseableKvIterator iterator)) {
iterator.close();
}
for (CloseableKvIterator<K, V> iterator : _iterators.keySet()) {
iterator.close();
}
}
@Override
public String toString() {
return "MergingKvIterator{" +
"_name='" + _name + '\'' +
", _sortedIterators=" + _sortedIterators.keySet() +
", _iterators=" + _iterators +
'}';
}
private interface FirstMatchState<K extends Comparable<K>, V> {
}
private record FirstMatchNone<K extends Comparable<K>, V>() implements FirstMatchState<K, V> {
}
private record FirstMatchFound<K extends Comparable<K>, V>(
CloseableKvIterator<K, V> iterator) implements FirstMatchState<K, V> {
}
private record FirstMatchConsumed<K extends Comparable<K>, V>() implements FirstMatchState<K, V> {
}
}

View File

@@ -1,6 +1,5 @@
package com.usatiuk.dhfs.objects;
package com.usatiuk.objects.iterators;
import com.usatiuk.dhfs.objects.persistence.IteratorStart;
import org.apache.commons.lang3.tuple.Pair;
import java.util.*;

View File

@@ -1,6 +1,5 @@
package com.usatiuk.dhfs.objects;
package com.usatiuk.objects.iterators;
import com.usatiuk.dhfs.objects.persistence.IteratorStart;
import io.quarkus.logging.Log;
import org.apache.commons.lang3.tuple.Pair;
@@ -10,12 +9,17 @@ import java.util.function.Function;
public class PredicateKvIterator<K extends Comparable<K>, V, V_T> extends ReversibleKvIterator<K, V_T> {
private final CloseableKvIterator<K, V> _backing;
private final Function<V, V_T> _transformer;
private Pair<K, V_T> _next;
private Pair<K, V_T> _next = null;
private boolean _checkedNext = false;
public PredicateKvIterator(CloseableKvIterator<K, V> backing, IteratorStart start, K startKey, Function<V, V_T> transformer) {
_goingForward = true;
_backing = backing;
_transformer = transformer;
if (start == IteratorStart.GE || start == IteratorStart.GT)
return;
fillNext();
boolean shouldGoBack = false;
@@ -64,6 +68,7 @@ public class PredicateKvIterator<K extends Comparable<K>, V, V_T> extends Revers
continue;
_next = Pair.of(next.getKey(), transformed);
}
_checkedNext = true;
}
@Override
@@ -80,12 +85,14 @@ public class PredicateKvIterator<K extends Comparable<K>, V, V_T> extends Revers
Log.tracev("Skipped in reverse: {0}", _next);
_next = null;
fillNext();
_checkedNext = false;
}
@Override
protected K peekImpl() {
if (!_checkedNext)
fillNext();
if (_next == null)
throw new NoSuchElementException();
return _next.getKey();
@@ -93,24 +100,33 @@ public class PredicateKvIterator<K extends Comparable<K>, V, V_T> extends Revers
@Override
protected void skipImpl() {
if (!_checkedNext)
fillNext();
if (_next == null)
throw new NoSuchElementException();
_next = null;
fillNext();
_checkedNext = false;
}
@Override
protected boolean hasImpl() {
if (!_checkedNext)
fillNext();
return _next != null;
}
@Override
protected Pair<K, V_T> nextImpl() {
if (!_checkedNext)
fillNext();
if (_next == null)
throw new NoSuchElementException("No more elements");
var ret = _next;
_next = null;
fillNext();
_checkedNext = false;
return ret;
}

View File

@@ -1,8 +1,8 @@
package com.usatiuk.dhfs.objects;
package com.usatiuk.objects.iterators;
import org.apache.commons.lang3.tuple.Pair;
public class ReversedKvIterator<K extends Comparable<K>, V> implements CloseableKvIterator<K, V> {
public class ReversedKvIterator<K extends Comparable<? super K>, V> implements CloseableKvIterator<K, V> {
private final CloseableKvIterator<K, V> _backing;
public ReversedKvIterator(CloseableKvIterator<K, V> backing) {

View File

@@ -1,4 +1,4 @@
package com.usatiuk.dhfs.objects;
package com.usatiuk.objects.iterators;
import org.apache.commons.lang3.tuple.Pair;

View File

@@ -1,4 +1,4 @@
package com.usatiuk.dhfs.objects;
package com.usatiuk.objects.iterators;
import java.util.Optional;

View File

@@ -1,6 +1,5 @@
package com.usatiuk.dhfs.objects;
package com.usatiuk.objects.iterators;
import com.usatiuk.dhfs.objects.persistence.IteratorStart;
import io.quarkus.logging.Log;
import org.apache.commons.lang3.tuple.Pair;

View File

@@ -0,0 +1,18 @@
package com.usatiuk.objects.snapshot;
import com.usatiuk.objects.JObjectKey;
import com.usatiuk.objects.iterators.CloseableKvIterator;
import com.usatiuk.objects.iterators.IteratorStart;
import com.usatiuk.dhfs.utils.AutoCloseableNoThrow;
import javax.annotation.Nonnull;
import java.util.Optional;
public interface Snapshot<K extends Comparable<K>, V> extends AutoCloseableNoThrow {
CloseableKvIterator<K, V> getIterator(IteratorStart start, K key);
@Nonnull
Optional<V> readObject(K name);
long id();
}

View File

@@ -1,4 +1,4 @@
package com.usatiuk.dhfs.objects.snapshot;
package com.usatiuk.objects.snapshot;
public interface SnapshotEntry {
long whenToRemove();

View File

@@ -1,4 +1,4 @@
package com.usatiuk.dhfs.objects.snapshot;
package com.usatiuk.objects.snapshot;
public record SnapshotEntryDeleted(long whenToRemove) implements SnapshotEntry {
@Override

View File

@@ -1,6 +1,6 @@
package com.usatiuk.dhfs.objects.snapshot;
package com.usatiuk.objects.snapshot;
import com.usatiuk.dhfs.objects.JDataVersionedWrapper;
import com.usatiuk.objects.JDataVersionedWrapper;
public record SnapshotEntryObject(JDataVersionedWrapper data, long whenToRemove) implements SnapshotEntry {
@Override

View File

@@ -1,6 +1,6 @@
package com.usatiuk.dhfs.objects.snapshot;
package com.usatiuk.objects.snapshot;
import com.usatiuk.dhfs.objects.JObjectKey;
import com.usatiuk.objects.JObjectKey;
import javax.annotation.Nonnull;
import java.util.Comparator;

View File

@@ -1,7 +1,9 @@
package com.usatiuk.dhfs.objects.snapshot;
package com.usatiuk.objects.snapshot;
import com.usatiuk.dhfs.objects.*;
import com.usatiuk.dhfs.objects.persistence.IteratorStart;
import com.usatiuk.objects.JDataVersionedWrapper;
import com.usatiuk.objects.JObjectKey;
import com.usatiuk.objects.iterators.*;
import com.usatiuk.objects.iterators.*;
import io.quarkus.logging.Log;
import org.apache.commons.lang3.tuple.Pair;
@@ -20,7 +22,12 @@ public class SnapshotKvIterator extends ReversibleKvIterator<JObjectKey, MaybeTo
_objects = objects;
_version = version;
_goingForward = true;
_backing = new NavigableMapKvIterator<>(_objects, start, new SnapshotKey(startKey, Long.MIN_VALUE));
if (start == IteratorStart.LT || start == IteratorStart.GE)
_backing = new NavigableMapKvIterator<>(_objects, start, new SnapshotKey(startKey, Long.MIN_VALUE));
else if (start == IteratorStart.GT || start == IteratorStart.LE)
_backing = new NavigableMapKvIterator<>(_objects, start, new SnapshotKey(startKey, Long.MAX_VALUE));
else
throw new UnsupportedOperationException();
fill();
boolean shouldGoBack = false;

View File

@@ -0,0 +1,38 @@
package com.usatiuk.objects.snapshot;
import com.usatiuk.objects.JDataVersionedWrapper;
import com.usatiuk.objects.JObjectKey;
import com.usatiuk.objects.stores.WritebackObjectPersistentStore;
import com.usatiuk.objects.transaction.TxRecord;
import jakarta.enterprise.context.ApplicationScoped;
import jakarta.inject.Inject;
import javax.annotation.Nonnull;
import java.util.Collection;
import java.util.Optional;
import java.util.function.Consumer;
@ApplicationScoped
public class SnapshotManager {
@Inject
WritebackObjectPersistentStore writebackStore;
public Snapshot<JObjectKey, JDataVersionedWrapper> createSnapshot() {
return writebackStore.getSnapshot();
}
// This should not be called for the same objects concurrently
public Consumer<Runnable> commitTx(Collection<TxRecord.TxObjectRecord<?>> writes) {
// TODO: FIXME:
synchronized (this) {
return writebackStore.commitTx(writes, (id, commit) -> {
commit.run();
});
}
}
@Nonnull
public Optional<JDataVersionedWrapper> readObjectDirect(JObjectKey name) {
return writebackStore.readObject(name);
}
}

View File

@@ -0,0 +1,352 @@
package com.usatiuk.objects.snapshot;
import com.usatiuk.objects.iterators.IteratorStart;
import jakarta.annotation.Nullable;
import java.util.*;
public class SnapshotNavigableMap<K extends Comparable<K>, V> implements NavigableMap<K, V> {
private record Bound<K extends Comparable<K>>(K key, boolean inclusive) {
}
private final Snapshot<K, V> _snapshot;
@Nullable
private final Bound<K> _lowerBound;
@Nullable
private final Bound<K> _upperBound;
private SnapshotNavigableMap(Snapshot<K, V> snapshot, Bound<K> lowerBound, Bound<K> upperBound) {
_snapshot = snapshot;
_lowerBound = lowerBound;
_upperBound = upperBound;
}
public SnapshotNavigableMap(Snapshot<K, V> snapshot) {
this(snapshot, null, null);
}
private final boolean checkBounds(K key) {
if (_lowerBound != null) {
if (_lowerBound.inclusive()) {
if (key.compareTo(_lowerBound.key()) < 0) {
return false;
}
} else {
if (key.compareTo(_lowerBound.key()) <= 0) {
return false;
}
}
}
if (_upperBound != null) {
if (_upperBound.inclusive()) {
if (key.compareTo(_upperBound.key()) > 0) {
return false;
}
} else {
if (key.compareTo(_upperBound.key()) >= 0) {
return false;
}
}
}
return true;
}
@Override
public Entry<K, V> lowerEntry(K key) {
try (var it = _snapshot.getIterator(IteratorStart.LT, key)) {
if (it.hasNext()) {
var realKey = it.peekNextKey();
if (realKey.compareTo(key) >= 0) {
return null;
}
if (!checkBounds(realKey)) {
return null;
}
return it.next();
}
}
return null;
}
@Override
public K lowerKey(K key) {
try (var it = _snapshot.getIterator(IteratorStart.LT, key)) {
if (it.hasNext()) {
var realKey = it.peekNextKey();
if (realKey.compareTo(key) >= 0) {
return null;
}
if (!checkBounds(realKey)) {
return null;
}
return realKey;
}
}
return null;
}
@Override
public Entry<K, V> floorEntry(K key) {
try (var it = _snapshot.getIterator(IteratorStart.LE, key)) {
if (it.hasNext()) {
var realKey = it.peekNextKey();
if (realKey.compareTo(key) > 0) {
return null;
}
if (!checkBounds(realKey)) {
return null;
}
return it.next();
}
}
return null;
}
@Override
public K floorKey(K key) {
try (var it = _snapshot.getIterator(IteratorStart.LE, key)) {
if (it.hasNext()) {
var realKey = it.peekNextKey();
if (realKey.compareTo(key) > 0) {
return null;
}
if (!checkBounds(realKey)) {
return null;
}
return realKey;
}
}
return null;
}
@Override
public Entry<K, V> ceilingEntry(K key) {
try (var it = _snapshot.getIterator(IteratorStart.GE, key)) {
if (it.hasNext()) {
var realKey = it.peekNextKey();
if (realKey.compareTo(key) < 0) {
return null;
}
if (!checkBounds(realKey)) {
return null;
}
return it.next();
}
}
return null;
}
@Override
public K ceilingKey(K key) {
try (var it = _snapshot.getIterator(IteratorStart.GE, key)) {
if (it.hasNext()) {
var realKey = it.peekNextKey();
if (realKey.compareTo(key) < 0) {
return null;
}
if (!checkBounds(realKey)) {
return null;
}
return realKey;
}
}
return null;
}
@Override
public Entry<K, V> higherEntry(K key) {
try (var it = _snapshot.getIterator(IteratorStart.GT, key)) {
if (it.hasNext()) {
var realKey = it.peekNextKey();
if (realKey.compareTo(key) <= 0) {
return null;
}
if (!checkBounds(realKey)) {
return null;
}
return it.next();
}
}
return null;
}
@Override
public K higherKey(K key) {
try (var it = _snapshot.getIterator(IteratorStart.GT, key)) {
if (it.hasNext()) {
var realKey = it.peekNextKey();
if (realKey.compareTo(key) <= 0) {
return null;
}
if (!checkBounds(realKey)) {
return null;
}
return realKey;
}
}
return null;
}
@Override
public Entry<K, V> firstEntry() {
var lb = _lowerBound == null ? null : _lowerBound.key();
var start = _lowerBound != null ? (_lowerBound.inclusive() ? IteratorStart.GE : IteratorStart.GT) : IteratorStart.GE;
try (var it = _snapshot.getIterator(start, lb)) {
if (it.hasNext()) {
var realKey = it.peekNextKey();
if (!checkBounds(realKey)) {
return null;
}
return it.next();
}
}
return null;
}
@Override
public Entry<K, V> lastEntry() {
var b = _upperBound == null ? null : _upperBound.key();
var start = _upperBound != null ? (_upperBound.inclusive() ? IteratorStart.LE : IteratorStart.LT) : IteratorStart.LE;
try (var it = _snapshot.getIterator(start, b)) {
if (it.hasNext()) {
var realKey = it.peekNextKey();
if (!checkBounds(realKey)) {
return null;
}
return it.next();
}
}
return null;
}
@Override
public Entry<K, V> pollFirstEntry() {
throw new UnsupportedOperationException();
}
@Override
public Entry<K, V> pollLastEntry() {
throw new UnsupportedOperationException();
}
@Override
public NavigableMap<K, V> descendingMap() {
return null;
}
@Override
public NavigableSet<K> navigableKeySet() {
return null;
}
@Override
public NavigableSet<K> descendingKeySet() {
return null;
}
@Override
public NavigableMap<K, V> subMap(K fromKey, boolean fromInclusive, K toKey, boolean toInclusive) {
return null;
}
@Override
public NavigableMap<K, V> headMap(K toKey, boolean inclusive) {
return null;
}
@Override
public NavigableMap<K, V> tailMap(K fromKey, boolean inclusive) {
return null;
}
@Override
public Comparator<? super K> comparator() {
return null;
}
@Override
public SortedMap<K, V> subMap(K fromKey, K toKey) {
return null;
}
@Override
public SortedMap<K, V> headMap(K toKey) {
return null;
}
@Override
public SortedMap<K, V> tailMap(K fromKey) {
return null;
}
@Override
public K firstKey() {
return null;
}
@Override
public K lastKey() {
return null;
}
@Override
public int size() {
return 0;
}
@Override
public boolean isEmpty() {
return false;
}
@Override
public boolean containsKey(Object key) {
return false;
}
@Override
public boolean containsValue(Object value) {
return false;
}
@Override
public V get(Object key) {
return null;
}
@Override
public V put(K key, V value) {
return null;
}
@Override
public V remove(Object key) {
return null;
}
@Override
public void putAll(Map<? extends K, ? extends V> m) {
}
@Override
public void clear() {
}
@Override
public Set<K> keySet() {
return Set.of();
}
@Override
public Collection<V> values() {
return List.of();
}
@Override
public Set<Entry<K, V>> entrySet() {
return Set.of();
}
}

View File

@@ -0,0 +1,294 @@
package com.usatiuk.objects.stores;
import com.usatiuk.objects.JDataVersionedWrapper;
import com.usatiuk.objects.JObjectKey;
import com.usatiuk.objects.iterators.*;
import com.usatiuk.objects.iterators.*;
import com.usatiuk.objects.snapshot.Snapshot;
import com.usatiuk.objects.transaction.LockManager;
import io.quarkus.logging.Log;
import io.quarkus.runtime.Startup;
import jakarta.enterprise.context.ApplicationScoped;
import jakarta.inject.Inject;
import org.apache.commons.lang3.tuple.Pair;
import org.eclipse.microprofile.config.inject.ConfigProperty;
import org.pcollections.TreePMap;
import javax.annotation.Nonnull;
import java.util.LinkedHashMap;
import java.util.Optional;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.locks.ReentrantReadWriteLock;
@ApplicationScoped
public class CachingObjectPersistentStore {
private final LinkedHashMap<JObjectKey, CacheEntry> _cache = new LinkedHashMap<>();
private final ReentrantReadWriteLock _lock = new ReentrantReadWriteLock();
@Inject
LockManager lockManager;
@Inject
SerializingObjectPersistentStore delegate;
@ConfigProperty(name = "dhfs.objects.lru.limit")
long sizeLimit;
@ConfigProperty(name = "dhfs.objects.lru.print-stats")
boolean printStats;
private TreePMap<JObjectKey, CacheEntry> _sortedCache = TreePMap.empty();
private long _cacheVersion = 0;
private long _curSize = 0;
private long _evict = 0;
private ExecutorService _statusExecutor = null;
@Startup
void init() {
if (printStats) {
_statusExecutor = Executors.newSingleThreadExecutor();
_statusExecutor.submit(() -> {
try {
while (true) {
Thread.sleep(10000);
if (_curSize > 0)
Log.info("Cache status: size=" + _curSize / 1024 / 1024 + "MB" + " evicted=" + _evict);
_evict = 0;
}
} catch (InterruptedException ignored) {
}
});
}
}
private void put(JObjectKey key, Optional<JDataVersionedWrapper> obj) {
// Log.tracev("Adding {0} to cache: {1}", key, obj);
_lock.writeLock().lock();
try {
int size = obj.map(JDataVersionedWrapper::estimateSize).orElse(16);
_curSize += size;
var entry = new CacheEntry(obj.<MaybeTombstone<JDataVersionedWrapper>>map(Data::new).orElse(new Tombstone<>()), size);
var old = _cache.putLast(key, entry);
_sortedCache = _sortedCache.plus(key, entry);
if (old != null)
_curSize -= old.size();
while (_curSize >= sizeLimit) {
var del = _cache.pollFirstEntry();
_sortedCache = _sortedCache.minus(del.getKey());
_curSize -= del.getValue().size();
_evict++;
}
} finally {
_lock.writeLock().unlock();
}
}
@Nonnull
public Optional<JDataVersionedWrapper> readObject(JObjectKey name) {
_lock.readLock().lock();
try {
var got = _cache.get(name);
if (got != null) {
return got.object().opt();
}
} finally {
_lock.readLock().unlock();
}
// Global object lock, prevent putting the object into cache
// if its being written right now by another thread
try (var lock = lockManager.tryLockObject(name)) {
var got = delegate.readObject(name);
if (lock == null)
return got;
_lock.writeLock().lock();
try {
put(name, got);
// No need to increase cache version, the objects didn't change
} finally {
_lock.writeLock().unlock();
}
return got;
}
}
public void commitTx(TxManifestObj<? extends JDataVersionedWrapper> names, long txId) {
var serialized = delegate.prepareManifest(names);
Log.tracev("Committing: {0} writes, {1} deletes", names.written().size(), names.deleted().size());
// A little complicated locking to minimize write lock holding time
delegate.commitTx(serialized, txId, (commit) -> {
_lock.writeLock().lock();
try {
// Make the changes visible atomically both in cache and in the underlying store
for (var write : names.written()) {
put(write.getLeft(), Optional.of(write.getRight()));
}
for (var del : names.deleted()) {
put(del, Optional.empty());
}
++_cacheVersion;
commit.run();
} finally {
_lock.writeLock().unlock();
}
});
Log.tracev("Committed: {0} writes, {1} deletes", names.written().size(), names.deleted().size());
}
public Snapshot<JObjectKey, JDataVersionedWrapper> getSnapshot() {
TreePMap<JObjectKey, CacheEntry> curSortedCache;
Snapshot<JObjectKey, JDataVersionedWrapper> backing = null;
long cacheVersion;
try {
// Log.tracev("Getting cache snapshot");
// Decrease the lock time as much as possible
_lock.readLock().lock();
try {
curSortedCache = _sortedCache;
cacheVersion = _cacheVersion;
// TODO: Could this be done without lock?
backing = delegate.getSnapshot();
} finally {
_lock.readLock().unlock();
}
Snapshot<JObjectKey, JDataVersionedWrapper> finalBacking = backing;
return new Snapshot<JObjectKey, JDataVersionedWrapper>() {
private final TreePMap<JObjectKey, CacheEntry> _curSortedCache = curSortedCache;
private final Snapshot<JObjectKey, JDataVersionedWrapper> _backing = finalBacking;
private final long _snapshotCacheVersion = cacheVersion;
private void maybeCache(JObjectKey key, Optional<JDataVersionedWrapper> obj) {
if (_snapshotCacheVersion != _cacheVersion)
return;
_lock.writeLock().lock();
try {
if (_snapshotCacheVersion != _cacheVersion) {
// Log.tracev("Not caching: {0}", key);
} else {
// Log.tracev("Caching: {0}", key);
put(key, obj);
}
} finally {
_lock.writeLock().unlock();
}
}
@Override
public CloseableKvIterator<JObjectKey, JDataVersionedWrapper> getIterator(IteratorStart start, JObjectKey key) {
return new TombstoneMergingKvIterator<>("cache", start, key,
(mS, mK)
-> new MappingKvIterator<>(
new NavigableMapKvIterator<>(_curSortedCache, mS, mK),
e -> {
// Log.tracev("Taken from cache: {0}", e);
return e.object();
}
),
(mS, mK) -> new MappingKvIterator<>(new CachingKvIterator(_backing.getIterator(start, key)), Data::new));
}
@Nonnull
@Override
public Optional<JDataVersionedWrapper> readObject(JObjectKey name) {
var cached = _curSortedCache.get(name);
if (cached != null) {
return switch (cached.object()) {
case Data<JDataVersionedWrapper> data -> Optional.of(data.value());
case Tombstone<JDataVersionedWrapper> tombstone -> {
yield Optional.empty();
}
default -> throw new IllegalStateException("Unexpected value: " + cached.object());
};
}
var read = _backing.readObject(name);
maybeCache(name, read);
return _backing.readObject(name);
}
@Override
public long id() {
return _backing.id();
}
@Override
public void close() {
_backing.close();
}
private class CachingKvIterator implements CloseableKvIterator<JObjectKey, JDataVersionedWrapper> {
private final CloseableKvIterator<JObjectKey, JDataVersionedWrapper> _delegate;
private CachingKvIterator(CloseableKvIterator<JObjectKey, JDataVersionedWrapper> delegate) {
_delegate = delegate;
}
@Override
public JObjectKey peekNextKey() {
return _delegate.peekNextKey();
}
@Override
public void skip() {
_delegate.skip();
}
@Override
public void close() {
_delegate.close();
}
@Override
public boolean hasNext() {
return _delegate.hasNext();
}
@Override
public JObjectKey peekPrevKey() {
return _delegate.peekPrevKey();
}
@Override
public Pair<JObjectKey, JDataVersionedWrapper> prev() {
var prev = _delegate.prev();
maybeCache(prev.getKey(), Optional.of(prev.getValue()));
return prev;
}
@Override
public boolean hasPrev() {
return _delegate.hasPrev();
}
@Override
public void skipPrev() {
_delegate.skipPrev();
}
@Override
public Pair<JObjectKey, JDataVersionedWrapper> next() {
var next = _delegate.next();
maybeCache(next.getKey(), Optional.of(next.getValue()));
return next;
}
}
};
} catch (Throwable ex) {
if (backing != null) {
backing.close();
}
throw ex;
}
}
public long getLastTxId() {
return delegate.getLastCommitId();
}
private record CacheEntry(MaybeTombstone<JDataVersionedWrapper> object, long size) {
}
}

View File

@@ -1,10 +1,12 @@
package com.usatiuk.dhfs.objects.persistence;
package com.usatiuk.objects.stores;
import com.google.protobuf.ByteString;
import com.usatiuk.dhfs.objects.CloseableKvIterator;
import com.usatiuk.dhfs.objects.JObjectKey;
import com.usatiuk.dhfs.objects.KeyPredicateKvIterator;
import com.usatiuk.dhfs.objects.ReversibleKvIterator;
import com.usatiuk.objects.JObjectKey;
import com.usatiuk.objects.iterators.CloseableKvIterator;
import com.usatiuk.objects.iterators.IteratorStart;
import com.usatiuk.objects.iterators.KeyPredicateKvIterator;
import com.usatiuk.objects.iterators.ReversibleKvIterator;
import com.usatiuk.objects.snapshot.Snapshot;
import com.usatiuk.dhfs.supportlib.UninitializedByteBuffer;
import com.usatiuk.dhfs.utils.RefcountedCloseable;
import io.quarkus.arc.properties.IfBuildProperty;
@@ -25,7 +27,9 @@ import java.lang.ref.Cleaner;
import java.nio.ByteBuffer;
import java.nio.charset.StandardCharsets;
import java.nio.file.Path;
import java.util.*;
import java.util.Arrays;
import java.util.NoSuchElementException;
import java.util.Optional;
import java.util.concurrent.atomic.AtomicReference;
import java.util.concurrent.locks.ReentrantReadWriteLock;
import java.util.function.Consumer;
@@ -36,19 +40,16 @@ import static org.lmdbjava.Env.create;
@ApplicationScoped
@IfBuildProperty(name = "dhfs.objects.persistence", stringValue = "lmdb")
public class LmdbObjectPersistentStore implements ObjectPersistentStore {
private static final String DB_NAME = "objects";
private static final byte[] DB_VER_OBJ_NAME = "__DB_VER_OBJ".getBytes(StandardCharsets.UTF_8);
private final Path _root;
private final AtomicReference<RefcountedCloseable<Txn<ByteBuffer>>> _curReadTxn = new AtomicReference<>();
private final ReentrantReadWriteLock _lock = new ReentrantReadWriteLock();
private Env<ByteBuffer> _env;
private Dbi<ByteBuffer> _db;
private boolean _ready = false;
private final AtomicReference<RefcountedCloseable<Txn<ByteBuffer>>> _curReadTxn = new AtomicReference<>();
private long _lastTxId = 0;
private final ReentrantReadWriteLock _lock = new ReentrantReadWriteLock();
private static final String DB_NAME = "objects";
private static final byte[] DB_VER_OBJ_NAME = "__DB_VER_OBJ".getBytes(StandardCharsets.UTF_8);
public LmdbObjectPersistentStore(@ConfigProperty(name = "dhfs.objects.persistence.files.root") String root) {
_root = Path.of(root).resolve("objects");
}
@@ -90,22 +91,6 @@ public class LmdbObjectPersistentStore implements ObjectPersistentStore {
if (!_ready) throw new IllegalStateException("Wrong service order!");
}
@Nonnull
@Override
public Collection<JObjectKey> findAllObjects() {
// try (Txn<ByteBuffer> txn = env.txnRead()) {
// try (var cursor = db.openCursor(txn)) {
// var keys = List.of();
// while (cursor.next()) {
// keys.add(JObjectKey.fromBytes(cursor.key()));
// }
// return keys;
// }
// }
return List.of();
}
@Nonnull
@Override
public Optional<ByteString> readObject(JObjectKey name) {
@@ -116,33 +101,157 @@ public class LmdbObjectPersistentStore implements ObjectPersistentStore {
}
}
private RefcountedCloseable<Txn<ByteBuffer>> getCurTxn() {
_lock.readLock().lock();
try {
var got = _curReadTxn.get();
var refInc = Optional.ofNullable(got).map(RefcountedCloseable::ref).orElse(null);
if (refInc != null) {
return got;
} else {
var newTxn = new RefcountedCloseable<>(_env.txnRead());
_curReadTxn.compareAndSet(got, newTxn);
return newTxn;
}
} finally {
_lock.readLock().unlock();
}
}
@Override
public CloseableKvIterator<JObjectKey, ByteString> getIterator(IteratorStart start, JObjectKey key) {
return new KeyPredicateKvIterator<>(new LmdbKvIterator(start, key), start, key, (k) -> !Arrays.equals(k.name().getBytes(StandardCharsets.UTF_8), DB_VER_OBJ_NAME));
}
@Override
public Snapshot<JObjectKey, ByteString> getSnapshot() {
_lock.readLock().lock();
try {
var txn = new RefcountedCloseable<>(_env.txnRead());
var commitId = getLastCommitId();
return new Snapshot<JObjectKey, ByteString>() {
private final RefcountedCloseable<Txn<ByteBuffer>> _txn = txn;
private final long _id = commitId;
private boolean _closed = false;
@Override
public CloseableKvIterator<JObjectKey, ByteString> getIterator(IteratorStart start, JObjectKey key) {
assert !_closed;
return new KeyPredicateKvIterator<>(new LmdbKvIterator(_txn.ref(), start, key), start, key, (k) -> !Arrays.equals(k.name().getBytes(StandardCharsets.UTF_8), DB_VER_OBJ_NAME));
}
@Nonnull
@Override
public Optional<ByteString> readObject(JObjectKey name) {
assert !_closed;
var got = _db.get(_txn.get(), name.toByteBuffer());
var ret = Optional.ofNullable(got).map(ByteString::copyFrom);
return ret;
}
@Override
public long id() {
assert !_closed;
return _id;
}
@Override
public void close() {
assert !_closed;
_closed = true;
_txn.unref();
}
};
} finally {
_lock.readLock().unlock();
}
}
@Override
public void commitTx(TxManifestRaw names, long txId, Consumer<Runnable> commitLocked) {
verifyReady();
try (Txn<ByteBuffer> txn = _env.txnWrite()) {
for (var written : names.written()) {
// TODO:
var bb = UninitializedByteBuffer.allocateUninitialized(written.getValue().size());
bb.put(written.getValue().asReadOnlyByteBuffer());
bb.flip();
_db.put(txn, written.getKey().toByteBuffer(), bb);
}
for (JObjectKey key : names.deleted()) {
_db.delete(txn, key.toByteBuffer());
}
var bb = ByteBuffer.allocateDirect(DB_VER_OBJ_NAME.length);
bb.put(DB_VER_OBJ_NAME);
bb.flip();
var bbData = ByteBuffer.allocateDirect(8);
commitLocked.accept(() -> {
_lock.writeLock().lock();
try {
var realTxId = txId;
if (realTxId == -1)
realTxId = _lastTxId + 1;
assert realTxId > _lastTxId;
_lastTxId = realTxId;
bbData.putLong(realTxId);
bbData.flip();
_db.put(txn, bb, bbData);
_curReadTxn.set(null);
txn.commit();
} finally {
_lock.writeLock().unlock();
}
});
}
}
@Override
public long getTotalSpace() {
verifyReady();
return _root.toFile().getTotalSpace();
}
@Override
public long getFreeSpace() {
verifyReady();
return _root.toFile().getFreeSpace();
}
@Override
public long getUsableSpace() {
verifyReady();
return _root.toFile().getUsableSpace();
}
@Override
public long getLastCommitId() {
_lock.readLock().lock();
try {
return _lastTxId;
} finally {
_lock.readLock().unlock();
}
}
private class LmdbKvIterator extends ReversibleKvIterator<JObjectKey, ByteString> {
private static final Cleaner CLEANER = Cleaner.create();
private final RefcountedCloseable<Txn<ByteBuffer>> _txn;
private final Cursor<ByteBuffer> _cursor;
private boolean _hasNext = false;
private static final Cleaner CLEANER = Cleaner.create();
private final MutableObject<Boolean> _closed = new MutableObject<>(false);
// private final Exception _allocationStacktrace = new Exception();
private final Exception _allocationStacktrace = null;
private boolean _hasNext = false;
LmdbKvIterator(IteratorStart start, JObjectKey key) {
LmdbKvIterator(RefcountedCloseable<Txn<ByteBuffer>> txn, IteratorStart start, JObjectKey key) {
_txn = txn;
_goingForward = true;
_lock.readLock().lock();
try {
var got = _curReadTxn.get();
var refInc = Optional.ofNullable(got).map(RefcountedCloseable::ref).orElse(null);
if (refInc != null) {
_txn = got;
} else {
var newTxn = new RefcountedCloseable<>(_env.txnRead());
_curReadTxn.compareAndSet(got, newTxn);
_txn = newTxn;
}
} finally {
_lock.readLock().unlock();
}
_cursor = _db.openCursor(_txn.get());
var closedRef = _closed;
@@ -155,7 +264,10 @@ public class LmdbObjectPersistentStore implements ObjectPersistentStore {
});
verifyReady();
if (!_cursor.get(key.toByteBuffer(), GetOp.MDB_SET_RANGE)) {
if (key.toByteBuffer().remaining() == 0) {
if (!_cursor.first())
return;
} else if (!_cursor.get(key.toByteBuffer(), GetOp.MDB_SET_RANGE)) {
return;
}
@@ -214,6 +326,10 @@ public class LmdbObjectPersistentStore implements ObjectPersistentStore {
Log.tracev("got: {0}, hasNext: {1}", realGot, _hasNext);
}
LmdbKvIterator(IteratorStart start, JObjectKey key) {
this(getCurTxn(), start, key);
}
@Override
public void close() {
if (_closed.getValue()) {
@@ -287,81 +403,4 @@ public class LmdbObjectPersistentStore implements ObjectPersistentStore {
}
}
@Override
public CloseableKvIterator<JObjectKey, ByteString> getIterator(IteratorStart start, JObjectKey key) {
return new KeyPredicateKvIterator<>(new LmdbKvIterator(start, key), start, key, (k) -> !Arrays.equals(k.name().getBytes(StandardCharsets.UTF_8), DB_VER_OBJ_NAME));
}
@Override
public void commitTx(TxManifestRaw names, long txId, Consumer<Runnable> commitLocked) {
verifyReady();
try (Txn<ByteBuffer> txn = _env.txnWrite()) {
for (var written : names.written()) {
// TODO:
var bb = UninitializedByteBuffer.allocateUninitialized(written.getValue().size());
bb.put(written.getValue().asReadOnlyByteBuffer());
bb.flip();
_db.put(txn, written.getKey().toByteBuffer(), bb);
}
for (JObjectKey key : names.deleted()) {
_db.delete(txn, key.toByteBuffer());
}
var bb = ByteBuffer.allocateDirect(DB_VER_OBJ_NAME.length);
bb.put(DB_VER_OBJ_NAME);
bb.flip();
var bbData = ByteBuffer.allocateDirect(8);
commitLocked.accept(() -> {
_lock.writeLock().lock();
try {
var realTxId = txId;
if (realTxId == -1)
realTxId = _lastTxId + 1;
assert realTxId > _lastTxId;
_lastTxId = realTxId;
bbData.putLong(realTxId);
bbData.flip();
_db.put(txn, bb, bbData);
_curReadTxn.set(null);
txn.commit();
} finally {
_lock.writeLock().unlock();
}
});
}
}
@Override
public long getTotalSpace() {
verifyReady();
return _root.toFile().getTotalSpace();
}
@Override
public long getFreeSpace() {
verifyReady();
return _root.toFile().getFreeSpace();
}
@Override
public long getUsableSpace() {
verifyReady();
return _root.toFile().getUsableSpace();
}
@Override
public long getLastCommitId() {
_lock.readLock().lock();
try {
return _lastTxId;
} finally {
_lock.readLock().unlock();
}
}
}

View File

@@ -1,33 +1,26 @@
package com.usatiuk.dhfs.objects.persistence;
package com.usatiuk.objects.stores;
import com.google.protobuf.ByteString;
import com.usatiuk.dhfs.objects.CloseableKvIterator;
import com.usatiuk.dhfs.objects.JObjectKey;
import com.usatiuk.dhfs.objects.NavigableMapKvIterator;
import com.usatiuk.objects.JObjectKey;
import com.usatiuk.objects.iterators.CloseableKvIterator;
import com.usatiuk.objects.iterators.IteratorStart;
import com.usatiuk.objects.iterators.NavigableMapKvIterator;
import com.usatiuk.objects.snapshot.Snapshot;
import io.quarkus.arc.properties.IfBuildProperty;
import jakarta.enterprise.context.ApplicationScoped;
import org.pcollections.TreePMap;
import javax.annotation.Nonnull;
import java.util.Collection;
import java.util.Optional;
import java.util.concurrent.ConcurrentSkipListMap;
import java.util.concurrent.locks.ReentrantReadWriteLock;
import java.util.function.Consumer;
@ApplicationScoped
@IfBuildProperty(name = "dhfs.objects.persistence", stringValue = "memory")
public class MemoryObjectPersistentStore implements ObjectPersistentStore {
private final ConcurrentSkipListMap<JObjectKey, ByteString> _objects = new ConcurrentSkipListMap<>();
private long _lastCommitId = 0;
private final ReentrantReadWriteLock _lock = new ReentrantReadWriteLock();
@Nonnull
@Override
public Collection<JObjectKey> findAllObjects() {
synchronized (this) {
return _objects.keySet();
}
}
private TreePMap<JObjectKey, ByteString> _objects = TreePMap.empty();
private long _lastCommitId = 0;
@Nonnull
@Override
@@ -42,14 +35,45 @@ public class MemoryObjectPersistentStore implements ObjectPersistentStore {
return new NavigableMapKvIterator<>(_objects, start, key);
}
@Override
public Snapshot<JObjectKey, ByteString> getSnapshot() {
synchronized (this) {
return new Snapshot<JObjectKey, ByteString>() {
private final TreePMap<JObjectKey, ByteString> _objects = MemoryObjectPersistentStore.this._objects;
private final long _lastCommitId = MemoryObjectPersistentStore.this._lastCommitId;
@Override
public CloseableKvIterator<JObjectKey, ByteString> getIterator(IteratorStart start, JObjectKey key) {
return new NavigableMapKvIterator<>(_objects, start, key);
}
@Nonnull
@Override
public Optional<ByteString> readObject(JObjectKey name) {
return Optional.ofNullable(_objects.get(name));
}
@Override
public long id() {
return _lastCommitId;
}
@Override
public void close() {
}
};
}
}
@Override
public void commitTx(TxManifestRaw names, long txId, Consumer<Runnable> commitLocked) {
synchronized (this) {
for (var written : names.written()) {
_objects.put(written.getKey(), written.getValue());
_objects = _objects.plus(written.getKey(), written.getValue());
}
for (JObjectKey key : names.deleted()) {
_objects.remove(key);
_objects = _objects.minus(key);
}
commitLocked.accept(() -> {
_lock.writeLock().lock();

View File

@@ -1,20 +1,18 @@
package com.usatiuk.dhfs.objects.persistence;
package com.usatiuk.objects.stores;
import com.google.protobuf.ByteString;
import com.usatiuk.dhfs.objects.CloseableKvIterator;
import com.usatiuk.dhfs.objects.JObjectKey;
import com.usatiuk.objects.JObjectKey;
import com.usatiuk.objects.iterators.CloseableKvIterator;
import com.usatiuk.objects.iterators.IteratorStart;
import com.usatiuk.objects.snapshot.Snapshot;
import javax.annotation.Nonnull;
import java.util.Collection;
import java.util.Optional;
import java.util.function.Consumer;
// Persistent storage of objects
// All changes are written as sequential transactions
public interface ObjectPersistentStore {
@Nonnull
Collection<JObjectKey> findAllObjects();
@Nonnull
Optional<ByteString> readObject(JObjectKey name);
@@ -22,6 +20,8 @@ public interface ObjectPersistentStore {
// Does not have to guarantee consistent view, snapshots are handled by upper layers
CloseableKvIterator<JObjectKey, ByteString> getIterator(IteratorStart start, JObjectKey key);
Snapshot<JObjectKey, ByteString> getSnapshot();
/**
* @param commitLocked - a function that will be called with a Runnable that will commit the transaction
* the changes in the store will be visible to new transactions only after the runnable is called

View File

@@ -1,4 +1,6 @@
package com.usatiuk.dhfs.objects;
package com.usatiuk.objects.stores;
import com.usatiuk.objects.JObjectKey;
public record PendingDelete(JObjectKey key, long bundleId) implements PendingWriteEntry {
}

View File

@@ -1,4 +1,6 @@
package com.usatiuk.dhfs.objects;
package com.usatiuk.objects.stores;
import com.usatiuk.objects.JDataVersionedWrapper;
public record PendingWrite(JDataVersionedWrapper data, long bundleId) implements PendingWriteEntry {
}

View File

@@ -1,4 +1,4 @@
package com.usatiuk.dhfs.objects;
package com.usatiuk.objects.stores;
public interface PendingWriteEntry {
long bundleId();

View File

@@ -0,0 +1,80 @@
package com.usatiuk.objects.stores;
import com.google.protobuf.ByteString;
import com.usatiuk.objects.JDataVersionedWrapper;
import com.usatiuk.objects.JObjectKey;
import com.usatiuk.objects.ObjectSerializer;
import com.usatiuk.objects.iterators.CloseableKvIterator;
import com.usatiuk.objects.iterators.IteratorStart;
import com.usatiuk.objects.iterators.MappingKvIterator;
import com.usatiuk.objects.snapshot.Snapshot;
import jakarta.enterprise.context.ApplicationScoped;
import jakarta.inject.Inject;
import org.apache.commons.lang3.tuple.Pair;
import javax.annotation.Nonnull;
import java.util.Optional;
import java.util.function.Consumer;
@ApplicationScoped
public class SerializingObjectPersistentStore {
@Inject
ObjectSerializer<JDataVersionedWrapper> serializer;
@Inject
ObjectPersistentStore delegateStore;
@Nonnull
Optional<JDataVersionedWrapper> readObject(JObjectKey name) {
return delegateStore.readObject(name).map(serializer::deserialize);
}
public TxManifestRaw prepareManifest(TxManifestObj<? extends JDataVersionedWrapper> names) {
return new TxManifestRaw(
names.written().stream()
.map(e -> Pair.of(e.getKey(), serializer.serialize(e.getValue())))
.toList()
, names.deleted());
}
public Snapshot<JObjectKey, JDataVersionedWrapper> getSnapshot() {
return new Snapshot<JObjectKey, JDataVersionedWrapper>() {
private final Snapshot<JObjectKey, ByteString> _backing = delegateStore.getSnapshot();
@Override
public CloseableKvIterator<JObjectKey, JDataVersionedWrapper> getIterator(IteratorStart start, JObjectKey key) {
return new MappingKvIterator<>(_backing.getIterator(start, key), d -> serializer.deserialize(d));
}
@Nonnull
@Override
public Optional<JDataVersionedWrapper> readObject(JObjectKey name) {
return _backing.readObject(name).map(serializer::deserialize);
}
@Override
public long id() {
return _backing.id();
}
@Override
public void close() {
_backing.close();
}
};
}
// void commitTx(TxManifestObj<? extends JDataVersionedWrapper> names, Consumer<Runnable> commitLocked) {
// delegateStore.commitTx(prepareManifest(names), commitLocked);
// }
void commitTx(TxManifestRaw names, long txId, Consumer<Runnable> commitLocked) {
delegateStore.commitTx(names, txId, commitLocked);
}
long getLastCommitId() {
return delegateStore.getLastCommitId();
}
}

View File

@@ -1,6 +1,6 @@
package com.usatiuk.dhfs.objects.persistence;
package com.usatiuk.objects.stores;
import com.usatiuk.dhfs.objects.JObjectKey;
import com.usatiuk.objects.JObjectKey;
import org.apache.commons.lang3.tuple.Pair;
import java.io.Serializable;

View File

@@ -1,7 +1,7 @@
package com.usatiuk.dhfs.objects.persistence;
package com.usatiuk.objects.stores;
import com.google.protobuf.ByteString;
import com.usatiuk.dhfs.objects.JObjectKey;
import com.usatiuk.objects.JObjectKey;
import org.apache.commons.lang3.tuple.Pair;
import java.io.Serializable;

View File

@@ -1,9 +1,13 @@
package com.usatiuk.dhfs.objects;
package com.usatiuk.objects.stores;
import com.usatiuk.dhfs.objects.persistence.CachingObjectPersistentStore;
import com.usatiuk.dhfs.objects.persistence.IteratorStart;
import com.usatiuk.dhfs.objects.persistence.TxManifestObj;
import com.usatiuk.dhfs.objects.transaction.TxRecord;
import com.usatiuk.objects.JDataVersionedWrapper;
import com.usatiuk.objects.JDataVersionedWrapperImpl;
import com.usatiuk.objects.JObjectKey;
import com.usatiuk.objects.iterators.*;
import com.usatiuk.objects.iterators.*;
import com.usatiuk.objects.snapshot.Snapshot;
import com.usatiuk.objects.transaction.TxCommitException;
import com.usatiuk.objects.transaction.TxRecord;
import io.quarkus.logging.Log;
import io.quarkus.runtime.ShutdownEvent;
import io.quarkus.runtime.StartupEvent;
@@ -303,6 +307,150 @@ public class WritebackObjectPersistentStore {
}
}
public Optional<PendingWriteEntry> getPendingWrite(JObjectKey key) {
synchronized (_pendingBundles) {
return Optional.ofNullable(_pendingWrites.get().get(key));
}
}
@Nonnull
public Optional<JDataVersionedWrapper> readObject(JObjectKey name) {
var pending = getPendingWrite(name).orElse(null);
return switch (pending) {
case PendingWrite write -> Optional.of(write.data());
case PendingDelete ignored -> Optional.empty();
case null -> cachedStore.readObject(name);
default -> throw new IllegalStateException("Unexpected value: " + pending);
};
}
@Nonnull
public VerboseReadResult readObjectVerbose(JObjectKey key) {
var pending = getPendingWrite(key).orElse(null);
if (pending != null) {
return new VerboseReadResultPending(pending);
}
return new VerboseReadResultPersisted(cachedStore.readObject(key));
}
/**
* @param commitLocked - a function that will be called with a Consumer of a new transaction id,
* that will commit the transaction the changes in the store will be visible to new transactions
* only after the runnable is called
*/
public Consumer<Runnable> commitTx(Collection<TxRecord.TxObjectRecord<?>> writes, BiConsumer<Long, Runnable> commitLocked) {
var bundle = createBundle();
long bundleId = bundle.getId();
try {
for (var action : writes) {
switch (action) {
case TxRecord.TxObjectRecordWrite<?> write -> {
Log.trace("Flushing object " + write.key());
bundle.commit(new JDataVersionedWrapperImpl(write.data(), bundleId));
}
case TxRecord.TxObjectRecordDeleted deleted -> {
Log.trace("Deleting object " + deleted.key());
bundle.delete(deleted.key());
}
default -> {
throw new TxCommitException("Unexpected value: " + action.key());
}
}
}
} catch (Throwable t) {
dropBundle(bundle);
throw new TxCommitException(t.getMessage(), t);
}
Log.tracef("Committing transaction %d to storage", bundleId);
commitLocked.accept(bundleId, () -> {
commitBundle(bundle);
});
return r -> asyncFence(bundleId, r);
}
public Snapshot<JObjectKey, JDataVersionedWrapper> getSnapshot() {
PSortedMap<JObjectKey, PendingWriteEntry> pendingWrites;
Snapshot<JObjectKey, JDataVersionedWrapper> cache = null;
long lastTxId;
try {
_pendingWritesVersionLock.readLock().lock();
try {
pendingWrites = _pendingWrites.get();
cache = cachedStore.getSnapshot();
lastTxId = getLastTxId();
} finally {
_pendingWritesVersionLock.readLock().unlock();
}
Snapshot<JObjectKey, JDataVersionedWrapper> finalCache = cache;
return new Snapshot<JObjectKey, JDataVersionedWrapper>() {
private final PSortedMap<JObjectKey, PendingWriteEntry> _pendingWrites = pendingWrites;
private final Snapshot<JObjectKey, JDataVersionedWrapper> _cache = finalCache;
private final long txId = lastTxId;
@Override
public CloseableKvIterator<JObjectKey, JDataVersionedWrapper> getIterator(IteratorStart start, JObjectKey key) {
return new TombstoneMergingKvIterator<>("writeback-ps", start, key,
(tS, tK) -> new MappingKvIterator<>(
new NavigableMapKvIterator<>(_pendingWrites, tS, tK),
e -> switch (e) {
case PendingWrite pw -> new Data<>(pw.data());
case PendingDelete d -> new Tombstone<>();
default -> throw new IllegalStateException("Unexpected value: " + e);
}),
(tS, tK) -> new MappingKvIterator<>(_cache.getIterator(tS, tK), Data::new));
}
@Nonnull
@Override
public Optional<JDataVersionedWrapper> readObject(JObjectKey name) {
var cached = _pendingWrites.get(name);
if (cached != null) {
return switch (cached) {
case PendingWrite c -> Optional.of(c.data());
case PendingDelete d -> {
yield Optional.empty();
}
default -> throw new IllegalStateException("Unexpected value: " + cached);
};
}
return _cache.readObject(name);
}
@Override
public long id() {
assert lastTxId >= _cache.id();
return lastTxId;
}
@Override
public void close() {
_cache.close();
}
};
} catch (Throwable e) {
if (cache != null)
cache.close();
throw e;
}
}
public long getLastTxId() {
_pendingWritesVersionLock.readLock().lock();
try {
return _lastCommittedTx.get();
} finally {
_pendingWritesVersionLock.readLock().unlock();
}
}
public interface VerboseReadResult {
}
private static class TxBundle {
private final LinkedHashMap<JObjectKey, BundleEntry> _entries = new LinkedHashMap<>();
private final ArrayList<Runnable> _callbacks = new ArrayList<>();
@@ -384,107 +532,9 @@ public class WritebackObjectPersistentStore {
}
}
public Optional<PendingWriteEntry> getPendingWrite(JObjectKey key) {
synchronized (_pendingBundles) {
return Optional.ofNullable(_pendingWrites.get().get(key));
}
}
@Nonnull
public Optional<JDataVersionedWrapper> readObject(JObjectKey name) {
var pending = getPendingWrite(name).orElse(null);
return switch (pending) {
case PendingWrite write -> Optional.of(write.data());
case PendingDelete ignored -> Optional.empty();
case null -> cachedStore.readObject(name);
default -> throw new IllegalStateException("Unexpected value: " + pending);
};
}
public interface VerboseReadResult {
}
public record VerboseReadResultPersisted(Optional<JDataVersionedWrapper> data) implements VerboseReadResult {
}
public record VerboseReadResultPending(PendingWriteEntry pending) implements VerboseReadResult {
}
@Nonnull
public VerboseReadResult readObjectVerbose(JObjectKey key) {
var pending = getPendingWrite(key).orElse(null);
if (pending != null) {
return new VerboseReadResultPending(pending);
}
return new VerboseReadResultPersisted(cachedStore.readObject(key));
}
/**
* @param commitLocked - a function that will be called with a Consumer of a new transaction id,
* that will commit the transaction the changes in the store will be visible to new transactions
* only after the runnable is called
*/
public Consumer<Runnable> commitTx(Collection<TxRecord.TxObjectRecord<?>> writes, BiConsumer<Long, Runnable> commitLocked) {
var bundle = createBundle();
long bundleId = bundle.getId();
try {
for (var action : writes) {
switch (action) {
case TxRecord.TxObjectRecordWrite<?> write -> {
Log.trace("Flushing object " + write.key());
bundle.commit(new JDataVersionedWrapperImpl(write.data(), bundleId));
}
case TxRecord.TxObjectRecordDeleted deleted -> {
Log.trace("Deleting object " + deleted.key());
bundle.delete(deleted.key());
}
default -> {
throw new TxCommitException("Unexpected value: " + action.key());
}
}
}
} catch (Throwable t) {
dropBundle(bundle);
throw new TxCommitException(t.getMessage(), t);
}
Log.tracef("Committing transaction %d to storage", bundleId);
commitLocked.accept(bundleId, () -> {
commitBundle(bundle);
});
return r -> asyncFence(bundleId, r);
}
// Returns an iterator with a view of all commited objects
// Does not have to guarantee consistent view, snapshots are handled by upper layers
// Invalidated by commitBundle, but might return data after it has been really committed
public CloseableKvIterator<JObjectKey, JDataVersionedWrapper> getIterator(IteratorStart start, JObjectKey key) {
Log.tracev("Getting writeback iterator: {0}, {1}", start, key);
_pendingWritesVersionLock.readLock().lock();
try {
var curPending = _pendingWrites.get();
return new TombstoneMergingKvIterator<>("writeback-ps", start, key,
(tS, tK) -> new MappingKvIterator<>(
new NavigableMapKvIterator<>(curPending, tS, tK),
e -> switch (e) {
case PendingWrite pw -> new Data<>(pw.data());
case PendingDelete d -> new Tombstone<>();
default -> throw new IllegalStateException("Unexpected value: " + e);
}),
(tS, tK) -> cachedStore.getIterator(tS, tK));
} finally {
_pendingWritesVersionLock.readLock().unlock();
}
}
public long getLastTxId() {
_pendingWritesVersionLock.readLock().lock();
try {
return _lastCommittedTx.get();
} finally {
_pendingWritesVersionLock.readLock().unlock();
}
}
}

View File

@@ -1,15 +1,12 @@
package com.usatiuk.dhfs.objects;
package com.usatiuk.objects.transaction;
import com.usatiuk.dhfs.objects.persistence.IteratorStart;
import com.usatiuk.dhfs.objects.transaction.LockingStrategy;
import com.usatiuk.dhfs.objects.transaction.Transaction;
import com.usatiuk.objects.JData;
import com.usatiuk.objects.JObjectKey;
import com.usatiuk.objects.iterators.CloseableKvIterator;
import com.usatiuk.objects.iterators.IteratorStart;
import jakarta.enterprise.context.ApplicationScoped;
import jakarta.inject.Inject;
import org.apache.commons.lang3.tuple.Pair;
import javax.annotation.Nonnull;
import java.util.Collection;
import java.util.Iterator;
import java.util.Optional;
@ApplicationScoped
@@ -37,12 +34,6 @@ public class CurrentTransaction implements Transaction {
transactionManager.current().delete(key);
}
@Nonnull
@Override
public Collection<JObjectKey> findAllObjects() {
return transactionManager.current().findAllObjects();
}
@Override
public CloseableKvIterator<JObjectKey, JData> getIterator(IteratorStart start, JObjectKey key) {
return transactionManager.current().getIterator(start, key);

View File

@@ -1,7 +1,9 @@
package com.usatiuk.dhfs.objects;
package com.usatiuk.objects.transaction;
import com.usatiuk.dhfs.objects.snapshot.SnapshotManager;
import com.usatiuk.dhfs.objects.transaction.*;
import com.usatiuk.objects.JData;
import com.usatiuk.objects.JDataVersionedWrapper;
import com.usatiuk.objects.JObjectKey;
import com.usatiuk.objects.snapshot.SnapshotManager;
import com.usatiuk.dhfs.utils.AutoCloseableNoThrow;
import io.quarkus.logging.Log;
import io.quarkus.runtime.StartupEvent;
@@ -10,26 +12,32 @@ import jakarta.enterprise.context.ApplicationScoped;
import jakarta.enterprise.event.Observes;
import jakarta.enterprise.inject.Instance;
import jakarta.inject.Inject;
import org.apache.commons.lang3.tuple.Pair;
import java.util.*;
import java.util.function.Consumer;
import java.util.function.Function;
import java.util.stream.Stream;
// Manages all access to com.usatiuk.dhfs.objects.JData objects.
// Manages all access to com.usatiuk.objects.JData objects.
// In particular, it serves as a source of truth for what is committed to the backing storage.
// All data goes through it, it is responsible for transaction atomicity
// TODO: persistent tx id
@ApplicationScoped
public class JObjectManager {
private final List<PreCommitTxHook> _preCommitTxHooks;
private boolean _ready = false;
@Inject
SnapshotManager snapshotManager;
@Inject
TransactionFactory transactionFactory;
@Inject
LockManager lockManager;
private boolean _ready = false;
JObjectManager(Instance<PreCommitTxHook> preCommitTxHooks) {
_preCommitTxHooks = List.copyOf(preCommitTxHooks.stream().sorted(Comparator.comparingInt(PreCommitTxHook::getPriority)).toList());
Log.debugv("Pre-commit hooks: {0}", String.join("->", _preCommitTxHooks.stream().map(Objects::toString).toList()));
}
private void verifyReady() {
if (!_ready) throw new IllegalStateException("Wrong service order!");
@@ -39,10 +47,6 @@ public class JObjectManager {
_ready = true;
}
JObjectManager(Instance<PreCommitTxHook> preCommitTxHooks) {
_preCommitTxHooks = preCommitTxHooks.stream().sorted(Comparator.comparingInt(PreCommitTxHook::getPriority)).toList();
}
public TransactionPrivate createTransaction() {
verifyReady();
var tx = transactionFactory.createTransaction();
@@ -50,7 +54,7 @@ public class JObjectManager {
return tx;
}
public TransactionHandle commit(TransactionPrivate tx) {
public Pair<Collection<Runnable>, TransactionHandle> commit(TransactionPrivate tx) {
verifyReady();
var writes = new LinkedHashMap<JObjectKey, TxRecord.TxObjectRecord<?>>();
var dependenciesLocked = new LinkedHashMap<JObjectKey, Optional<JDataVersionedWrapper>>();
@@ -66,34 +70,51 @@ public class JObjectManager {
});
};
// For existing objects:
// Check that their version is not higher than the version of transaction being committed
// TODO: check deletions, inserts
try {
try {
Function<JObjectKey, JData> getPrev =
key -> switch (writes.get(key)) {
case TxRecord.TxObjectRecordWrite<?> write -> write.data();
case TxRecord.TxObjectRecordDeleted deleted -> null;
case null -> tx.getFromSource(JData.class, key).orElse(null);
default -> {
throw new TxCommitException("Unexpected value: " + writes.get(key));
}
};
long pendingCount = 0;
Map<PreCommitTxHook, Map<JObjectKey, TxRecord.TxObjectRecord<?>>> pendingWrites = Map.ofEntries(
_preCommitTxHooks.stream().map(p -> Pair.of(p, new HashMap<>())).toArray(Pair[]::new)
);
Map<PreCommitTxHook, Map<JObjectKey, TxRecord.TxObjectRecord<?>>> lastWrites = Map.ofEntries(
_preCommitTxHooks.stream().map(p -> Pair.of(p, new HashMap<>())).toArray(Pair[]::new)
);
boolean somethingChanged;
for (var n : tx.drainNewWrites()) {
for (var hookPut : _preCommitTxHooks) {
pendingWrites.get(hookPut).put(n.key(), n);
pendingCount++;
}
writes.put(n.key(), n);
}
// Run hooks for all objects
// Every hook should see every change made to every object, yet the object's evolution
// should be consistent from the view point of each individual hook
// For example, when a hook makes changes to an object, and another hook changes the object before/after it
// on the next iteration, the first hook should receive the version of the object it had created
// as the "old" version, and the new version with all the changes after it.
do {
somethingChanged = false;
Map<JObjectKey, TxRecord.TxObjectRecord<?>> currentIteration = new HashMap();
for (var hook : _preCommitTxHooks) {
for (var n : tx.drainNewWrites())
currentIteration.put(n.key(), n);
Log.trace("Commit iteration with " + currentIteration.size() + " records for hook " + hook.getClass());
var lastCurHookSeen = lastWrites.get(hook);
Function<JObjectKey, JData> getPrev =
key -> switch (lastCurHookSeen.get(key)) {
case TxRecord.TxObjectRecordWrite<?> write -> write.data();
case TxRecord.TxObjectRecordDeleted deleted -> null;
case null -> tx.getFromSource(JData.class, key).orElse(null);
default -> {
throw new TxCommitException("Unexpected value: " + writes.get(key));
}
};
for (var entry : currentIteration.entrySet()) {
somethingChanged = true;
Log.trace("Running pre-commit hook " + hook.getClass() + " for" + entry.getKey());
var curIteration = pendingWrites.get(hook);
// Log.trace("Commit iteration with " + curIteration.size() + " records for hook " + hook.getClass());
for (var entry : curIteration.entrySet()) {
// Log.trace("Running pre-commit hook " + hook.getClass() + " for" + entry.getKey());
var oldObj = getPrev.apply(entry.getKey());
lastCurHookSeen.put(entry.getKey(), entry.getValue());
switch (entry.getValue()) {
case TxRecord.TxObjectRecordWrite<?> write -> {
if (oldObj == null) {
@@ -108,32 +129,61 @@ public class JObjectManager {
default -> throw new TxCommitException("Unexpected value: " + entry);
}
}
}
writes.putAll(currentIteration);
} while (somethingChanged);
if (writes.isEmpty()) {
Log.trace("Committing transaction - no changes");
return new TransactionHandle() {
@Override
public void onFlush(Runnable runnable) {
runnable.run();
pendingCount -= curIteration.size();
curIteration.clear();
for (var n : tx.drainNewWrites()) {
for (var hookPut : _preCommitTxHooks) {
if (hookPut == hook) {
lastCurHookSeen.put(n.key(), n);
continue;
}
var before = pendingWrites.get(hookPut).put(n.key(), n);
if (before == null)
pendingCount++;
}
writes.put(n.key(), n);
}
};
}
} finally {
readSet = tx.reads();
Stream.concat(readSet.keySet().stream(), writes.keySet().stream())
.sorted(Comparator.comparing(JObjectKey::toString))
.forEach(addDependency);
for (var read : readSet.entrySet()) {
}
} while (pendingCount > 0);
} catch (Throwable e) {
for (var read : tx.reads().entrySet()) {
if (read.getValue() instanceof TransactionObjectLocked<?> locked) {
toUnlock.add(locked.lock());
}
}
throw e;
}
readSet = tx.reads();
if (!writes.isEmpty()) {
Stream.concat(readSet.keySet().stream(), writes.keySet().stream())
.sorted(Comparator.comparing(JObjectKey::toString))
.forEach(addDependency);
}
for (var read : readSet.entrySet()) {
if (read.getValue() instanceof TransactionObjectLocked<?> locked) {
toUnlock.add(locked.lock());
}
}
if (writes.isEmpty()) {
Log.trace("Committing transaction - no changes");
return Pair.of(
Stream.concat(
tx.getOnCommit().stream(),
tx.getOnFlush().stream()
).toList(),
new TransactionHandle() {
@Override
public void onFlush(Runnable runnable) {
runnable.run();
}
});
}
Log.trace("Committing transaction start");
@@ -175,20 +225,18 @@ public class JObjectManager {
return true;
}).toList());
for (var callback : tx.getOnCommit()) {
callback.run();
}
for (var callback : tx.getOnFlush()) {
addFlushCallback.accept(callback);
}
return new TransactionHandle() {
@Override
public void onFlush(Runnable runnable) {
addFlushCallback.accept(runnable);
}
};
return Pair.of(
List.copyOf(tx.getOnCommit()),
new TransactionHandle() {
@Override
public void onFlush(Runnable runnable) {
addFlushCallback.accept(runnable);
}
});
} catch (Throwable t) {
Log.trace("Error when committing transaction", t);
throw new TxCommitException(t.getMessage(), t);

View File

@@ -1,14 +1,23 @@
package com.usatiuk.dhfs.objects;
package com.usatiuk.objects.transaction;
import com.usatiuk.objects.JObjectKey;
import com.usatiuk.dhfs.utils.AutoCloseableNoThrow;
import com.usatiuk.dhfs.utils.DataLocker;
import jakarta.annotation.Nonnull;
import jakarta.annotation.Nullable;
import jakarta.enterprise.context.ApplicationScoped;
@ApplicationScoped
public class LockManager {
private final DataLocker _objLocker = new DataLocker();
@Nonnull
public AutoCloseableNoThrow lockObject(JObjectKey key) {
return _objLocker.lock(key);
}
@Nullable
public AutoCloseableNoThrow tryLockObject(JObjectKey key) {
return _objLocker.tryLock(key);
}
}

View File

@@ -1,4 +1,4 @@
package com.usatiuk.dhfs.objects.transaction;
package com.usatiuk.objects.transaction;
public enum LockingStrategy {
OPTIMISTIC, // Optimistic write, no blocking other possible writers/readers

View File

@@ -1,4 +1,7 @@
package com.usatiuk.dhfs.objects;
package com.usatiuk.objects.transaction;
import com.usatiuk.objects.JData;
import com.usatiuk.objects.JObjectKey;
public interface PreCommitTxHook {
default void onChange(JObjectKey key, JData old, JData cur) {

View File

@@ -1,12 +1,10 @@
package com.usatiuk.dhfs.objects.transaction;
package com.usatiuk.objects.transaction;
import com.usatiuk.dhfs.objects.CloseableKvIterator;
import com.usatiuk.dhfs.objects.JData;
import com.usatiuk.dhfs.objects.JObjectKey;
import com.usatiuk.dhfs.objects.persistence.IteratorStart;
import com.usatiuk.objects.JData;
import com.usatiuk.objects.JObjectKey;
import com.usatiuk.objects.iterators.CloseableKvIterator;
import com.usatiuk.objects.iterators.IteratorStart;
import javax.annotation.Nonnull;
import java.util.Collection;
import java.util.Optional;
// The transaction interface actually used by user code to retrieve objects
@@ -19,9 +17,6 @@ public interface Transaction extends TransactionHandle {
void delete(JObjectKey key);
@Nonnull
Collection<JObjectKey> findAllObjects(); // FIXME: This is crap
default <T extends JData> Optional<T> get(Class<T> type, JObjectKey key) {
return get(type, key, LockingStrategy.OPTIMISTIC);
}

View File

@@ -1,4 +1,4 @@
package com.usatiuk.dhfs.objects.transaction;
package com.usatiuk.objects.transaction;
public interface TransactionFactory {
TransactionPrivate createTransaction();

View File

@@ -1,14 +1,17 @@
package com.usatiuk.dhfs.objects.transaction;
package com.usatiuk.objects.transaction;
import com.usatiuk.dhfs.objects.*;
import com.usatiuk.dhfs.objects.persistence.IteratorStart;
import com.usatiuk.dhfs.objects.snapshot.SnapshotManager;
import com.usatiuk.objects.JData;
import com.usatiuk.objects.JDataVersionedWrapper;
import com.usatiuk.objects.JObjectKey;
import com.usatiuk.objects.iterators.*;
import com.usatiuk.objects.snapshot.Snapshot;
import com.usatiuk.objects.snapshot.SnapshotManager;
import io.quarkus.logging.Log;
import jakarta.enterprise.context.ApplicationScoped;
import jakarta.inject.Inject;
import org.apache.commons.lang3.tuple.Pair;
import org.eclipse.microprofile.config.inject.ConfigProperty;
import javax.annotation.Nonnull;
import java.util.*;
@ApplicationScoped
@@ -17,6 +20,8 @@ public class TransactionFactoryImpl implements TransactionFactory {
SnapshotManager snapshotManager;
@Inject
LockManager lockManager;
@ConfigProperty(name = "dhfs.objects.transaction.never-lock")
boolean neverLock;
@Override
public TransactionPrivate createTransaction() {
@@ -52,16 +57,147 @@ public class TransactionFactoryImpl implements TransactionFactory {
private class TransactionImpl implements TransactionPrivate {
private final Map<JObjectKey, TransactionObject<?>> _readSet = new HashMap<>();
private final NavigableMap<JObjectKey, TxRecord.TxObjectRecord<?>> _writes = new TreeMap<>();
private Map<JObjectKey, TxRecord.TxObjectRecord<?>> _newWrites = new HashMap<>();
private final List<Runnable> _onCommit = new ArrayList<>();
private final List<Runnable> _onFlush = new ArrayList<>();
private final SnapshotManager.Snapshot _snapshot;
private final Snapshot<JObjectKey, JDataVersionedWrapper> _snapshot;
private boolean _closed = false;
private Map<JObjectKey, TxRecord.TxObjectRecord<?>> _newWrites = new HashMap<>();
private TransactionImpl() {
_snapshot = snapshotManager.createSnapshot();
}
@Override
public void onCommit(Runnable runnable) {
_onCommit.add(runnable);
}
@Override
public void onFlush(Runnable runnable) {
_onFlush.add(runnable);
}
@Override
public Collection<Runnable> getOnCommit() {
return Collections.unmodifiableCollection(_onCommit);
}
@Override
public Snapshot<JObjectKey, JDataVersionedWrapper> snapshot() {
return _snapshot;
}
@Override
public Collection<Runnable> getOnFlush() {
return Collections.unmodifiableCollection(_onFlush);
}
@Override
public <T extends JData> Optional<T> getFromSource(Class<T> type, JObjectKey key) {
var got = _readSet.get(key);
if (got == null) {
var read = _snapshot.readObject(key);
_readSet.put(key, new TransactionObjectNoLock<>(read));
return read.map(JDataVersionedWrapper::data).map(type::cast);
}
return got.data().map(JDataVersionedWrapper::data).map(type::cast);
}
public <T extends JData> Optional<T> getWriteLockedFromSource(Class<T> type, JObjectKey key) {
var got = _readSet.get(key);
if (got == null) {
var lock = lockManager.lockObject(key);
try {
var read = _snapshot.readObject(key);
_readSet.put(key, new TransactionObjectLocked<>(read, lock));
return read.map(JDataVersionedWrapper::data).map(type::cast);
} catch (Exception e) {
lock.close();
throw e;
}
}
return got.data().map(JDataVersionedWrapper::data).map(type::cast);
}
@Override
public <T extends JData> Optional<T> get(Class<T> type, JObjectKey key, LockingStrategy strategy) {
switch (_writes.get(key)) {
case TxRecord.TxObjectRecordWrite<?> write -> {
return Optional.of(type.cast(write.data()));
}
case TxRecord.TxObjectRecordDeleted deleted -> {
return Optional.empty();
}
case null, default -> {
}
}
if (neverLock)
return getFromSource(type, key);
return switch (strategy) {
case OPTIMISTIC -> getFromSource(type, key);
case WRITE -> getWriteLockedFromSource(type, key);
};
}
@Override
public void delete(JObjectKey key) {
var got = _writes.get(key);
if (got != null) {
if (got instanceof TxRecord.TxObjectRecordDeleted) {
return;
}
}
_writes.put(key, new TxRecord.TxObjectRecordDeleted(key));
_newWrites.put(key, new TxRecord.TxObjectRecordDeleted(key));
}
@Override
public CloseableKvIterator<JObjectKey, JData> getIterator(IteratorStart start, JObjectKey key) {
Log.tracev("Getting tx iterator with start={0}, key={1}", start, key);
return new ReadTrackingIterator(new TombstoneMergingKvIterator<>("tx", start, key,
(tS, tK) -> new MappingKvIterator<>(new NavigableMapKvIterator<>(_writes, tS, tK),
t -> switch (t) {
case TxRecord.TxObjectRecordWrite<?> write ->
new Data<>(new ReadTrackingInternalCrapTx(write.data()));
case TxRecord.TxObjectRecordDeleted deleted -> new Tombstone<>();
case null, default -> null;
}),
(tS, tK) -> new MappingKvIterator<>(_snapshot.getIterator(tS, tK),
d -> new Data<ReadTrackingInternalCrap>(new ReadTrackingInternalCrapSource(d)))));
}
@Override
public void put(JData obj) {
_writes.put(obj.key(), new TxRecord.TxObjectRecordWrite<>(obj));
_newWrites.put(obj.key(), new TxRecord.TxObjectRecordWrite<>(obj));
}
@Override
public Collection<TxRecord.TxObjectRecord<?>> drainNewWrites() {
var ret = _newWrites;
_newWrites = new HashMap<>();
return ret.values();
}
@Override
public Map<JObjectKey, TransactionObject<?>> reads() {
return Collections.unmodifiableMap(_readSet);
}
@Override
public void close() {
if (_closed) return;
_closed = true;
_snapshot.close();
}
private class ReadTrackingIterator implements CloseableKvIterator<JObjectKey, JData> {
private final CloseableKvIterator<JObjectKey, ReadTrackingInternalCrap> _backing;
@@ -122,138 +258,5 @@ public class TransactionFactoryImpl implements TransactionFactory {
return Pair.of(got.getKey(), got.getValue().obj());
}
}
@Override
public void onCommit(Runnable runnable) {
_onCommit.add(runnable);
}
@Override
public void onFlush(Runnable runnable) {
_onFlush.add(runnable);
}
@Override
public Collection<Runnable> getOnCommit() {
return Collections.unmodifiableCollection(_onCommit);
}
@Override
public SnapshotManager.Snapshot snapshot() {
return _snapshot;
}
@Override
public Collection<Runnable> getOnFlush() {
return Collections.unmodifiableCollection(_onFlush);
}
@Override
public <T extends JData> Optional<T> getFromSource(Class<T> type, JObjectKey key) {
var got = _readSet.get(key);
if (got == null) {
var read = _snapshot.readObject(key);
_readSet.put(key, new TransactionObjectNoLock<>(read));
return read.map(JDataVersionedWrapper::data).map(type::cast);
}
return got.data().map(JDataVersionedWrapper::data).map(type::cast);
}
public <T extends JData> Optional<T> getWriteLockedFromSource(Class<T> type, JObjectKey key) {
var got = _readSet.get(key);
if (got == null) {
var lock = lockManager.lockObject(key);
try {
var read = _snapshot.readObject(key);
_readSet.put(key, new TransactionObjectLocked<>(read, lock));
return read.map(JDataVersionedWrapper::data).map(type::cast);
} catch (Exception e) {
lock.close();
throw e;
}
}
return got.data().map(JDataVersionedWrapper::data).map(type::cast);
}
@Override
public <T extends JData> Optional<T> get(Class<T> type, JObjectKey key, LockingStrategy strategy) {
switch (_writes.get(key)) {
case TxRecord.TxObjectRecordWrite<?> write -> {
return Optional.of(type.cast(write.data()));
}
case TxRecord.TxObjectRecordDeleted deleted -> {
return Optional.empty();
}
case null, default -> {
}
}
return switch (strategy) {
case OPTIMISTIC -> getFromSource(type, key);
case WRITE -> getWriteLockedFromSource(type, key);
};
}
@Override
public void delete(JObjectKey key) {
var got = _writes.get(key);
if (got != null) {
if (got instanceof TxRecord.TxObjectRecordDeleted) {
return;
}
}
_writes.put(key, new TxRecord.TxObjectRecordDeleted(key));
_newWrites.put(key, new TxRecord.TxObjectRecordDeleted(key));
}
@Nonnull
@Override
public Collection<JObjectKey> findAllObjects() {
// return store.findAllObjects();
return List.of();
}
@Override
public CloseableKvIterator<JObjectKey, JData> getIterator(IteratorStart start, JObjectKey key) {
Log.tracev("Getting tx iterator with start={0}, key={1}", start, key);
return new ReadTrackingIterator(new TombstoneMergingKvIterator<>("tx", start, key,
(tS, tK) -> new MappingKvIterator<>(new NavigableMapKvIterator<>(_writes, tS, tK),
t -> switch (t) {
case TxRecord.TxObjectRecordWrite<?> write ->
new Data<>(new ReadTrackingInternalCrapTx(write.data()));
case TxRecord.TxObjectRecordDeleted deleted -> new Tombstone<>();
case null, default -> null;
}),
(tS, tK) -> new MappingKvIterator<>(_snapshot.getIterator(tS, tK),
d -> new Data<ReadTrackingInternalCrap>(new ReadTrackingInternalCrapSource(d)))));
}
@Override
public void put(JData obj) {
_writes.put(obj.key(), new TxRecord.TxObjectRecordWrite<>(obj));
_newWrites.put(obj.key(), new TxRecord.TxObjectRecordWrite<>(obj));
}
@Override
public Collection<TxRecord.TxObjectRecord<?>> drainNewWrites() {
var ret = _newWrites;
_newWrites = new HashMap<>();
return ret.values();
}
@Override
public Map<JObjectKey, TransactionObject<?>> reads() {
return Collections.unmodifiableMap(_readSet);
}
@Override
public void close() {
_snapshot.close();
}
}
}

View File

@@ -1,4 +1,4 @@
package com.usatiuk.dhfs.objects.transaction;
package com.usatiuk.objects.transaction;
public interface TransactionHandle {
void onFlush(Runnable runnable);

View File

@@ -1,4 +1,4 @@
package com.usatiuk.dhfs.objects.transaction;
package com.usatiuk.objects.transaction;
import java.util.Collection;

View File

@@ -1,7 +1,5 @@
package com.usatiuk.dhfs.objects;
package com.usatiuk.objects.transaction;
import com.usatiuk.dhfs.objects.transaction.Transaction;
import com.usatiuk.dhfs.objects.transaction.TransactionHandle;
import com.usatiuk.dhfs.utils.VoidFn;
import io.quarkus.logging.Log;

View File

@@ -1,11 +1,11 @@
package com.usatiuk.dhfs.objects;
package com.usatiuk.objects.transaction;
import com.usatiuk.dhfs.objects.transaction.Transaction;
import com.usatiuk.dhfs.objects.transaction.TransactionHandle;
import com.usatiuk.dhfs.objects.transaction.TransactionPrivate;
import io.quarkus.logging.Log;
import jakarta.enterprise.context.ApplicationScoped;
import jakarta.inject.Inject;
import org.apache.commons.lang3.tuple.Pair;
import java.util.Collection;
@ApplicationScoped
public class TransactionManagerImpl implements TransactionManager {
@@ -31,8 +31,10 @@ public class TransactionManagerImpl implements TransactionManager {
}
Log.trace("Committing transaction");
Pair<Collection<Runnable>, TransactionHandle> ret;
try {
return jObjectManager.commit(_currentTransaction.get());
ret = jObjectManager.commit(_currentTransaction.get());
} catch (Throwable e) {
Log.trace("Transaction commit failed", e);
throw e;
@@ -40,6 +42,15 @@ public class TransactionManagerImpl implements TransactionManager {
_currentTransaction.get().close();
_currentTransaction.remove();
}
for (var r : ret.getLeft()) {
try {
r.run();
} catch (Throwable e) {
Log.error("Transaction commit hook error: ", e);
}
}
return ret.getRight();
}
@Override

View File

@@ -0,0 +1,10 @@
package com.usatiuk.objects.transaction;
import com.usatiuk.objects.JData;
import com.usatiuk.objects.JDataVersionedWrapper;
import java.util.Optional;
public interface TransactionObject<T extends JData> {
Optional<JDataVersionedWrapper> data();
}

View File

@@ -1,6 +1,7 @@
package com.usatiuk.dhfs.objects;
package com.usatiuk.objects.transaction;
import com.usatiuk.dhfs.objects.transaction.TransactionObject;
import com.usatiuk.objects.JData;
import com.usatiuk.objects.JDataVersionedWrapper;
import com.usatiuk.dhfs.utils.AutoCloseableNoThrow;
import java.util.Optional;

View File

@@ -1,6 +1,7 @@
package com.usatiuk.dhfs.objects;
package com.usatiuk.objects.transaction;
import com.usatiuk.dhfs.objects.transaction.TransactionObject;
import com.usatiuk.objects.JData;
import com.usatiuk.objects.JDataVersionedWrapper;
import java.util.Optional;

View File

@@ -1,8 +1,9 @@
package com.usatiuk.dhfs.objects.transaction;
package com.usatiuk.objects.transaction;
import com.usatiuk.dhfs.objects.JData;
import com.usatiuk.dhfs.objects.JObjectKey;
import com.usatiuk.dhfs.objects.snapshot.SnapshotManager;
import com.usatiuk.objects.JData;
import com.usatiuk.objects.JDataVersionedWrapper;
import com.usatiuk.objects.JObjectKey;
import com.usatiuk.objects.snapshot.Snapshot;
import com.usatiuk.dhfs.utils.AutoCloseableNoThrow;
import java.util.Collection;
@@ -19,5 +20,5 @@ public interface TransactionPrivate extends Transaction, TransactionHandlePrivat
Collection<Runnable> getOnCommit();
SnapshotManager.Snapshot snapshot();
Snapshot<JObjectKey, JDataVersionedWrapper> snapshot();
}

View File

@@ -1,4 +1,4 @@
package com.usatiuk.dhfs.objects;
package com.usatiuk.objects.transaction;
public class TxCommitException extends RuntimeException {
public TxCommitException(String message) {

View File

@@ -1,7 +1,7 @@
package com.usatiuk.dhfs.objects.transaction;
package com.usatiuk.objects.transaction;
import com.usatiuk.dhfs.objects.JData;
import com.usatiuk.dhfs.objects.JObjectKey;
import com.usatiuk.objects.JData;
import com.usatiuk.objects.JObjectKey;
public class TxRecord {
public interface TxObjectRecord<T> {

View File

@@ -4,5 +4,7 @@ dhfs.objects.lru.limit=134217728
dhfs.objects.lru.print-stats=true
dhfs.objects.lock_timeout_secs=15
dhfs.objects.persistence.files.root=${HOME}/dhfs_default/data/objs
quarkus.package.jar.decompiler.enabled=true
dhfs.objects.persistence.snapshot-extra-checks=false
dhfs.objects.persistence.snapshot-extra-checks=false
dhfs.objects.transaction.never-lock=true
quarkus.log.category."com.usatiuk.objects.iterators".level=INFO
quarkus.log.category."com.usatiuk.objects.iterators".min-level=INFO

View File

@@ -1,11 +0,0 @@
package com.usatiuk.dhfs.objects.snapshot;
import com.usatiuk.dhfs.objects.JObjectKey;
import org.junit.jupiter.api.Test;
import java.util.Map;
public class SnapshotKvIteratorTest {
}

View File

@@ -1,4 +1,4 @@
package com.usatiuk.dhfs.objects;
package com.usatiuk.objects;
import org.junit.jupiter.api.Assertions;

View File

@@ -1,9 +1,9 @@
package com.usatiuk.dhfs.objects;
package com.usatiuk.objects;
import io.quarkus.test.junit.QuarkusTest;
import io.quarkus.test.junit.TestProfile;
@QuarkusTest
@TestProfile(Profiles.ObjectsTestProfileExtraChecks.class)
public class ObjectsTestExtraChecks extends ObjectsTestImpl {
public class ObjectsTestExtraChecksTest extends ObjectsTestImpl {
}

View File

@@ -1,9 +1,10 @@
package com.usatiuk.dhfs.objects;
package com.usatiuk.objects;
import com.usatiuk.dhfs.objects.data.Parent;
import com.usatiuk.dhfs.objects.persistence.IteratorStart;
import com.usatiuk.dhfs.objects.transaction.LockingStrategy;
import com.usatiuk.dhfs.objects.transaction.Transaction;
import com.usatiuk.objects.data.Parent;
import com.usatiuk.objects.iterators.IteratorStart;
import com.usatiuk.objects.transaction.LockingStrategy;
import com.usatiuk.objects.transaction.Transaction;
import com.usatiuk.objects.transaction.TransactionManager;
import io.quarkus.logging.Log;
import jakarta.inject.Inject;
import org.junit.jupiter.api.Assertions;
@@ -68,6 +69,30 @@ public abstract class ObjectsTestImpl {
});
}
@Test
void onCommitHookTest() {
txm.run(() -> {
var newParent = new Parent(JObjectKey.of("ParentOnCommitHook"), "John");
curTx.put(newParent);
curTx.onCommit(() -> txm.run(() -> {
curTx.put(new Parent(JObjectKey.of("ParentOnCommitHook2"), "John2"));
}));
});
txm.run(() -> {
curTx.onCommit(() -> txm.run(() -> {
curTx.put(new Parent(JObjectKey.of("ParentOnCommitHook3"), "John3"));
}));
});
txm.run(() -> {
var parent = curTx.get(Parent.class, new JObjectKey("ParentOnCommitHook")).orElse(null);
Assertions.assertEquals("John", parent.name());
var parent2 = curTx.get(Parent.class, new JObjectKey("ParentOnCommitHook2")).orElse(null);
Assertions.assertEquals("John2", parent2.name());
var parent3 = curTx.get(Parent.class, new JObjectKey("ParentOnCommitHook3")).orElse(null);
Assertions.assertEquals("John3", parent3.name());
});
}
@Test
void createGetObject() {
txm.run(() -> {

View File

@@ -1,9 +1,9 @@
package com.usatiuk.dhfs.objects;
package com.usatiuk.objects;
import io.quarkus.test.junit.QuarkusTest;
import io.quarkus.test.junit.TestProfile;
@QuarkusTest
@TestProfile(Profiles.ObjectsTestProfileNoExtraChecks.class)
public class ObjectsTestNoExtraChecks extends ObjectsTestImpl {
public class ObjectsTestNoExtraChecksTest extends ObjectsTestImpl {
}

View File

@@ -1,7 +1,9 @@
package com.usatiuk.dhfs.objects;
package com.usatiuk.objects;
import com.usatiuk.dhfs.objects.data.Parent;
import com.usatiuk.dhfs.objects.transaction.Transaction;
import com.usatiuk.objects.data.Parent;
import com.usatiuk.objects.transaction.PreCommitTxHook;
import com.usatiuk.objects.transaction.Transaction;
import com.usatiuk.objects.transaction.TransactionManager;
import io.quarkus.test.junit.QuarkusTest;
import io.quarkus.test.junit.TestProfile;
import io.quarkus.test.junit.mockito.InjectSpy;

View File

@@ -1,4 +1,4 @@
package com.usatiuk.dhfs.objects;
package com.usatiuk.objects;
import io.quarkus.test.junit.QuarkusTestProfile;

View File

@@ -1,4 +1,4 @@
package com.usatiuk.dhfs.objects;
package com.usatiuk.objects;
import io.quarkus.logging.Log;
import io.quarkus.runtime.ShutdownEvent;

View File

@@ -1,7 +1,7 @@
package com.usatiuk.dhfs.objects.data;
package com.usatiuk.objects.data;
import com.usatiuk.dhfs.objects.JData;
import com.usatiuk.dhfs.objects.JObjectKey;
import com.usatiuk.objects.JData;
import com.usatiuk.objects.JObjectKey;
public record Kid(JObjectKey key, String name) implements JData {
public Kid withName(String name) {

View File

@@ -1,7 +1,7 @@
package com.usatiuk.dhfs.objects.data;
package com.usatiuk.objects.data;
import com.usatiuk.dhfs.objects.JData;
import com.usatiuk.dhfs.objects.JObjectKey;
import com.usatiuk.objects.JData;
import com.usatiuk.objects.JObjectKey;
public record Parent(JObjectKey key, String name) implements JData {
public Parent withName(String name) {

View File

@@ -1,6 +1,6 @@
package com.usatiuk.dhfs.objects;
package com.usatiuk.objects.iterators;
import com.usatiuk.dhfs.objects.persistence.IteratorStart;
import com.usatiuk.objects.Just;
import org.apache.commons.lang3.tuple.Pair;
import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.Test;

View File

@@ -1,6 +1,6 @@
package com.usatiuk.dhfs.objects;
package com.usatiuk.objects.iterators;
import com.usatiuk.dhfs.objects.persistence.IteratorStart;
import com.usatiuk.objects.Just;
import org.apache.commons.lang3.tuple.Pair;
import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.Test;
@@ -12,80 +12,6 @@ import java.util.NoSuchElementException;
public class MergingKvIteratorTest {
private class SimpleIteratorWrapper<K extends Comparable<K>, V> implements CloseableKvIterator<K, V> {
private final Iterator<Pair<K, V>> _iterator;
private Pair<K, V> _next;
public SimpleIteratorWrapper(Iterator<Pair<K, V>> iterator) {
_iterator = iterator;
fillNext();
}
private void fillNext() {
while (_iterator.hasNext() && _next == null) {
_next = _iterator.next();
}
}
@Override
public K peekNextKey() {
if (_next == null) {
throw new NoSuchElementException();
}
return _next.getKey();
}
@Override
public void skip() {
if (_next == null) {
throw new NoSuchElementException();
}
_next = null;
fillNext();
}
@Override
public K peekPrevKey() {
throw new UnsupportedOperationException();
}
@Override
public Pair<K, V> prev() {
throw new UnsupportedOperationException();
}
@Override
public boolean hasPrev() {
throw new UnsupportedOperationException();
}
@Override
public void skipPrev() {
throw new UnsupportedOperationException();
}
@Override
public void close() {
}
@Override
public boolean hasNext() {
return _next != null;
}
@Override
public Pair<K, V> next() {
if (_next == null) {
throw new NoSuchElementException("No more elements");
}
var ret = _next;
_next = null;
fillNext();
return ret;
}
}
@Test
public void testTestIterator() {
var list = List.of(Pair.of(1, 2), Pair.of(3, 4), Pair.of(5, 6));
@@ -345,4 +271,78 @@ public class MergingKvIteratorTest {
}
Assertions.assertFalse(mergingIterator2.hasNext());
}
private class SimpleIteratorWrapper<K extends Comparable<K>, V> implements CloseableKvIterator<K, V> {
private final Iterator<Pair<K, V>> _iterator;
private Pair<K, V> _next;
public SimpleIteratorWrapper(Iterator<Pair<K, V>> iterator) {
_iterator = iterator;
fillNext();
}
private void fillNext() {
while (_iterator.hasNext() && _next == null) {
_next = _iterator.next();
}
}
@Override
public K peekNextKey() {
if (_next == null) {
throw new NoSuchElementException();
}
return _next.getKey();
}
@Override
public void skip() {
if (_next == null) {
throw new NoSuchElementException();
}
_next = null;
fillNext();
}
@Override
public K peekPrevKey() {
throw new UnsupportedOperationException();
}
@Override
public Pair<K, V> prev() {
throw new UnsupportedOperationException();
}
@Override
public boolean hasPrev() {
throw new UnsupportedOperationException();
}
@Override
public void skipPrev() {
throw new UnsupportedOperationException();
}
@Override
public void close() {
}
@Override
public boolean hasNext() {
return _next != null;
}
@Override
public Pair<K, V> next() {
if (_next == null) {
throw new NoSuchElementException("No more elements");
}
var ret = _next;
_next = null;
fillNext();
return ret;
}
}
}

View File

@@ -1,6 +1,6 @@
package com.usatiuk.dhfs.objects;
package com.usatiuk.objects.iterators;
import com.usatiuk.dhfs.objects.persistence.IteratorStart;
import com.usatiuk.objects.Just;
import org.apache.commons.lang3.tuple.Pair;
import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.Test;

View File

@@ -1,6 +1,6 @@
package com.usatiuk.dhfs.objects;
package com.usatiuk.objects.iterators;
import com.usatiuk.dhfs.objects.persistence.IteratorStart;
import com.usatiuk.objects.Just;
import org.apache.commons.lang3.tuple.Pair;
import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.Test;

View File

@@ -0,0 +1,6 @@
package com.usatiuk.objects.snapshot;
public class SnapshotKvIteratorTest {
}

View File

@@ -1,10 +1,11 @@
package com.usatiuk.dhfs.objects.persistence;
package com.usatiuk.objects.stores;
import com.google.protobuf.ByteString;
import com.usatiuk.dhfs.objects.JObjectKey;
import com.usatiuk.dhfs.objects.Just;
import com.usatiuk.dhfs.objects.TempDataProfile;
import com.usatiuk.objects.JObjectKey;
import com.usatiuk.objects.Just;
import com.usatiuk.objects.TempDataProfile;
import com.usatiuk.objects.iterators.IteratorStart;
import io.quarkus.test.junit.QuarkusTest;
import io.quarkus.test.junit.TestProfile;
import jakarta.inject.Inject;
@@ -37,7 +38,14 @@ public class LmdbKvIteratorTest {
), -1, Runnable::run
);
var iterator = store.getIterator(IteratorStart.LE, JObjectKey.of(Long.toString(3)));
var iterator = store.getIterator(IteratorStart.GE, JObjectKey.of(""));
Just.checkIterator(iterator, List.of(Pair.of(JObjectKey.of(Long.toString(1)), ByteString.copyFrom(new byte[]{2})),
Pair.of(JObjectKey.of(Long.toString(2)), ByteString.copyFrom(new byte[]{3})),
Pair.of(JObjectKey.of(Long.toString(3)), ByteString.copyFrom(new byte[]{4}))));
Assertions.assertFalse(iterator.hasNext());
iterator.close();
iterator = store.getIterator(IteratorStart.LE, JObjectKey.of(Long.toString(3)));
Just.checkIterator(iterator, Pair.of(JObjectKey.of(Long.toString(3)), ByteString.copyFrom(new byte[]{4})));
Assertions.assertFalse(iterator.hasNext());
iterator.close();

View File

@@ -29,7 +29,7 @@
<project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
<quarkus.platform.artifact-id>quarkus-bom</quarkus.platform.artifact-id>
<quarkus.platform.group-id>io.quarkus.platform</quarkus.platform.group-id>
<quarkus.platform.version>3.15.2</quarkus.platform.version>
<quarkus.platform.version>3.20.0</quarkus.platform.version>
<surefire-plugin.version>3.5.2</surefire-plugin.version>
<dhfs.native-libs-dir>${project.parent.build.outputDirectory}/native</dhfs.native-libs-dir>
</properties>

View File

@@ -1,5 +0,0 @@
*
!target/*-runner
!target/*-runner.jar
!target/lib/*
!target/quarkus-app/*

View File

@@ -1,43 +0,0 @@
#Maven
target/
pom.xml.tag
pom.xml.releaseBackup
pom.xml.versionsBackup
release.properties
.flattened-pom.xml
# Eclipse
.project
.classpath
.settings/
bin/
# IntelliJ
.idea
*.ipr
*.iml
*.iws
# NetBeans
nb-configuration.xml
# Visual Studio Code
.vscode
.factorypath
# OSX
.DS_Store
# Vim
*.swp
*.swo
# patch
*.orig
*.rej
# Local environment
.env
# Plugin directory
/.quarkus/cli/plugins/

View File

@@ -1,2 +0,0 @@
FROM azul/zulu-openjdk-debian:21-jre-latest
RUN apt update && apt install -y libfuse2 curl

Some files were not shown because too many files have changed in this diff Show More