1313import java .nio .ByteBuffer ;
1414import java .util .*;
1515import java .util .concurrent .ConcurrentHashMap ;
16+ import java .util .concurrent .atomic .AtomicBoolean ;
1617import java .util .concurrent .locks .ReadWriteLock ;
1718import java .util .concurrent .locks .ReentrantReadWriteLock ;
1819
@@ -71,181 +72,10 @@ public class MerkleTree {
7172 private int depth = 0 ;
7273 private byte [] rootHash = null ;
7374
74- private boolean closed = false ;
75+ private AtomicBoolean closed = new AtomicBoolean ( false ) ;
7576 //endregion
7677
7778 //region ===================== Constructors =====================
78- // @SneakyThrows
79- // public MerkleTree(String treeName) {
80- // errorIf(openTrees.containsKey(treeName), "There is already open instance of this tree. 2 open instances of the same tree are not allowed at the same time");
81- // this.treeName = treeName;
82- //
83- // Options options = new Options().setCreateIfMissing(true)
84- // .setMaxTotalWalSize(45 * 1024 * 1024L) // Good: Limits total WAL to 512MB
85- // .setWalSizeLimitMB(15) // Good: Standard size per WAL file
86- // .setWalTtlSeconds(24 * 60 * 60) // Good: Cleans up old WAL files after 24h
87- // .setInfoLogLevel(InfoLogLevel.FATAL_LEVEL) // Good: Minimizes logging overhead
88- // .setDbLogDir("") // Good: Disables separate log directory
89- // .setLogFileTimeToRoll(0); // Good: Immediate roll when size limit reached
90- //
91- // // Add these additional safety options
92- // options.setAllowMmapReads(false) // Disable memory mapping
93- // .setAllowMmapWrites(false)
94- // .setMaxOpenFiles(1000)
95- // .setMaxFileOpeningThreads(10)
96- // .setIncreaseParallelism(1); // Single-threaded mode is safer
97- //
98- // options.setParanoidChecks(true) // Enable paranoid checks for corruption
99- // .setUseDirectReads(true) // Direct I/O for reads
100- // .setUseDirectIoForFlushAndCompaction(true) // Direct I/O for writes
101- // .setEnableWriteThreadAdaptiveYield(true)
102- // .setAllowConcurrentMemtableWrite(true);
103- //
104- // File directory = new File("merkleTree/");
105- // if(!directory.exists()) directory.mkdirs();
106- //
107- // this.db = RocksDB.open(options, "merkleTree/" + treeName);
108- //
109- // final ColumnFamilyDescriptor db1Descriptor =
110- // new ColumnFamilyDescriptor(METADATA_DB_NAME.getBytes());
111- // final ColumnFamilyDescriptor db2Descriptor =
112- // new ColumnFamilyDescriptor(NODES_DB_NAME.getBytes());
113- //
114- // metaDataHandle = db.createColumnFamily(db1Descriptor);
115- // nodesHandle = db.createColumnFamily(db2Descriptor);
116- //
117- // loadMetaData();
118- //
119- // //Shutdown hook
120- // Runtime.getRuntime().addShutdownHook(new Thread(() -> {
121- // try {
122- // close();
123- // } catch (RocksDBException e) {
124- // e.printStackTrace();
125- // }
126- // })
127- // );
128- //
129- // openTrees.put(treeName, this);
130- // }
131-
132- // public MerkleTree(String treeName) throws RocksDBException {
133- // RocksDB.loadLibrary();
134- // this.treeName = treeName;
135- // errorIf(openTrees.containsKey(treeName), "There is already open instance of this tree. 2 open instances of the same tree are not allowed at the same time");
136- //
137- // DBOptions dbOptions = new DBOptions().setCreateIfMissing(true)
138- // .setMaxTotalWalSize(45 * 1024 * 1024L) // Good: Limits total WAL to 512MB
139- // .setWalSizeLimitMB(15) // Good: Standard size per WAL file
140- // .setWalTtlSeconds(24 * 60 * 60) // Good: Cleans up old WAL files after 24h
141- // .setInfoLogLevel(InfoLogLevel.FATAL_LEVEL) // Good: Minimizes logging overhead
142- // .setDbLogDir("") // Good: Disables separate log directory
143- // .setLogFileTimeToRoll(0); // Good: Immediate roll when size limit reached
144- //
145- // // Add these additional safety options
146- // dbOptions.setAllowMmapReads(false) // Disable memory mapping
147- // .setAllowMmapWrites(false)
148- // .setMaxOpenFiles(1000)
149- // .setMaxFileOpeningThreads(10)
150- // .setIncreaseParallelism(1); // Single-threaded mode is safer
151- //
152- // dbOptions.setParanoidChecks(true) // Enable paranoid checks for corruption
153- // .setUseDirectReads(true) // Direct I/O for reads
154- // .setUseDirectIoForFlushAndCompaction(true) // Direct I/O for writes
155- // .setEnableWriteThreadAdaptiveYield(true)
156- // .setAllowConcurrentMemtableWrite(true);
157- // // set your other options here...
158- //
159- // String dbPath = "merkleTree/" + treeName;
160- //
161- //
162- // Options options = new Options().setCreateIfMissing(true)
163- // .setMaxTotalWalSize(45 * 1024 * 1024L) // Good: Limits total WAL to 512MB
164- // .setWalSizeLimitMB(15) // Good: Standard size per WAL file
165- // .setWalTtlSeconds(24 * 60 * 60) // Good: Cleans up old WAL files after 24h
166- // .setInfoLogLevel(InfoLogLevel.FATAL_LEVEL) // Good: Minimizes logging overhead
167- // .setDbLogDir("") // Good: Disables separate log directory
168- // .setLogFileTimeToRoll(0); // Good: Immediate roll when size limit reached
169- //
170- // // Add these additional safety options
171- // options.setAllowMmapReads(false) // Disable memory mapping
172- // .setAllowMmapWrites(false)
173- // .setMaxOpenFiles(1000)
174- // .setMaxFileOpeningThreads(10)
175- // .setIncreaseParallelism(1); // Single-threaded mode is safer
176- //
177- // options.setParanoidChecks(true) // Enable paranoid checks for corruption
178- // .setUseDirectReads(true) // Direct I/O for reads
179- // .setUseDirectIoForFlushAndCompaction(true) // Direct I/O for writes
180- // .setEnableWriteThreadAdaptiveYield(true)
181- // .setAllowConcurrentMemtableWrite(true);
182- //
183- // // 1) Figure out which column families already exist
184- // List<byte[]> existingCFNames = RocksDB.listColumnFamilies(options, dbPath);
185- //
186- // List<ColumnFamilyDescriptor> cfDescriptors = new ArrayList<>();
187- // if (existingCFNames.isEmpty()) {
188- // // Means this is a brand new DB -- no column families yet
189- // // We always need the default CF
190- // cfDescriptors.add(new ColumnFamilyDescriptor(
191- // RocksDB.DEFAULT_COLUMN_FAMILY,
192- // new ColumnFamilyOptions())
193- // );
194- //
195- // // Also create metaData CF
196- // cfDescriptors.add(new ColumnFamilyDescriptor(
197- // METADATA_DB_NAME.getBytes(),
198- // new ColumnFamilyOptions())
199- // );
200- //
201- // // Also create nodes CF
202- // cfDescriptors.add(new ColumnFamilyDescriptor(
203- // NODES_DB_NAME.getBytes(),
204- // new ColumnFamilyOptions())
205- // );
206- // } else {
207- // // We already have some (or all) CFs in the DB. We must open them *all*.
208- // for (byte[] cfName : existingCFNames) {
209- // cfDescriptors.add(
210- // new ColumnFamilyDescriptor(cfName, new ColumnFamilyOptions())
211- // );
212- // }
213- // }
214- //
215- // // 2) Open DB with all column family descriptors
216- // List<ColumnFamilyHandle> cfHandles = new ArrayList<>();
217- // this.db = RocksDB.open(dbOptions, dbPath, cfDescriptors, cfHandles);
218- //
219- // // 3) Figure out which handle corresponds to metaData, which to nodes
220- // // They come back in the same order we put them in cfDescriptors.
221- // for (int i = 0; i < cfDescriptors.size(); i++) {
222- // String cfName = new String(cfDescriptors.get(i).getName());
223- // if (cfName.equals(METADATA_DB_NAME)) {
224- // metaDataHandle = cfHandles.get(i);
225- // } else if (cfName.equals(NODES_DB_NAME)) {
226- // nodesHandle = cfHandles.get(i);
227- // } else if (cfName.equals("default")) {
228- // // If you need the default CF handle, grab it here
229- // }
230- // }
231- //
232- // // If we found that we do NOT have metaDataHandle or nodesHandle yet (for example, an existing DB
233- // // had only a default CF), you can create them here:
234- // if (metaDataHandle == null) {
235- // metaDataHandle = db.createColumnFamily(
236- // new ColumnFamilyDescriptor(METADATA_DB_NAME.getBytes(), new ColumnFamilyOptions())
237- // );
238- // }
239- // if (nodesHandle == null) {
240- // nodesHandle = db.createColumnFamily(
241- // new ColumnFamilyDescriptor(NODES_DB_NAME.getBytes(), new ColumnFamilyOptions())
242- // );
243- // }
244- //
245- // // 4) Proceed as normal (e.g. load metadata, etc.)
246- // loadMetaData();
247- // }
248-
24979 public MerkleTree (String treeName ) throws RocksDBException {
25080 RocksDB .loadLibrary ();
25181 this .treeName = treeName ;
@@ -337,6 +167,7 @@ public MerkleTree(String treeName) throws RocksDBException {
337167 * Get the current root hash of the Merkle tree.
338168 */
339169 public byte [] getRootHash () {
170+ errorIfClosed ();
340171 lock .readLock ().lock ();
341172 try {
342173 if (rootHash == null ) return null ;
@@ -347,6 +178,7 @@ public byte[] getRootHash() {
347178 }
348179
349180 public int getNumLeaves () {
181+ errorIfClosed ();
350182 lock .readLock ().lock ();
351183 try {
352184 return numLeaves ;
@@ -356,6 +188,7 @@ public int getNumLeaves() {
356188 }
357189
358190 public int getDepth () {
191+ errorIfClosed ();
359192 lock .readLock ().lock ();
360193 try {
361194 return depth ;
@@ -370,6 +203,7 @@ public int getDepth() {
370203 * @throws RocksDBException If there's an error accessing RocksDB
371204 */
372205 public HashSet <Node > getAllNodes () throws RocksDBException {
206+ errorIfClosed ();
373207 lock .readLock ().lock ();
374208 try {
375209 HashSet <Node > allNodes = new HashSet <>();
@@ -406,6 +240,7 @@ public HashSet<Node> getAllNodes() throws RocksDBException {
406240 * @throws IllegalArgumentException If key is null
407241 */
408242 public byte [] getData (byte [] key ) {
243+ errorIfClosed ();
409244 byte [] data = keyDataCache .get (new ByteArrayWrapper (key ));
410245 if (data != null ) return data ;
411246
@@ -427,6 +262,8 @@ public byte[] getData(byte[] key) {
427262 * @throws IllegalArgumentException If key or data is null
428263 */
429264 public void addOrUpdateData (byte [] key , byte [] data ) throws RocksDBException {
265+ errorIfClosed ();
266+
430267 if (key == null ) {
431268 throw new IllegalArgumentException ("Key cannot be null" );
432269 }
@@ -464,6 +301,8 @@ public void addOrUpdateData(byte[] key, byte[] data) throws RocksDBException {
464301 }
465302
466303 public void revertUnsavedChanges () {
304+ errorIfClosed ();
305+
467306 lock .writeLock ().lock ();
468307 try {
469308 nodesCache .clear ();
@@ -479,6 +318,8 @@ public void revertUnsavedChanges() {
479318 }
480319
481320 public boolean containsKey (byte [] key ) {
321+ errorIfClosed ();
322+
482323 if (key == null ) {
483324 throw new IllegalArgumentException ("Key cannot be null" );
484325 }
@@ -494,6 +335,7 @@ public boolean containsKey(byte[] key) {
494335 }
495336
496337 public List <byte []> getAllKeys () {
338+ errorIfClosed ();
497339 lock .readLock ().lock ();
498340 try {
499341 List <byte []> keys = new ArrayList <>();
@@ -511,6 +353,7 @@ public List<byte[]> getAllKeys() {
511353 }
512354
513355 public List <byte []> getAllData () {
356+ errorIfClosed ();
514357 lock .readLock ().lock ();
515358 try {
516359 List <byte []> data = new ArrayList <>();
@@ -531,6 +374,7 @@ public List<byte[]> getAllData() {
531374 * Flush all in-memory changes (nodes, metadata) to RocksDB.
532375 */
533376 public void flushToDisk () throws RocksDBException {
377+ errorIfClosed ();
534378 lock .writeLock ().lock ();
535379 try {
536380 try (WriteBatch batch = new WriteBatch ()) {
@@ -584,7 +428,7 @@ public void flushToDisk() throws RocksDBException {
584428 public void close () throws RocksDBException {
585429 lock .writeLock ().lock ();
586430 try {
587- if (closed ) return ;
431+ if (closed . get () ) return ;
588432 flushToDisk ();
589433
590434 if (metaDataHandle != null ) {
@@ -620,13 +464,15 @@ public void close() throws RocksDBException {
620464 }
621465
622466 openTrees .remove (treeName );
623- closed = true ;
467+ closed . set ( true ) ;
624468 } finally {
625469 lock .writeLock ().unlock ();
626470 }
627471 }
628472
629473 public MerkleTree clone (String newTreeName ) throws RocksDBException {
474+ errorIfClosed ();
475+
630476 if (newTreeName == null || newTreeName .isEmpty ()) {
631477 throw new IllegalArgumentException ("New tree name cannot be null or empty" );
632478 }
@@ -670,10 +516,11 @@ public MerkleTree clone(String newTreeName) throws RocksDBException {
670516 * This is much faster than iterating through all entries and deleting them individually.
671517 */
672518 public void clear () {
519+ errorIfClosed ();
673520 lock .writeLock ().lock ();
674521 try {
675522 // First close the current DB
676- if (!closed ) {
523+ if (!closed . get () ) {
677524 // Close all column family handles
678525 if (metaDataHandle != null ) {
679526 try {
@@ -783,9 +630,6 @@ public void clear() {
783630 this .metaDataHandle = cfHandles .get (1 );
784631 this .nodesHandle = cfHandles .get (2 );
785632 this .keyDataHandle = cfHandles .get (3 );
786-
787- // Reset closed flag
788- closed = false ;
789633 } catch (RocksDBException e ) {
790634 throw new RuntimeException ("Failed to re-initialize RocksDB after clearing: " + e .getMessage (), e );
791635 }
@@ -1021,6 +865,12 @@ private void addNode(int level, Node node) throws RocksDBException {
1021865 }
1022866 }
1023867
868+ private void errorIfClosed () {
869+ if (closed .get ()) {
870+ throw new IllegalStateException ("MerkleTree is closed" );
871+ }
872+ }
873+
1024874 //endregion
1025875
1026876 //region ===================== Nested Classes =====================
0 commit comments