diff --git a/io-hotmoka-node-local/src/main/java/io/hotmoka/node/local/internal/AbstractCheckableLocalNodeImpl.java b/io-hotmoka-node-local/src/main/java/io/hotmoka/node/local/internal/AbstractCheckableLocalNodeImpl.java index 4fe4fa96a..55d287b97 100644 --- a/io-hotmoka-node-local/src/main/java/io/hotmoka/node/local/internal/AbstractCheckableLocalNodeImpl.java +++ b/io-hotmoka-node-local/src/main/java/io/hotmoka/node/local/internal/AbstractCheckableLocalNodeImpl.java @@ -18,6 +18,8 @@ import java.util.Optional; import java.util.concurrent.BlockingQueue; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.ConcurrentMap; import java.util.concurrent.LinkedBlockingDeque; import java.util.logging.Level; import java.util.logging.Logger; @@ -123,8 +125,9 @@ protected void moveToFinalStoreOf(T transaction) throws NodeException { var rootAsBI = ByteIterable.fromBytes(getStore().getStateId()); env.executeInTransaction(txn -> setRootBranch(oldStore, rootAsBI, txn)); - if (!storesToGC.offer(oldStore)) - LOGGER.warning("could not enqueue old store for garbage collection: the queue is full!"); + if (!isUsed(oldStore)) + if (!storesToGC.offer(oldStore)) + LOGGER.warning("could not enqueue old store for garbage collection: the queue is full!"); } catch (StoreException | ExodusException e) { throw new NodeException(e); @@ -146,6 +149,30 @@ protected void closeResources() throws NodeException, InterruptedException { } } + private final ConcurrentMap, Integer> storeUsers = new ConcurrentHashMap<>(); + + @Override + protected void enter(S store) { + super.enter(store); + storeUsers.putIfAbsent(store, 0); + storeUsers.compute(store, (_store, old) -> old + 1); + } + + @Override + protected void exit(S store) { + storeUsers.compute(store, (_store, old) -> old - 1); + + if (!isUsed(store)) + if (!storesToGC.offer(store)) + LOGGER.warning("could not enqueue old store for garbage collection: the queue is full!"); + + super.exit(store); + } + + private boolean isUsed(S store) { + return store == getStore() || storeUsers.getOrDefault(store, 0) > 0; + } + private void setRootBranch(S oldStore, ByteIterable rootAsBI, Transaction txn) { storeOfNode.put(txn, ROOT, rootAsBI); // we set the root branch //storeOfNode.put(txn, PAST_STORES, null); // we add the old store to the past stores list diff --git a/io-hotmoka-node-local/src/main/java/io/hotmoka/node/local/internal/AbstractLocalNodeImpl.java b/io-hotmoka-node-local/src/main/java/io/hotmoka/node/local/internal/AbstractLocalNodeImpl.java index 975806ebc..57d51efd3 100644 --- a/io-hotmoka-node-local/src/main/java/io/hotmoka/node/local/internal/AbstractLocalNodeImpl.java +++ b/io-hotmoka-node-local/src/main/java/io/hotmoka/node/local/internal/AbstractLocalNodeImpl.java @@ -206,9 +206,15 @@ public final Subscription subscribeToEvents(StorageReference creator, BiConsumer @Override public final ConsensusConfig getConfig() throws NodeException { + S store = this.store; + enter(store); + try (var scope = mkScope()) { return store.getConfig(); } + finally { + exit(store); + } } @Override @@ -234,12 +240,18 @@ public final TransactionReference getTakamakaCode() throws NodeException { @Override public final StorageReference getManifest() throws NodeException { + S store = this.store; + enter(store); + try (var scope = mkScope()) { return store.getManifest().orElseThrow(UninitializedNodeException::new); } catch (StoreException e) { throw new NodeException(e); } + finally { + exit(store); + } } @Override @@ -267,16 +279,25 @@ public final TransactionResponse getPolledResponse(TransactionReference referenc @Override public final TransactionRequest getRequest(TransactionReference reference) throws UnknownReferenceException, NodeException { + S store = this.store; + enter(store); + try (var scope = mkScope()) { return store.getRequest(Objects.requireNonNull(reference)); } catch (StoreException e) { throw new NodeException(e); } + finally { + exit(store); + } } @Override public final TransactionResponse getResponse(TransactionReference reference) throws TransactionRejectedException, UnknownReferenceException, NodeException { + S store = this.store; + enter(store); + try (var scope = mkScope()) { try { return store.getResponse(Objects.requireNonNull(reference)); @@ -292,10 +313,16 @@ public final TransactionResponse getResponse(TransactionReference reference) thr catch (StoreException e) { throw new NodeException(e); } + finally { + exit(store); + } } @Override public final ClassTag getClassTag(StorageReference reference) throws UnknownReferenceException, NodeException { + S store = this.store; + enter(store); + try (var scope = mkScope()) { Objects.requireNonNull(reference); @@ -311,21 +338,30 @@ public final ClassTag getClassTag(StorageReference reference) throws UnknownRefe catch (StoreException e) { throw new NodeException(e); } + finally { + exit(store); + } } @Override public final Stream getState(StorageReference reference) throws UnknownReferenceException, NodeException { + S store = this.store; + enter(store); + try (var scope = mkScope()) { try { Stream history = store.getHistory(Objects.requireNonNull(reference)); var updates = new HashSet(); - CheckRunnable.check(StoreException.class, () -> history.forEachOrdered(UncheckConsumer.uncheck(transaction -> addUpdatesCommitted(reference, transaction, updates)))); + CheckRunnable.check(StoreException.class, () -> history.forEachOrdered(UncheckConsumer.uncheck(transaction -> addUpdatesCommitted(store, reference, transaction, updates)))); return updates.stream(); } catch (StoreException e) { throw new NodeException(e); } } + finally { + exit(store); + } } @Override @@ -385,6 +421,9 @@ public final Optional addStaticMethodCallTransaction(StaticMethodC @Override public final Optional runInstanceMethodCallTransaction(InstanceMethodCallTransactionRequest request) throws TransactionRejectedException, TransactionException, CodeExecutionException, NodeException { + S store = this.store; + enter(store); + try (var scope = mkScope()) { var reference = TransactionReferences.of(hasher.hash(request)); String referenceAsString = reference.toString(); @@ -396,10 +435,16 @@ public final Optional runInstanceMethodCallTransaction(InstanceMet catch (StoreException e) { throw new NodeException(e); } + finally { + exit(store); + } } @Override public final Optional runStaticMethodCallTransaction(StaticMethodCallTransactionRequest request) throws TransactionRejectedException, TransactionException, CodeExecutionException, NodeException { + S store = this.store; + enter(store); + try (var scope = mkScope()) { var reference = TransactionReferences.of(hasher.hash(request)); String referenceAsString = reference.toString(); @@ -411,6 +456,9 @@ public final Optional runStaticMethodCallTransaction(StaticMethodC catch (StoreException e) { throw new NodeException(e); } + finally { + exit(store); + } } @Override @@ -448,9 +496,25 @@ public final MethodFuture postStaticMethodCallTransaction(StaticMethodCallTransa */ protected void initWithEmptyStore() throws NodeException { // the node is starting from scratch: the caches are left empty and the consensus is well-known - this.store = mkStore(executors, config, hasher); + store = mkStore(executors, config, hasher); } + /** + * Called when this node is executing something that needs the given store. + * It can be used, for instance, to take note that the store cannot be + * garbage-collected from that moment. + * + * @param store the store + */ + protected void enter(S store) {} + + /** + * Called when this node finished executing something that needed the given store. + * + * @param store the store + */ + protected void exit(S store) {} + /** * Initializes the node with the last saved store, hence resuming a node * from its saved state. The saved store must have been initialized, so that @@ -481,12 +545,18 @@ protected void signalRejected(TransactionRequest request, TransactionRejected } protected void checkTransaction(TransactionRequest request) throws TransactionRejectedException, NodeException { + S store = this.store; + enter(store); + try { store.checkTransaction(request); } catch (StoreException e) { throw new NodeException(e); } + finally { + exit(store); + } } protected T beginTransaction(long now) throws NodeException { @@ -578,6 +648,9 @@ else if (request instanceof ConstructorCallTransactionRequest cctr) else LOGGER.info(reference + ": posting (" + request.getClass().getSimpleName() + ')'); + S store = this.store; + enter(store); + try { store.getResponse(reference); // if the response is found, then no exception is thrown above and the request was repeated @@ -589,11 +662,14 @@ else if (request instanceof ConstructorCallTransactionRequest cctr) catch (UnknownReferenceException e) { // this is fine: there was no previous request with the same reference so we register // its semaphore and post the request for execution - createSemaphore(reference); + createSemaphore(store, reference); postRequest(request); return reference; } + finally { + exit(store); + } } private static String trim(String s) { @@ -609,7 +685,7 @@ private static String trim(String s) { * @param updates the set where they must be added * @throws StoreException */ - private void addUpdatesCommitted(StorageReference object, TransactionReference referenceInHistory, Set updates) throws StoreException { + private void addUpdatesCommitted(S store, StorageReference object, TransactionReference referenceInHistory, Set updates) throws StoreException { try { if (store.getResponse(referenceInHistory) instanceof TransactionResponseWithUpdates trwu) trwu.getUpdates() @@ -629,7 +705,7 @@ private void addUpdatesCommitted(StorageReference object, TransactionReference r * @param reference the reference of the transaction for the request * @throws TransactionRejectedException */ - private void createSemaphore(TransactionReference reference) throws TransactionRejectedException { + private void createSemaphore(S store, TransactionReference reference) throws TransactionRejectedException { if (semaphores.putIfAbsent(reference, new Semaphore(0)) != null) throw new TransactionRejectedException("Repeated request " + reference, store.getConfig()); } diff --git a/io-hotmoka-node-local/src/main/java/io/hotmoka/node/local/internal/store/trie/AbstractTrieBasedStoreImpl.java b/io-hotmoka-node-local/src/main/java/io/hotmoka/node/local/internal/store/trie/AbstractTrieBasedStoreImpl.java index bba2137a0..a0b2c520e 100644 --- a/io-hotmoka-node-local/src/main/java/io/hotmoka/node/local/internal/store/trie/AbstractTrieBasedStoreImpl.java +++ b/io-hotmoka-node-local/src/main/java/io/hotmoka/node/local/internal/store/trie/AbstractTrieBasedStoreImpl.java @@ -166,22 +166,44 @@ protected S addDelta(StoreCache cache, LinkedHashMap env.computeInTransaction(UncheckFunction.uncheck(txn -> { + System.out.println("added requests: " + addedRequests.entrySet().size()); var trieOfRequests = mkTrieOfRequests(txn); - for (var entry: addedRequests.entrySet()) + for (var entry: addedRequests.entrySet()) { + trieOfRequests.incrementReferenceCountOfRoot(); + var old = trieOfRequests; trieOfRequests = trieOfRequests.put(entry.getKey(), entry.getValue()); + old.free(); + } + System.out.println("added responses: " + addedResponses.entrySet().size()); var trieOfResponses = mkTrieOfResponses(txn); - for (var entry: addedResponses.entrySet()) + for (var entry: addedResponses.entrySet()) { + trieOfResponses.incrementReferenceCountOfRoot(); + var old = trieOfResponses; trieOfResponses = trieOfResponses.put(entry.getKey(), entry.getValue()); - + old.free(); + } + + System.out.println("added histories: " + addedHistories.entrySet().size()); var trieOfHistories = mkTrieOfHistories(txn); - for (var entry: addedHistories.entrySet()) + for (var entry: addedHistories.entrySet()) { + trieOfHistories.incrementReferenceCountOfRoot(); + var old = trieOfHistories; trieOfHistories = trieOfHistories.put(entry.getKey(), Stream.of(entry.getValue())); + old.free(); + } var trieOfInfo = mkTrieOfInfo(txn); + trieOfInfo.incrementReferenceCountOfRoot(); + var old = trieOfInfo; trieOfInfo = trieOfInfo.increaseHeight(); - if (addedManifest.isPresent()) + old.free(); + if (addedManifest.isPresent()) { + trieOfInfo.incrementReferenceCountOfRoot(); + old = trieOfInfo; trieOfInfo = trieOfInfo.setManifest(addedManifest.get()); + old.free(); + } // we increment the reference count of the roots of the resulting tries, so that // they do not get garbage collected until this store is freed diff --git a/io-hotmoka-node/src/main/java/io/hotmoka/node/internal/updates/AbstractUpdate.java b/io-hotmoka-node/src/main/java/io/hotmoka/node/internal/updates/AbstractUpdate.java index 09915184f..27d282209 100644 --- a/io-hotmoka-node/src/main/java/io/hotmoka/node/internal/updates/AbstractUpdate.java +++ b/io-hotmoka-node/src/main/java/io/hotmoka/node/internal/updates/AbstractUpdate.java @@ -104,7 +104,7 @@ public static Update from(UnmarshallingContext context) throws IOException { return Updates.classTag(StorageValues.referenceWithoutSelectorFrom(context), (ClassType) StorageTypes.from(context), TransactionReferences.from(context)); } catch (ClassCastException e) { - throw new IOException("Failed unmrshalling a class tag", e); + throw new IOException("Failed unmarshalling a class tag", e); } } case UpdateOfBigIntegerImpl.SELECTOR_BALANCE: return Updates.ofBigInteger(StorageValues.referenceWithoutSelectorFrom(context), FieldSignatures.BALANCE_FIELD, context.readBigInteger()); diff --git a/io-hotmoka-patricia/src/main/java/io/hotmoka/patricia/internal/AbstractPatriciaTrieImpl.java b/io-hotmoka-patricia/src/main/java/io/hotmoka/patricia/internal/AbstractPatriciaTrieImpl.java index 85f17550e..fa39fd204 100644 --- a/io-hotmoka-patricia/src/main/java/io/hotmoka/patricia/internal/AbstractPatriciaTrieImpl.java +++ b/io-hotmoka-patricia/src/main/java/io/hotmoka/patricia/internal/AbstractPatriciaTrieImpl.java @@ -376,6 +376,9 @@ private static byte[] compactNibblesIntoBytes(byte[] nibbles, byte evenSelector, return result; } + private static long freed; + private static long allocated; + private static byte[] expandBytesIntoNibbles(byte[] bytes, byte evenSelector) { byte[] nibbles; @@ -441,6 +444,8 @@ protected final AbstractNode putInStore() throws TrieException { } catch (UnknownKeyException e) { try { + allocated++; + System.out.printf("%d/%d: %.2f\n", freed, allocated, freed * 100.0 / allocated); store.put(hash, toByteArray()); // we bind it to its hash in the store incrementReferenceCountOfDescedants(); return this; @@ -461,12 +466,14 @@ protected final AbstractNode putInStore() throws TrieException { */ protected void free(byte[] hash) throws TrieException { AbstractNode replacement = withDecrementedReferenceCount(); - //System.out.println(getClass().getSimpleName() + ": " + count + " -> " + replacement.count); + System.out.println(getClass().getSimpleName() + ": " + count + " -> " + replacement.count); try { if (replacement.count > 0) store.put(hash, replacement.toByteArray()); // TODO: can we avoid to unmarshal and marshal again? else { + freed++; + System.out.printf("%d/%d: %.2f\n", freed, allocated, freed * 100.0 / allocated); store.remove(hash); freeDescendants(); } diff --git a/io-hotmoka-tests/src/test/java/io/hotmoka/tests/HotmokaTest.java b/io-hotmoka-tests/src/test/java/io/hotmoka/tests/HotmokaTest.java index 8bec8a56a..9d95d3b14 100644 --- a/io-hotmoka-tests/src/test/java/io/hotmoka/tests/HotmokaTest.java +++ b/io-hotmoka-tests/src/test/java/io/hotmoka/tests/HotmokaTest.java @@ -186,9 +186,9 @@ public interface TestBody { privateKeyOfGamete = keys.getPrivate(); Node wrapped; - node = wrapped = mkDiskBlockchain(); + //node = wrapped = mkDiskBlockchain(); //node = wrapped = mkTendermintBlockchain(); - //node = mkRemoteNode(wrapped = mkDiskBlockchain()); + node = mkRemoteNode(wrapped = mkDiskBlockchain()); //node = mkRemoteNode(wrapped = mkTendermintBlockchain()); //node = wrapped = mkRemoteNode("ec2-54-194-239-91.eu-west-1.compute.amazonaws.com:8080"); //node = wrapped = mkRemoteNode("localhost:8080"); @@ -428,9 +428,6 @@ protected final StorageValue addInstanceNonVoidMethodCallTransaction(PrivateKey /** * Takes care of computing the next nonce. - * @throws InterruptedException - * @throws TimeoutException - * @throws NodeException */ protected final StorageValue addStaticNonVoidMethodCallTransaction(PrivateKey key, StorageReference caller, BigInteger gasLimit, BigInteger gasPrice, TransactionReference classpath, NonVoidMethodSignature method, StorageValue... actuals) throws TransactionException, CodeExecutionException, TransactionRejectedException, InvalidKeyException, SignatureException, NodeException, TimeoutException, InterruptedException { return node.addStaticMethodCallTransaction(TransactionRequests.staticMethodCall(signature.getSigner(key, SignedTransactionRequest::toByteArrayWithoutSignature), caller, getNonceOf(caller), chainId, gasLimit, gasPrice, classpath, method, actuals)).orElseThrow(() -> new NodeException(method + " did not return any value")); @@ -438,9 +435,6 @@ protected final StorageValue addStaticNonVoidMethodCallTransaction(PrivateKey ke /** * Takes care of computing the next nonce. - * @throws InterruptedException - * @throws TimeoutException - * @throws NodeException */ protected final void addStaticVoidMethodCallTransaction(PrivateKey key, StorageReference caller, BigInteger gasLimit, BigInteger gasPrice, TransactionReference classpath, VoidMethodSignature method, StorageValue... actuals) throws TransactionException, CodeExecutionException, TransactionRejectedException, InvalidKeyException, SignatureException, NodeException, TimeoutException, InterruptedException { node.addStaticMethodCallTransaction(TransactionRequests.staticMethodCall(signature.getSigner(key, SignedTransactionRequest::toByteArrayWithoutSignature), caller, getNonceOf(caller), chainId, gasLimit, gasPrice, classpath, method, actuals));