mirror of
				https://github.com/usatiuk/dhfs.git
				synced 2025-10-28 20:47:49 +01:00 
			
		
		
		
	Compare commits
	
		
			71 Commits
		
	
	
		
			lazy-reads
			...
			objects-ma
		
	
	| Author | SHA1 | Date | |
|---|---|---|---|
| fe20db9b3b | |||
| 8b3c0a6f2c | |||
| 4a19f69c38 | |||
| 8d40019f2c | |||
| 312cf18b27 | |||
| 6a8394852e | |||
| 3e69e5dfb9 | |||
| 8fdbaf5aa7 | |||
| 4d44e3541b | |||
| 18d5a7f90e | |||
| adcc5f464f | |||
| d9ded36891 | |||
| 038b873364 | |||
| 8f7869d87a | |||
| e0b4f97349 | |||
| 035f64df5a | |||
| 4c5fd91050 | |||
| 8559c9b984 | |||
| e80e33568b | |||
| 03850d3522 | |||
| 527395447c | |||
| 9108b27dd3 | |||
| 3bd0c4e2bb | |||
| c977b5f6c9 | |||
| c5a875c27f | |||
| ba6bb756bb | |||
| a63e7e59b3 | |||
| 9a02a554a1 | |||
| 892e5ca9b7 | |||
| c12bff3ee7 | |||
| 59a0b9a856 | |||
| 817d12a161 | |||
| 258c257778 | |||
| b0bb9121e7 | |||
| a224c6bd51 | |||
| 13ecdd3106 | |||
| 8a07f37566 | |||
| dc0e73b1aa | |||
| 16eb1d28d9 | |||
| 4f397cd2d4 | |||
| 6a20550353 | |||
| 92bca1e4e1 | |||
| 4bfa93fca4 | |||
| 7d762c70fa | |||
| 20daa857e6 | |||
| 97c0f002fb | |||
| f260bb0491 | |||
| a2e75dbdc7 | |||
| fa64dac9aa | |||
| 8fbdf50732 | |||
| be1f5d12c9 | |||
| 1fd3b9e5e0 | |||
| 8499e20823 | |||
| 842bd49246 | |||
| 1b0af6e883 | |||
| 667f8b3b42 | |||
| 0aca2c5dbb | |||
| 223ba20418 | |||
| ae17ab6ce9 | |||
| 6e37320e7c | |||
| d37dc944d0 | |||
| d483eba20d | |||
| 4cbb4ce2be | |||
| 5f85e944e3 | |||
| 4c90a74fea | |||
| 38ab6de85b | |||
| 29fd2826a3 | |||
| 3faab4c324 | |||
| 5071cd908a | |||
| 3470ce8690 | |||
| 1d22465e4a | 
							
								
								
									
										2
									
								
								.github/workflows/server.yml
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										2
									
								
								.github/workflows/server.yml
									
									
									
									
										vendored
									
									
								
							| @@ -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 | ||||
|   | ||||
							
								
								
									
										17
									
								
								dhfs-parent/.run/Main 2.run.xml
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										17
									
								
								dhfs-parent/.run/Main 2.run.xml
									
									
									
									
									
										Normal 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> | ||||
							
								
								
									
										18
									
								
								dhfs-parent/.run/Main.run.xml
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										18
									
								
								dhfs-parent/.run/Main.run.xml
									
									
									
									
									
										Normal 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> | ||||
| @@ -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; | ||||
|   | ||||
| @@ -82,7 +82,7 @@ | ||||
|  | ||||
|     <profiles> | ||||
|         <profile> | ||||
|             <id>native-image</id> | ||||
|             <id>native</id> | ||||
|             <activation> | ||||
|                 <property> | ||||
|                     <name>native</name> | ||||
|   | ||||
| @@ -1 +0,0 @@ | ||||
| quarkus.package.jar.decompiler.enabled=true | ||||
| @@ -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> | ||||
| @@ -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); | ||||
|         } | ||||
|     } | ||||
|   | ||||
| @@ -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(); | ||||
|     } | ||||
| } | ||||
|   | ||||
| @@ -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(); | ||||
|     } | ||||
| } | ||||
|   | ||||
| @@ -9,6 +9,8 @@ public interface StorageInterface< | ||||
|  | ||||
|     NodeIdT getTrashId(); | ||||
|  | ||||
|     NodeIdT getLostFoundId(); | ||||
|  | ||||
|     NodeIdT getNewNodeId(); | ||||
|  | ||||
|     TreeNode<TimestampT, PeerIdT, MetaT, NodeIdT> getById(NodeIdT id); | ||||
|   | ||||
| @@ -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); | ||||
|   | ||||
| @@ -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"))); | ||||
|     } | ||||
| } | ||||
|   | ||||
| @@ -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; | ||||
|   | ||||
| @@ -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(); | ||||
|     } | ||||
| } | ||||
| @@ -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 + | ||||
|                 '}'; | ||||
|     } | ||||
| } | ||||
| @@ -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(); | ||||
|     } | ||||
| } | ||||
| @@ -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(); | ||||
|     } | ||||
| } | ||||
| @@ -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); | ||||
|     } | ||||
| } | ||||
| @@ -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(); | ||||
| } | ||||
| @@ -1,4 +1,4 @@ | ||||
| package com.usatiuk.dhfs.objects; | ||||
| package com.usatiuk.objects; | ||||
| 
 | ||||
| import java.io.Serializable; | ||||
| 
 | ||||
| @@ -1,4 +1,4 @@ | ||||
| package com.usatiuk.dhfs.objects; | ||||
| package com.usatiuk.objects; | ||||
| 
 | ||||
| public interface JDataVersionedWrapper { | ||||
|     JData data(); | ||||
| @@ -1,4 +1,4 @@ | ||||
| package com.usatiuk.dhfs.objects; | ||||
| package com.usatiuk.objects; | ||||
| 
 | ||||
| import jakarta.annotation.Nonnull; | ||||
| 
 | ||||
| @@ -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; | ||||
|     } | ||||
| } | ||||
| @@ -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)); | ||||
|     } | ||||
| } | ||||
| @@ -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()); | ||||
|     } | ||||
| } | ||||
| @@ -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); | ||||
|         } | ||||
|     } | ||||
| } | ||||
| @@ -1,4 +1,4 @@ | ||||
| package com.usatiuk.dhfs.objects; | ||||
| package com.usatiuk.objects; | ||||
| 
 | ||||
| import com.google.protobuf.ByteString; | ||||
| 
 | ||||
| @@ -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); | ||||
|     } | ||||
| } | ||||
| @@ -1,4 +1,4 @@ | ||||
| package com.usatiuk.dhfs.objects; | ||||
| package com.usatiuk.objects.iterators; | ||||
| 
 | ||||
| import java.util.Optional; | ||||
| 
 | ||||
| @@ -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> { | ||||
| @@ -1,4 +1,4 @@ | ||||
| package com.usatiuk.dhfs.objects.persistence; | ||||
| package com.usatiuk.objects.iterators; | ||||
| 
 | ||||
| public enum IteratorStart { | ||||
|     LT, | ||||
| @@ -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; | ||||
| @@ -1,4 +1,4 @@ | ||||
| package com.usatiuk.dhfs.objects; | ||||
| package com.usatiuk.objects.iterators; | ||||
| 
 | ||||
| import org.apache.commons.lang3.tuple.Pair; | ||||
| 
 | ||||
| @@ -1,4 +1,4 @@ | ||||
| package com.usatiuk.dhfs.objects; | ||||
| package com.usatiuk.objects.iterators; | ||||
| 
 | ||||
| import java.util.Optional; | ||||
| 
 | ||||
| @@ -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> { | ||||
|     } | ||||
| } | ||||
| @@ -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.*; | ||||
| @@ -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; | ||||
|     } | ||||
| 
 | ||||
| @@ -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) { | ||||
| @@ -1,4 +1,4 @@ | ||||
| package com.usatiuk.dhfs.objects; | ||||
| package com.usatiuk.objects.iterators; | ||||
| 
 | ||||
| import org.apache.commons.lang3.tuple.Pair; | ||||
| 
 | ||||
| @@ -1,4 +1,4 @@ | ||||
| package com.usatiuk.dhfs.objects; | ||||
| package com.usatiuk.objects.iterators; | ||||
| 
 | ||||
| import java.util.Optional; | ||||
| 
 | ||||
| @@ -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; | ||||
| 
 | ||||
| @@ -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(); | ||||
| } | ||||
| @@ -1,4 +1,4 @@ | ||||
| package com.usatiuk.dhfs.objects.snapshot; | ||||
| package com.usatiuk.objects.snapshot; | ||||
| 
 | ||||
| public interface SnapshotEntry { | ||||
|     long whenToRemove(); | ||||
| @@ -1,4 +1,4 @@ | ||||
| package com.usatiuk.dhfs.objects.snapshot; | ||||
| package com.usatiuk.objects.snapshot; | ||||
| 
 | ||||
| public record SnapshotEntryDeleted(long whenToRemove) implements SnapshotEntry { | ||||
|     @Override | ||||
| @@ -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 | ||||
| @@ -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; | ||||
| @@ -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; | ||||
| @@ -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); | ||||
|     } | ||||
| } | ||||
| @@ -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(); | ||||
|     } | ||||
| } | ||||
| @@ -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) { | ||||
|     } | ||||
| } | ||||
| @@ -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(); | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
| } | ||||
| @@ -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(); | ||||
| @@ -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 | ||||
| @@ -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 { | ||||
| } | ||||
| @@ -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 { | ||||
| } | ||||
| @@ -1,4 +1,4 @@ | ||||
| package com.usatiuk.dhfs.objects; | ||||
| package com.usatiuk.objects.stores; | ||||
| 
 | ||||
| public interface PendingWriteEntry { | ||||
|     long bundleId(); | ||||
| @@ -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(); | ||||
|     } | ||||
| } | ||||
| @@ -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; | ||||
| @@ -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; | ||||
| @@ -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(); | ||||
|         } | ||||
|     } | ||||
| } | ||||
| @@ -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); | ||||
| @@ -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); | ||||
| @@ -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); | ||||
|     } | ||||
| } | ||||
| @@ -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 | ||||
| @@ -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) { | ||||
| @@ -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); | ||||
|     } | ||||
| @@ -1,4 +1,4 @@ | ||||
| package com.usatiuk.dhfs.objects.transaction; | ||||
| package com.usatiuk.objects.transaction; | ||||
| 
 | ||||
| public interface TransactionFactory { | ||||
|     TransactionPrivate createTransaction(); | ||||
| @@ -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(); | ||||
|         } | ||||
|     } | ||||
| } | ||||
| @@ -1,4 +1,4 @@ | ||||
| package com.usatiuk.dhfs.objects.transaction; | ||||
| package com.usatiuk.objects.transaction; | ||||
| 
 | ||||
| public interface TransactionHandle { | ||||
|     void onFlush(Runnable runnable); | ||||
| @@ -1,4 +1,4 @@ | ||||
| package com.usatiuk.dhfs.objects.transaction; | ||||
| package com.usatiuk.objects.transaction; | ||||
| 
 | ||||
| import java.util.Collection; | ||||
| 
 | ||||
| @@ -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; | ||||
| 
 | ||||
| @@ -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 | ||||
| @@ -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(); | ||||
| } | ||||
| @@ -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; | ||||
| @@ -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; | ||||
| 
 | ||||
| @@ -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(); | ||||
| } | ||||
| @@ -1,4 +1,4 @@ | ||||
| package com.usatiuk.dhfs.objects; | ||||
| package com.usatiuk.objects.transaction; | ||||
| 
 | ||||
| public class TxCommitException extends RuntimeException { | ||||
|     public TxCommitException(String message) { | ||||
| @@ -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> { | ||||
| @@ -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 | ||||
|   | ||||
| @@ -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 { | ||||
|  | ||||
|  | ||||
| } | ||||
| @@ -1,4 +1,4 @@ | ||||
| package com.usatiuk.dhfs.objects; | ||||
| package com.usatiuk.objects; | ||||
| 
 | ||||
| import org.junit.jupiter.api.Assertions; | ||||
| 
 | ||||
| @@ -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 { | ||||
| } | ||||
| @@ -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(() -> { | ||||
| @@ -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 { | ||||
| } | ||||
| @@ -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; | ||||
| @@ -1,4 +1,4 @@ | ||||
| package com.usatiuk.dhfs.objects; | ||||
| package com.usatiuk.objects; | ||||
| 
 | ||||
| import io.quarkus.test.junit.QuarkusTestProfile; | ||||
| 
 | ||||
| @@ -1,4 +1,4 @@ | ||||
| package com.usatiuk.dhfs.objects; | ||||
| package com.usatiuk.objects; | ||||
| 
 | ||||
| import io.quarkus.logging.Log; | ||||
| import io.quarkus.runtime.ShutdownEvent; | ||||
| @@ -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) { | ||||
| @@ -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) { | ||||
| @@ -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; | ||||
| @@ -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; | ||||
|         } | ||||
|     } | ||||
| } | ||||
| @@ -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; | ||||
| @@ -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; | ||||
| @@ -0,0 +1,6 @@ | ||||
| package com.usatiuk.objects.snapshot; | ||||
|  | ||||
| public class SnapshotKvIteratorTest { | ||||
|  | ||||
|  | ||||
| } | ||||
| @@ -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(); | ||||
| @@ -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> | ||||
|   | ||||
| @@ -1,5 +0,0 @@ | ||||
| * | ||||
| !target/*-runner | ||||
| !target/*-runner.jar | ||||
| !target/lib/* | ||||
| !target/quarkus-app/* | ||||
							
								
								
									
										43
									
								
								dhfs-parent/server-old/.gitignore
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										43
									
								
								dhfs-parent/server-old/.gitignore
									
									
									
									
										vendored
									
									
								
							| @@ -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/ | ||||
| @@ -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
		Reference in New Issue
	
	Block a user