From 64e2ef92fd2f3d7f4ffe71bc98320d0fde8dec69 Mon Sep 17 00:00:00 2001 From: "Brian S. O'Neill" Date: Fri, 15 Nov 2024 09:19:58 -0800 Subject: [PATCH] Speed up verification by using multiple threads. --- src/main/java/org/cojen/tupl/Database.java | 69 ++++---- src/main/java/org/cojen/tupl/Index.java | 7 +- src/main/java/org/cojen/tupl/core/BTree.java | 33 +++- .../java/org/cojen/tupl/core/BTreeCopier.java | 1 + .../java/org/cojen/tupl/core/BTreeCursor.java | 24 ++- .../org/cojen/tupl/core/BTreeSeparator.java | 41 +++-- .../org/cojen/tupl/core/BTreeVerifier.java | 156 ++++++++++++++++++ .../org/cojen/tupl/core/LocalDatabase.java | 4 +- src/main/java/org/cojen/tupl/core/Tree.java | 7 +- .../org/cojen/tupl/core/VerifyObserver.java | 18 +- .../cojen/tupl/diag/VerificationObserver.java | 5 +- .../java/org/cojen/tupl/jmx/Registration.java | 2 +- .../org/cojen/tupl/remote/ClientDatabase.java | 4 +- .../org/cojen/tupl/remote/ClientIndex.java | 4 +- .../org/cojen/tupl/remote/RemoteDatabase.java | 3 +- .../org/cojen/tupl/remote/RemoteIndex.java | 3 +- .../org/cojen/tupl/remote/ServerDatabase.java | 6 +- .../org/cojen/tupl/remote/ServerIndex.java | 7 +- .../remote/VerificationObserverRelay.java | 42 ++--- .../java/org/cojen/tupl/tools/Verify.java | 20 +-- .../cojen/tupl/views/UnmodifiableView.java | 4 +- .../org/cojen/tupl/core/BTreeGraftTest.java | 2 +- .../tupl/core/CheckpointFailureTest.java | 2 +- .../java/org/cojen/tupl/core/CloseTest.java | 2 +- .../java/org/cojen/tupl/core/CompactTest.java | 22 +-- .../cojen/tupl/core/CrudBasicFilterTest.java | 2 +- .../tupl/core/CrudBasicTransformTest.java | 2 +- .../org/cojen/tupl/core/CrudDefaultTest.java | 2 +- .../tupl/core/CrudDisjointUnionTest.java | 2 +- .../cojen/tupl/core/CrudNonTransformTest.java | 2 +- .../tupl/core/CrudSelfDifferenceTest.java | 2 +- .../tupl/core/CrudSelfIntersectionTest.java | 2 +- .../cojen/tupl/core/CrudSelfUnionTest.java | 2 +- .../java/org/cojen/tupl/core/CrudTest.java | 2 +- .../tupl/core/CursorBasicFilterTest.java | 2 +- .../tupl/core/CursorBasicTransformTest.java | 2 +- .../cojen/tupl/core/CursorDefaultTest.java | 2 +- .../tupl/core/CursorDisjointUnionTest.java | 2 +- .../tupl/core/CursorNonTransformTest.java | 2 +- .../java/org/cojen/tupl/core/CursorTest.java | 2 +- .../org/cojen/tupl/core/EnduranceTest.java | 2 +- .../org/cojen/tupl/core/EvictionTest.java | 2 +- .../org/cojen/tupl/core/LargeKeyTest.java | 10 +- .../org/cojen/tupl/core/LargePageTest.java | 2 +- .../cojen/tupl/core/LargeValueFuzzTest.java | 2 +- .../org/cojen/tupl/core/LargeValueTest.java | 18 +- .../java/org/cojen/tupl/core/RecoverTest.java | 6 +- .../tupl/core/SnapshotMappedDirectTest.java | 2 +- .../org/cojen/tupl/core/SnapshotTest.java | 6 +- .../org/cojen/tupl/core/TransformerTest.java | 2 +- .../cojen/tupl/core/ValueAccessorTest.java | 28 ++-- .../java/org/cojen/tupl/core/ViewTest.java | 4 +- .../org/cojen/tupl/remote/RemoteTest.java | 8 +- 53 files changed, 410 insertions(+), 200 deletions(-) create mode 100644 src/main/java/org/cojen/tupl/core/BTreeVerifier.java diff --git a/src/main/java/org/cojen/tupl/Database.java b/src/main/java/org/cojen/tupl/Database.java index 4d5fe7beb..f3dbf94a2 100644 --- a/src/main/java/org/cojen/tupl/Database.java +++ b/src/main/java/org/cojen/tupl/Database.java @@ -144,7 +144,7 @@ public static Database rebuild(DatabaseConfig oldConfig, DatabaseConfig newConfi * * @return shared Index instance */ - public abstract Index openIndex(byte[] name) throws IOException; + public Index openIndex(byte[] name) throws IOException; /** * Returns the given named index, creating it if necessary. Name is UTF-8 @@ -161,7 +161,7 @@ public default Index openIndex(String name) throws IOException { * * @return shared Index instance; null if not found */ - public abstract Index findIndex(byte[] name) throws IOException; + public Index findIndex(byte[] name) throws IOException; /** * Returns the given named index, returning null if not found. Name is UTF-8 @@ -179,7 +179,7 @@ public default Index findIndex(String name) throws IOException { * @return shared Index instance * @throws IllegalArgumentException if id is reserved */ - public abstract Index indexById(long id) throws IOException; + public Index indexById(long id) throws IOException; /** * Returns an index by its identifier, returning null if not found. @@ -239,7 +239,7 @@ public default Table openJoinTable(Class joinType, String spec) throws * @throws IllegalStateException if name is already in use by another index * @throws IllegalStateException if index belongs to another database instance */ - public abstract void renameIndex(Index index, byte[] newName) throws IOException; + public void renameIndex(Index index, byte[] newName) throws IOException; /** * Renames the given index to the one given. Name is UTF-8 encoded. @@ -274,7 +274,7 @@ public default void renameIndex(Index index, String newName) throws IOException * @see EventListener * @see Index#drop Index.drop */ - public abstract Runnable deleteIndex(Index index) throws IOException; + public Runnable deleteIndex(Index index) throws IOException; /** * Creates a new unnamed temporary index. Temporary indexes never get written to the redo @@ -282,21 +282,21 @@ public default void renameIndex(Index index, String newName) throws IOException * explicitly {@linkplain #deleteIndex deleted} when no longer needed, rather than waiting * until the database is re-opened. */ - public abstract Index newTemporaryIndex() throws IOException; + public Index newTemporaryIndex() throws IOException; /** * Returns an {@linkplain UnmodifiableViewException unmodifiable} View which maps all * available index names to identifiers. Identifiers are long integers, {@linkplain * org.cojen.tupl.io.Utils#decodeLongBE big-endian} encoded. */ - public abstract View indexRegistryByName() throws IOException; + public View indexRegistryByName() throws IOException; /** * Returns an {@linkplain UnmodifiableViewException unmodifiable} View which maps all * available index identifiers to names. Identifiers are long integers, {@linkplain * org.cojen.tupl.io.Utils#decodeLongBE big-endian} encoded. */ - public abstract View indexRegistryById() throws IOException; + public View indexRegistryById() throws IOException; /** * Returns a new Transaction with the {@linkplain DatabaseConfig#durabilityMode default} @@ -310,7 +310,7 @@ public default Transaction newTransaction() { * Returns a new Transaction with the given durability mode. If null, the * {@linkplain DatabaseConfig#durabilityMode default} is used. */ - public abstract Transaction newTransaction(DurabilityMode durabilityMode); + public Transaction newTransaction(DurabilityMode durabilityMode); /** * Returns a handler instance suitable for writing custom redo and undo operations. A @@ -320,7 +320,7 @@ public default Transaction newTransaction() { * @return new writer instance * @throws IllegalStateException if no recovery instance by the given name is installed */ - public abstract CustomHandler customWriter(String name) throws IOException; + public CustomHandler customWriter(String name) throws IOException; /** * Returns a handler instance suitable for preparing transactions. A corresponding recovery @@ -330,14 +330,14 @@ public default Transaction newTransaction() { * @return new writer instance * @throws IllegalStateException if no recovery instance by the given name is installed */ - public abstract PrepareHandler prepareWriter(String name) throws IOException; + public PrepareHandler prepareWriter(String name) throws IOException; /** * Returns a new Sorter instance. The standard algorithm is a parallel external mergesort, * which attempts to use all available processors. All external storage is maintained in * the database itself, in the form of temporary indexes. */ - public abstract Sorter newSorter(); + public Sorter newSorter(); /** * Preallocates pages for immediate use. The actual amount allocated @@ -345,7 +345,7 @@ public default Transaction newTransaction() { * * @return actual amount allocated */ - public abstract long preallocate(long bytes) throws IOException; + public long preallocate(long bytes) throws IOException; /** * Set a soft capacity limit for the database, to prevent filling up the storage @@ -404,7 +404,7 @@ public default void capacityLimitOverride(long bytes) { * * @return a snapshot control object, which must be closed when no longer needed */ - public abstract Snapshot beginSnapshot() throws IOException; + public Snapshot beginSnapshot() throws IOException; /** * Restore from a {@linkplain #beginSnapshot snapshot}, into the data files defined by the @@ -426,7 +426,7 @@ public static Database restoreFromSnapshot(DatabaseConfig config, InputStream in * @param out cache priming destination; buffering is recommended; not auto-closed * @see DatabaseConfig#cachePriming */ - public abstract void createCachePrimer(OutputStream out) throws IOException; + public void createCachePrimer(OutputStream out) throws IOException; /** * Prime the cache, from a set encoded {@linkplain #createCachePrimer earlier}. @@ -434,7 +434,7 @@ public static Database restoreFromSnapshot(DatabaseConfig config, InputStream in * @param in caching priming source; buffering is recommended; auto-closed * @see DatabaseConfig#cachePriming */ - public abstract void applyCachePrimer(InputStream in) throws IOException; + public void applyCachePrimer(InputStream in) throws IOException; /** * Returns an object for enabling remote access into this database. As long as the server @@ -445,12 +445,12 @@ public static Database restoreFromSnapshot(DatabaseConfig config, InputStream in * * @see #connect connect */ - public abstract Server newServer() throws IOException; + public Server newServer() throws IOException; /** * Returns a collection of database statistics. */ - public abstract DatabaseStats stats(); + public DatabaseStats stats(); /** * Flushes all committed transactions, but not durably. Transactions committed with @@ -461,14 +461,14 @@ public static Database restoreFromSnapshot(DatabaseConfig config, InputStream in * Calling this method on a replicated database has no effect. */ @Override - public abstract void flush() throws IOException; + public void flush() throws IOException; /** * Durably flushes all committed transactions. Transactions committed with {@linkplain * DurabilityMode#NO_FLUSH no-flush} and {@linkplain DurabilityMode#NO_SYNC no-sync} * effectively become {@linkplain DurabilityMode#SYNC sync} durable. */ - public abstract void sync() throws IOException; + public void sync() throws IOException; /** * Durably sync and checkpoint all changes to the database. In addition to ensuring that @@ -476,7 +476,7 @@ public static Database restoreFromSnapshot(DatabaseConfig config, InputStream in * modifications are durable. Checkpoints are performed automatically by a background * thread, at a {@linkplain DatabaseConfig#checkpointRate configurable} rate. */ - public abstract void checkpoint() throws IOException; + public void checkpoint() throws IOException; /** * Temporarily suspend automatic checkpoints and wait for any in-progress checkpoint to @@ -485,7 +485,7 @@ public static Database restoreFromSnapshot(DatabaseConfig config, InputStream in * * @throws IllegalStateException if suspended more than 231 times */ - public abstract void suspendCheckpoints(); + public void suspendCheckpoints(); /** * Resume automatic checkpoints after having been temporarily {@link #suspendCheckpoints @@ -493,7 +493,7 @@ public static Database restoreFromSnapshot(DatabaseConfig config, InputStream in * * @throws IllegalStateException if resumed more than suspended */ - public abstract void resumeCheckpoints(); + public void resumeCheckpoints(); /** * Returns the checkpoint commit lock, which can be held to prevent checkpoints from @@ -505,7 +505,7 @@ public static Database restoreFromSnapshot(DatabaseConfig config, InputStream in * modifications, if a checkpoint is trying to start. In addition, a thread holding the * commit lock must not attempt to issue a checkpoint, because deadlock is possible. */ - public abstract Lock commitLock(); + public Lock commitLock(); /** * Compacts the database by shrinking the database file. The compaction target is the @@ -530,21 +530,24 @@ public static Database restoreFromSnapshot(DatabaseConfig config, InputStream in * @throws IllegalArgumentException if compaction target is out of bounds * @throws IllegalStateException if compaction is already in progress */ - public abstract boolean compactFile(CompactionObserver observer, double target) + public boolean compactFile(CompactionObserver observer, double target) throws IOException; /** - * Verifies the integrity of the database and all indexes. + * Verifies the integrity of the database and all indexes. Using multiple threads speeds up + * verification, even though some nodes might be visited multiple times. * * @param observer optional observer; pass null for default + * @param numThreads pass 0 for default, or if negative, the actual number will be {@code + * (-numThreads * availableProcessors)}. * @return true if verification passed */ - public abstract boolean verify(VerificationObserver observer) throws IOException; + public boolean verify(VerificationObserver observer, int numThreads) throws IOException; /** * Returns true if the database instance is currently the leader. */ - public abstract boolean isLeader(); + public boolean isLeader(); /** * Registers the given task to start in a separate thread when the database instance has @@ -556,7 +559,7 @@ public abstract boolean compactFile(CompactionObserver observer, double target) * @param acquired called when leadership is acquired (can be null) * @param lost called when leadership is lost (can be null) */ - public abstract void uponLeader(Runnable acquired, Runnable lost); + public void uponLeader(Runnable acquired, Runnable lost); /** * If the database instance is currently acting as a leader, attempt to give up leadership @@ -564,7 +567,7 @@ public abstract boolean compactFile(CompactionObserver observer, double target) * is returned. When false is returned, the database is likely still the leader, either * because the database isn't replicated, or because no replicas exist to failover to. */ - public abstract boolean failover() throws IOException; + public boolean failover() throws IOException; /** * Closes the database, ensuring durability of committed transactions. No @@ -587,17 +590,17 @@ public default void close() throws IOException { * @see #shutdown */ @Override - public abstract void close(Throwable cause) throws IOException; + public void close(Throwable cause) throws IOException; /** * Returns true if database was explicitly closed, or if it was closed due to a panic. */ - public abstract boolean isClosed(); + public boolean isClosed(); /** * Cleanly closes the database, ensuring durability of all modifications. A checkpoint is * issued first, and so a quick recovery is performed when the database is re-opened. As a * side effect of shutting down, all extraneous files are deleted. */ - public abstract void shutdown() throws IOException; + public void shutdown() throws IOException; } diff --git a/src/main/java/org/cojen/tupl/Index.java b/src/main/java/org/cojen/tupl/Index.java index ef374b121..44399a0cf 100644 --- a/src/main/java/org/cojen/tupl/Index.java +++ b/src/main/java/org/cojen/tupl/Index.java @@ -85,12 +85,15 @@ public long evict(Transaction txn, byte[] lowKey, byte[] highKey, public IndexStats analyze(byte[] lowKey, byte[] highKey) throws IOException; /** - * Verifies the integrity of the index. + * Verifies the integrity of the index. Using multiple threads speeds up verification, + * even though some nodes might be visited multiple times. * * @param observer optional observer; pass null for default + * @param numThreads pass 0 for default, or if negative, the actual number will be {@code + * (-numThreads * availableProcessors)}. * @return true if verification passed */ - public boolean verify(VerificationObserver observer) throws IOException; + public boolean verify(VerificationObserver observer, int numThreads) throws IOException; /** * Closes this index reference. The underlying index is still valid and can be re-opened, diff --git a/src/main/java/org/cojen/tupl/core/BTree.java b/src/main/java/org/cojen/tupl/core/BTree.java index fd694ce8e..5434bdd46 100644 --- a/src/main/java/org/cojen/tupl/core/BTree.java +++ b/src/main/java/org/cojen/tupl/core/BTree.java @@ -42,6 +42,8 @@ import org.cojen.tupl.diag.EventType; import org.cojen.tupl.diag.IndexStats; +import org.cojen.tupl.util.Runner; + import org.cojen.tupl.views.BoundedView; import org.cojen.tupl.views.UnmodifiableView; @@ -847,15 +849,28 @@ final boolean compactTree(Index view, long highestNodeId, CompactionObserver obs } @Override - final boolean verifyTree(Index view, VerifyObserver observer) throws IOException { - BTreeCursor cursor = newCursor(Transaction.BOGUS); - try { - cursor.mKeyOnly = true; - cursor.first(); // must start with loaded key - int height = cursor.height(); - return observer.indexBegin(view, height) && cursor.verify(height, observer); - } finally { - cursor.reset(); + final boolean verifyTree(Index view, VerifyObserver observer, int numThreads) + throws IOException + { + if (numThreads <= 0) { + int procs = Runtime.getRuntime().availableProcessors(); + numThreads = numThreads == 0 ? procs : procs * -numThreads; + } + + if (numThreads <= 1) { + BTreeCursor cursor = newCursor(Transaction.BOGUS); + try { + cursor.mKeyOnly = true; + cursor.first(); // must start with loaded key + int height = cursor.height(); + return observer.indexBegin(view, height) && cursor.verify(height, observer); + } finally { + cursor.reset(); + } + } else { + var verifier = new BTreeVerifier(observer, this, Runner.current(), numThreads); + verifier.start(); + return !verifier.await(); } } diff --git a/src/main/java/org/cojen/tupl/core/BTreeCopier.java b/src/main/java/org/cojen/tupl/core/BTreeCopier.java index 3bbdad610..b500d45ae 100644 --- a/src/main/java/org/cojen/tupl/core/BTreeCopier.java +++ b/src/main/java/org/cojen/tupl/core/BTreeCopier.java @@ -79,6 +79,7 @@ public BTree result() throws IOException { return mMerged; } if (mCondition.await(mLatch) < 0) { + stop(); throw new InterruptedIOException(); } } diff --git a/src/main/java/org/cojen/tupl/core/BTreeCursor.java b/src/main/java/org/cojen/tupl/core/BTreeCursor.java index d2465584c..b8ab05dfa 100644 --- a/src/main/java/org/cojen/tupl/core/BTreeCursor.java +++ b/src/main/java/org/cojen/tupl/core/BTreeCursor.java @@ -636,11 +636,25 @@ private void nextLeaf() throws IOException { } } + /** + * Non-transactionally move to the next tree leaf node, loading it if necessary. Node might + * be empty or full of ghosts. Unless nothing is left, the key is loaded, but the value is + * only loaded when autoload mode is set. + */ + final void skipToNextLeaf() throws IOException { + // Move to next node by first setting current node position higher than possible. + mFrame.mNodePos = Integer.MAX_VALUE - 1; + Node node = toNextLeaf(frameSharedNotSplit()); + if (node != null) { + tryCopyCurrent(LocalTransaction.BOGUS); + } + } + /** * Non-transactionally move to the next tree leaf node, loading it if necessary. Node might * be empty or full of ghosts. Key and value are not loaded. */ - private void skipToNextLeaf() throws IOException { + private void skipToNextLeafNoLoad() throws IOException { // Move to next node by first setting current node position higher than possible. mFrame.mNodePos = Integer.MAX_VALUE - 1; nextLeaf(); @@ -4824,7 +4838,7 @@ final boolean compact(long highestNodeId, CompactionObserver observer) throws IO } // No fragmented values found. node.releaseShared(); - skipToNextLeaf(); + skipToNextLeafNoLoad(); if ((frame = mFrame) == null) { // No more entries to examine. return true; @@ -5044,14 +5058,14 @@ final boolean verify(final int height, VerifyObserver observer) throws IOExcepti if (!verifyFrames(height, stack, mFrame, observer)) { return false; } - skipToNextLeaf(); + skipToNextLeafNoLoad(); } } return true; } - private boolean verifyFrames(int level, Node[] stack, CursorFrame frame, - VerifyObserver observer) + final boolean verifyFrames(int level, Node[] stack, CursorFrame frame, + VerifyObserver observer) throws IOException { CursorFrame parentFrame = frame.mParentFrame; diff --git a/src/main/java/org/cojen/tupl/core/BTreeSeparator.java b/src/main/java/org/cojen/tupl/core/BTreeSeparator.java index e56ba152d..19374e5f3 100644 --- a/src/main/java/org/cojen/tupl/core/BTreeSeparator.java +++ b/src/main/java/org/cojen/tupl/core/BTreeSeparator.java @@ -71,12 +71,13 @@ abstract class BTreeSeparator extends LongAdder { } /** - * @param db is only used for calling newTemporaryIndex + * @param db is only used for calling newTemporaryIndex; pass null to not create any target + * trees, and the Worker.transfer method must be overridden * @param executor used for parallel separation; pass null to use only the starting thread * @param workerCount maximum parallelism; must be at least 1 */ BTreeSeparator(LocalDatabase db, BTree[] sources, Executor executor, int workerCount) { - if (db == null || sources.length <= 0 || workerCount <= 0) { + if (sources.length <= 0 || workerCount <= 0) { throw new IllegalArgumentException(); } if (executor == null) { @@ -139,7 +140,8 @@ protected void failed(Throwable cause) { * source trees are empty, but not deleted, unless the transfer and skip methods are * overridden. * - * @param firstRange first separated range; the ranges are ordered lowest to highest. + * @param firstRange first separated range; the ranges are ordered lowest to highest. If a + * null database was passed to the constructor, then the firstRange parameter is null */ protected abstract void finished(Chain firstRange); @@ -157,13 +159,12 @@ private void startWorker(Worker from, int spawnCount, byte[] lowKey, byte[] high worker.mHashtableNext = hashtable[slot]; hashtable[slot] = worker; - if (from == null) { - mFirstWorker = worker; - } else { - Worker next = from.mNext; - from.mNext = worker; - if (next != null) { - worker.mNext = next; + if (mDatabase != null) { + if (from == null) { + mFirstWorker = worker; + } else { + worker.mNext = from.mNext; + from.mNext = worker; } } } @@ -352,10 +353,13 @@ private void doRun() throws Exception { selector.mSkip = false; } else { if (tcursor == null) { - mTarget = mDatabase.newTemporaryIndex(); - tcursor = mTarget.newCursor(Transaction.BOGUS); - tcursor.mKeyOnly = true; - tcursor.firstLeaf(); + LocalDatabase db = mDatabase; + if (db != null) { + mTarget = db.newTemporaryIndex(); + tcursor = mTarget.newCursor(Transaction.BOGUS); + tcursor.mKeyOnly = true; + tcursor.firstLeaf(); + } } transfer(scursor, tcursor); if (++count == 0) { @@ -418,11 +422,14 @@ private void doRun() throws Exception { } /** - * Copies (or moves) the current entry from the source cursor to the unpositioned - * target cursor, and advance the source cursor to the next key. The source cursor - * value isn't autoloaded. + * Copies (or moves) the current entry from the source cursor to the target cursor, and + * advance the source cursor to the next key. The source cursor value isn't autoloaded. + * When first called, the target tree is empty, and the target cursor is positioned at + * the first leaf node. * * Note: When this method is overridden, the skip method should be overridden too. + * + * @param target is null if a null database was passed the BTreeSeparator constructor */ protected void transfer(BTreeCursor source, BTreeCursor target) throws IOException { target.appendTransfer(source); diff --git a/src/main/java/org/cojen/tupl/core/BTreeVerifier.java b/src/main/java/org/cojen/tupl/core/BTreeVerifier.java new file mode 100644 index 000000000..fc11a8f3e --- /dev/null +++ b/src/main/java/org/cojen/tupl/core/BTreeVerifier.java @@ -0,0 +1,156 @@ +/* + * Copyright (C) 2024 Cojen.org + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +package org.cojen.tupl.core; + +import java.io.InterruptedIOException; +import java.io.IOException; + +import java.util.concurrent.Executor; + +import org.cojen.tupl.diag.VerificationObserver; + +import org.cojen.tupl.util.Latch; +import org.cojen.tupl.util.LocalPool; + +/** + * Parallel tree verify utility. After construction, call start and then call await. + * + * @author Brian S. O'Neill + */ +/*P*/ +final class BTreeVerifier extends BTreeSeparator { + // Note: The BTreeSeparator is extended because it supports parallel processing, but the + // necessary methods are overridden such that no actual data transfer occurs. + + private final VerificationObserver mObserver; + + private final LocalPool mObserverPool; + + private final Latch mLatch; + private final Latch.Condition mCondition; + + private boolean mStarted, mFinished; + private volatile boolean mStopped; + + /** + * @param executor used for parallel separation; pass null to use only the starting thread + * @param workerCount maximum parallelism; must be at least 1 + */ + BTreeVerifier(VerificationObserver observer, BTree source, Executor executor, int workerCount) { + super(null, new BTree[] {source}, executor, workerCount); + mObserver = observer; + mObserverPool = new LocalPool<>(null, workerCount); + mLatch = new Latch(); + mCondition = new Latch.Condition(); + } + + /** + * @return true if stopped + */ + public boolean await() throws IOException { + mLatch.acquireExclusive(); + try { + while (!mFinished) { + if (mCondition.await(mLatch) < 0) { + stop(); + throw new InterruptedIOException(); + } + } + } finally { + mLatch.releaseExclusive(); + } + + return mStopped; + } + + @Override + protected void finished(Chain firstRange) { + mLatch.acquireExclusive(); + mFinished = true; + mCondition.signalAll(mLatch); + mLatch.releaseExclusive(); + } + + @Override + protected Worker newWorker(int spawnCount, byte[] lowKey, byte[] highKey, int numSources) { + return new Verifier(spawnCount, lowKey, highKey, numSources); + } + + private void begin(BTreeCursor source, int height) { + mLatch.acquireExclusive(); + try { + if (!mStarted) { + mStarted = true; + if (!mObserver.indexBegin(source.mTree, height)) { + mStopped = true; + stop(); + } + } + } finally { + mLatch.releaseExclusive(); + } + } + + final class Verifier extends Worker { + Verifier(int spawnCount, byte[] lowKey, byte[] highKey, int numSources) { + super(spawnCount, lowKey, highKey, numSources); + } + + @Override + protected void transfer(BTreeCursor source, BTreeCursor unused) throws IOException { + LocalPool.Entry entry = mObserverPool.access(); + + try { + Observer obs = entry.get(); + if (obs == null) { + int height = source.height(); + begin(source, height); + obs = new Observer(mObserver, height); + entry.replace(obs); + } + + Node[] stack = obs.mStack; + + if (!source.verifyFrames(stack.length, stack, source.mFrame, obs)) { + mStopped = true; + stop(); + return; + } + + source.skipToNextLeaf(); + } finally { + entry.release(); + } + } + + @Override + protected void skip(BTreeCursor source) throws IOException { + // Nothing should be skipped. + throw new AssertionError(); + } + } + + final class Observer extends VerifyObserver { + Node[] mStack; + + Observer(VerificationObserver wrapped, int height) { + super(wrapped); + mStack = new Node[height]; + } + } +} diff --git a/src/main/java/org/cojen/tupl/core/LocalDatabase.java b/src/main/java/org/cojen/tupl/core/LocalDatabase.java index 275fd02f1..525f9244d 100644 --- a/src/main/java/org/cojen/tupl/core/LocalDatabase.java +++ b/src/main/java/org/cojen/tupl/core/LocalDatabase.java @@ -2910,7 +2910,7 @@ private boolean doCompactFile(CompactionObserver observer, double target) throws } @Override - public boolean verify(VerificationObserver observer) throws IOException { + public boolean verify(VerificationObserver observer, int numThreads) throws IOException { var fls = new FreeListScan(); Runner.start(fls); @@ -2919,7 +2919,7 @@ public boolean verify(VerificationObserver observer) throws IOException { scanAllIndexes(ix -> { var tree = (Tree) ix; Index view = tree.observableView(); - return tree.verifyTree(view, vo) && vo.indexComplete(view, true, null); + return tree.verifyTree(view, vo, numThreads) && vo.indexComplete(view, true, null); }); // Throws an exception if it fails. diff --git a/src/main/java/org/cojen/tupl/core/Tree.java b/src/main/java/org/cojen/tupl/core/Tree.java index e3d8d5007..b75398e75 100644 --- a/src/main/java/org/cojen/tupl/core/Tree.java +++ b/src/main/java/org/cojen/tupl/core/Tree.java @@ -63,10 +63,10 @@ abstract boolean compactTree(Index view, long highestNodeId, CompactionObserver throws IOException; @Override - public final boolean verify(VerificationObserver observer) throws IOException { + public final boolean verify(VerificationObserver observer, int numThreads) throws IOException { var vo = new VerifyObserver(observer); Index view = observableView(); - if (verifyTree(view, vo)) { + if (verifyTree(view, vo, numThreads)) { vo.indexComplete(view, true, null); } return vo.passed(); @@ -76,7 +76,8 @@ public final boolean verify(VerificationObserver observer) throws IOException { * @param view view to pass to observer * @return false if should stop */ - abstract boolean verifyTree(Index view, VerifyObserver observer) throws IOException; + abstract boolean verifyTree(Index view, VerifyObserver observer, int numThreads) + throws IOException; /** * Count the number of cursors bound to the tree. diff --git a/src/main/java/org/cojen/tupl/core/VerifyObserver.java b/src/main/java/org/cojen/tupl/core/VerifyObserver.java index f56aabb9e..862e5aa8e 100644 --- a/src/main/java/org/cojen/tupl/core/VerifyObserver.java +++ b/src/main/java/org/cojen/tupl/core/VerifyObserver.java @@ -28,7 +28,7 @@ * * @author Brian S. O'Neill */ -final class VerifyObserver extends VerificationObserver { +class VerifyObserver extends VerificationObserver { private final VerificationObserver mWrapped; private volatile boolean mFailed; @@ -46,43 +46,43 @@ final class VerifyObserver extends VerificationObserver { } @Override - public boolean indexBegin(Index index, int height) { + public final boolean indexBegin(Index index, int height) { return mWrapped.indexBegin(index, height); } @Override - public boolean indexComplete(Index index, boolean passed, String message) { + public final boolean indexComplete(Index index, boolean passed, String message) { return mWrapped.indexComplete(index, passed & !mFailed, message); } @Override - public boolean indexNodePassed(long id, int level, - int entryCount, int freeBytes, int largeValueCount) + public final boolean indexNodePassed(long id, int level, + int entryCount, int freeBytes, int largeValueCount) { return mWrapped.indexNodePassed(id, level, entryCount, freeBytes, largeValueCount); } @Override - public boolean indexNodeFailed(long id, int level, String message) { + public final boolean indexNodeFailed(long id, int level, String message) { mFailed = true; return mWrapped.indexNodeFailed(id, level, message); } - boolean passed() { + final boolean passed() { return !mFailed; } /** * @see Node#verifyTreeNode */ - void heldShared() { + final void heldShared() { mLatched = true; } /** * @see Node#verifyTreeNode */ - void releaseShared(Latch latch) { + final void releaseShared(Latch latch) { if (mLatched) { mLatched = false; latch.releaseShared(); diff --git a/src/main/java/org/cojen/tupl/diag/VerificationObserver.java b/src/main/java/org/cojen/tupl/diag/VerificationObserver.java index 2c25e6e59..13c8b20aa 100644 --- a/src/main/java/org/cojen/tupl/diag/VerificationObserver.java +++ b/src/main/java/org/cojen/tupl/diag/VerificationObserver.java @@ -21,8 +21,9 @@ import org.cojen.tupl.Index; /** - * Index verification observer. Implementation does not need to be thread-safe, but instances - * should not be shared by concurrent verifications. + * Index verification observer. Because verification can use multiple threads, a few of the + * observer methods need to be thread-safe: {@link #indexNodePassed indexNodePassed} and {@link + * #indexNodeFailed indexNodeFailed}. * * @author Brian S O'Neill * @see Database#verify Database.verify diff --git a/src/main/java/org/cojen/tupl/jmx/Registration.java b/src/main/java/org/cojen/tupl/jmx/Registration.java index 5a14780d4..e2ecc02ec 100644 --- a/src/main/java/org/cojen/tupl/jmx/Registration.java +++ b/src/main/java/org/cojen/tupl/jmx/Registration.java @@ -372,7 +372,7 @@ public boolean indexNodeFailed(long id, int level, String message) { } }; - if (db.verify(observer)) { + if (db.verify(observer, 0)) { return "passed"; } else { String message = observer.failed; diff --git a/src/main/java/org/cojen/tupl/remote/ClientDatabase.java b/src/main/java/org/cojen/tupl/remote/ClientDatabase.java index ce3184cfb..7aa973b94 100644 --- a/src/main/java/org/cojen/tupl/remote/ClientDatabase.java +++ b/src/main/java/org/cojen/tupl/remote/ClientDatabase.java @@ -360,9 +360,9 @@ public boolean compactFile(CompactionObserver observer, double target) throws IO } @Override - public boolean verify(VerificationObserver observer) throws IOException { + public boolean verify(VerificationObserver observer, int numThreads) throws IOException { var server = ServerVerificationObserver.make(this, observer); - return server.check(mRemote.verify(server.flags(), server)); + return server.check(mRemote.verify(server.flags(), server, numThreads)); } @Override diff --git a/src/main/java/org/cojen/tupl/remote/ClientIndex.java b/src/main/java/org/cojen/tupl/remote/ClientIndex.java index ff4a11c7b..5f246d02a 100644 --- a/src/main/java/org/cojen/tupl/remote/ClientIndex.java +++ b/src/main/java/org/cojen/tupl/remote/ClientIndex.java @@ -86,9 +86,9 @@ public IndexStats analyze(byte[] lowKey, byte[] highKey) throws IOException { } @Override - public boolean verify(VerificationObserver observer) throws IOException { + public boolean verify(VerificationObserver observer, int numThreads) throws IOException { var server = ServerVerificationObserver.make(mDb, observer); - return server.check(mRemote.verify(server.flags(), server)); + return server.check(mRemote.verify(server.flags(), server, numThreads)); } @Override diff --git a/src/main/java/org/cojen/tupl/remote/RemoteDatabase.java b/src/main/java/org/cojen/tupl/remote/RemoteDatabase.java index 7a7410aa8..c9f207abf 100644 --- a/src/main/java/org/cojen/tupl/remote/RemoteDatabase.java +++ b/src/main/java/org/cojen/tupl/remote/RemoteDatabase.java @@ -110,7 +110,8 @@ public boolean compactFile(int flags, RemoteCompactionObserver observer, double /** * @param flags bit 1: provide indexNodePassed messages */ - public boolean verify(int flags, RemoteVerificationObserver observer) throws IOException; + public boolean verify(int flags, RemoteVerificationObserver observer, int numThreads) + throws IOException; @RemoteFailure(declared=false) public boolean isLeader(); diff --git a/src/main/java/org/cojen/tupl/remote/RemoteIndex.java b/src/main/java/org/cojen/tupl/remote/RemoteIndex.java index 8df124eee..51038891e 100644 --- a/src/main/java/org/cojen/tupl/remote/RemoteIndex.java +++ b/src/main/java/org/cojen/tupl/remote/RemoteIndex.java @@ -59,7 +59,8 @@ public long evict(RemoteTransaction txn, byte[] lowKey, byte[] highKey, /** * @param flags bit 1: provide indexNodePassed messages */ - public boolean verify(int flags, RemoteVerificationObserver observer) throws IOException; + public boolean verify(int flags, RemoteVerificationObserver observer, int numThreads) + throws IOException; @Disposer public void close() throws IOException; diff --git a/src/main/java/org/cojen/tupl/remote/ServerDatabase.java b/src/main/java/org/cojen/tupl/remote/ServerDatabase.java index 626681d39..83a90c808 100644 --- a/src/main/java/org/cojen/tupl/remote/ServerDatabase.java +++ b/src/main/java/org/cojen/tupl/remote/ServerDatabase.java @@ -188,8 +188,10 @@ public boolean compactFile(int flags, RemoteCompactionObserver remote, double ta } @Override - public boolean verify(int flags, RemoteVerificationObserver remote) throws IOException { - return VerificationObserverRelay.verify(flags, remote, mDb::verify); + public boolean verify(int flags, RemoteVerificationObserver remote, int numThreads) + throws IOException + { + return VerificationObserverRelay.verify(flags, remote, obs -> mDb.verify(obs, numThreads)); } @Override diff --git a/src/main/java/org/cojen/tupl/remote/ServerIndex.java b/src/main/java/org/cojen/tupl/remote/ServerIndex.java index 3636d123c..839ad63a8 100644 --- a/src/main/java/org/cojen/tupl/remote/ServerIndex.java +++ b/src/main/java/org/cojen/tupl/remote/ServerIndex.java @@ -79,8 +79,11 @@ public IndexStats analyze(byte[] lowKey, byte[] highKey) throws IOException { } @Override - public boolean verify(int flags, RemoteVerificationObserver remote) throws IOException { - return VerificationObserverRelay.verify(flags, remote, mView::verify); + public boolean verify(int flags, RemoteVerificationObserver remote, int numThreads) + throws IOException + { + return VerificationObserverRelay.verify + (flags, remote, obs -> mView.verify(obs, numThreads)); } @Override diff --git a/src/main/java/org/cojen/tupl/remote/VerificationObserverRelay.java b/src/main/java/org/cojen/tupl/remote/VerificationObserverRelay.java index e2678c3ba..3a039f1e2 100644 --- a/src/main/java/org/cojen/tupl/remote/VerificationObserverRelay.java +++ b/src/main/java/org/cojen/tupl/remote/VerificationObserverRelay.java @@ -62,7 +62,7 @@ public boolean indexNodeFailed(long id, int level, String message) { private final boolean mPassMessages; private final RemoteVerificationObserver mRemote; - private Pipe mPipe; + private volatile Pipe mPipe; private VerificationObserverRelay(int flags, RemoteVerificationObserver remote) { mPassMessages = (flags & 1) != 0; @@ -126,28 +126,30 @@ public boolean indexNodePassed(long id, int level, return true; } - try { - Pipe pipe = mPipe; - - if (pipe == null) { - mPipe = pipe = mRemote.indexNodePassed(null); - // Notify the remote side to expect terminators and wait for an ack. - pipe.write(1); - pipe.flush(); - if (pipe.read() < 0) { - return false; + synchronized (this) { + try { + Pipe pipe = mPipe; + + if (pipe == null) { + mPipe = pipe = mRemote.indexNodePassed(null); + // Notify the remote side to expect terminators and wait for an ack. + pipe.write(1); + pipe.flush(); + if (pipe.read() < 0) { + return false; + } } - } - pipe.writeLong(id); - pipe.writeInt(level); - pipe.writeInt(entryCount); - pipe.writeInt(freeBytes); - pipe.writeInt(largeValueCount); + pipe.writeLong(id); + pipe.writeInt(level); + pipe.writeInt(entryCount); + pipe.writeInt(freeBytes); + pipe.writeInt(largeValueCount); - return true; - } catch (IOException e) { - return false; + return true; + } catch (IOException e) { + return false; + } } } diff --git a/src/main/java/org/cojen/tupl/tools/Verify.java b/src/main/java/org/cojen/tupl/tools/Verify.java index 8251f1ba8..615aa720a 100644 --- a/src/main/java/org/cojen/tupl/tools/Verify.java +++ b/src/main/java/org/cojen/tupl/tools/Verify.java @@ -25,9 +25,9 @@ import org.cojen.tupl.diag.VerificationObserver; /** - * Simple database verification utility. Main method requires a single argument — a base - * file path for the database. An optional cache size can be provided too. Main method exits - * with a status of 1 if verification failed, 0 if succeeded. + * Simple database verification utility. The main method requires a single argument — a + * base file path for the database. An optional cache size can be provided too. The main method + * exits with a status of 1 if verification failed, or else 0 if it succeeded. * * @author Brian S O'Neill * @see Database#verify Database.verify @@ -52,7 +52,7 @@ public static void main(String[] args) throws Exception { System.out.println(db.stats()); var v = new Verify(); - db.verify(v); + db.verify(v, 0); System.out.println(v); System.exit(v.failed); } @@ -74,11 +74,11 @@ public boolean indexBegin(Index ix, int height) { } @Override - public boolean indexNodePassed(long id, - int level, - int entryCount, - int freeBytes, - int largeValueCount) + public synchronized boolean indexNodePassed(long id, + int level, + int entryCount, + int freeBytes, + int largeValueCount) { totalEntryCount += entryCount; totalFreeBytes += freeBytes; @@ -90,7 +90,7 @@ public boolean indexNodePassed(long id, } @Override - public boolean indexNodeFailed(long id, int level, String message) { + public synchronized boolean indexNodeFailed(long id, int level, String message) { failed = 1; return super.indexNodeFailed(id, level, message); } diff --git a/src/main/java/org/cojen/tupl/views/UnmodifiableView.java b/src/main/java/org/cojen/tupl/views/UnmodifiableView.java index 707a2790a..0957f5310 100644 --- a/src/main/java/org/cojen/tupl/views/UnmodifiableView.java +++ b/src/main/java/org/cojen/tupl/views/UnmodifiableView.java @@ -295,7 +295,7 @@ public IndexStats analyze(byte[] lowKey, byte[] highKey) throws IOException { } @Override - public boolean verify(VerificationObserver observer) throws IOException { + public boolean verify(VerificationObserver observer, int numThreads) throws IOException { if (!(mSource instanceof Index ix)) { return true; } @@ -339,7 +339,7 @@ private Index wrap(Index index) { }; } - return ix.verify(obs); + return ix.verify(obs, numThreads); } @Override diff --git a/src/test/java/org/cojen/tupl/core/BTreeGraftTest.java b/src/test/java/org/cojen/tupl/core/BTreeGraftTest.java index 9a2962db2..0cc2f9afe 100644 --- a/src/test/java/org/cojen/tupl/core/BTreeGraftTest.java +++ b/src/test/java/org/cojen/tupl/core/BTreeGraftTest.java @@ -194,7 +194,7 @@ private static void findInFilledTree(BTree tree, int count, Random rnd, byte key private static int height(BTree tree) throws Exception { var obs = new HeightObserver(); - assertTrue(tree.verify(obs)); + assertTrue(tree.verify(obs, 1)); return obs.height(); } diff --git a/src/test/java/org/cojen/tupl/core/CheckpointFailureTest.java b/src/test/java/org/cojen/tupl/core/CheckpointFailureTest.java index b9812938e..d03445359 100644 --- a/src/test/java/org/cojen/tupl/core/CheckpointFailureTest.java +++ b/src/test/java/org/cojen/tupl/core/CheckpointFailureTest.java @@ -102,7 +102,7 @@ public void checkpointResume() throws Exception { config.dataPageArray(pa); mDb = Database.open(config); - assertTrue(mDb.verify(null)); + assertTrue(mDb.verify(null, 1)); Index ix = mDb.openIndex("test"); diff --git a/src/test/java/org/cojen/tupl/core/CloseTest.java b/src/test/java/org/cojen/tupl/core/CloseTest.java index 2ee279c72..872c76ede 100644 --- a/src/test/java/org/cojen/tupl/core/CloseTest.java +++ b/src/test/java/org/cojen/tupl/core/CloseTest.java @@ -653,7 +653,7 @@ private void verify(boolean delete) throws Exception { } try { - ix.verify(null); + ix.verify(null, 1); fail(); } catch (ClosedIndexException e) { if (delete) { diff --git a/src/test/java/org/cojen/tupl/core/CompactTest.java b/src/test/java/org/cojen/tupl/core/CompactTest.java index 63dad22de..9b114105b 100644 --- a/src/test/java/org/cojen/tupl/core/CompactTest.java +++ b/src/test/java/org/cojen/tupl/core/CompactTest.java @@ -115,7 +115,7 @@ public void basic() throws Exception { assertTrue(stats2.totalPages < stats1.totalPages); } - assertTrue(mDb.verify(null)); + assertTrue(mDb.verify(null, 1)); rnd = new Random(seed); for (int i=0; i T startAndWaitUntilBlocked(T t) throws InterruptedException { diff --git a/src/test/java/org/cojen/tupl/core/CursorBasicFilterTest.java b/src/test/java/org/cojen/tupl/core/CursorBasicFilterTest.java index 26b5b63d6..d70a3aa9d 100644 --- a/src/test/java/org/cojen/tupl/core/CursorBasicFilterTest.java +++ b/src/test/java/org/cojen/tupl/core/CursorBasicFilterTest.java @@ -55,7 +55,7 @@ protected View openIndex(String name) throws Exception { @Override protected boolean verify(View view) throws Exception { - return mViews.get(view).verify(null); + return mViews.get(view).verify(null, 1); } @Override diff --git a/src/test/java/org/cojen/tupl/core/CursorBasicTransformTest.java b/src/test/java/org/cojen/tupl/core/CursorBasicTransformTest.java index f6fb8d963..6fbf164d8 100644 --- a/src/test/java/org/cojen/tupl/core/CursorBasicTransformTest.java +++ b/src/test/java/org/cojen/tupl/core/CursorBasicTransformTest.java @@ -45,7 +45,7 @@ protected View openIndex(String name) throws Exception { @Override protected boolean verify(View view) throws Exception { - return mViews.get(view).verify(null); + return mViews.get(view).verify(null, 1); } @Override diff --git a/src/test/java/org/cojen/tupl/core/CursorDefaultTest.java b/src/test/java/org/cojen/tupl/core/CursorDefaultTest.java index 80c665dad..aa004d2ae 100644 --- a/src/test/java/org/cojen/tupl/core/CursorDefaultTest.java +++ b/src/test/java/org/cojen/tupl/core/CursorDefaultTest.java @@ -36,7 +36,7 @@ protected View openIndex(String name) throws Exception { @Override protected boolean verify(View ix) throws Exception { - return ((Index) (((DefaultView) ix).mSource)).verify(null); + return ((Index) (((DefaultView) ix).mSource)).verify(null, 1); } @Override diff --git a/src/test/java/org/cojen/tupl/core/CursorDisjointUnionTest.java b/src/test/java/org/cojen/tupl/core/CursorDisjointUnionTest.java index 0842cead4..3ad2de2c2 100644 --- a/src/test/java/org/cojen/tupl/core/CursorDisjointUnionTest.java +++ b/src/test/java/org/cojen/tupl/core/CursorDisjointUnionTest.java @@ -48,7 +48,7 @@ protected View openIndex(String name) throws Exception { protected boolean verify(View view) throws Exception { boolean result = true; for (Index ix : mViews.get(view)) { - result &= ix.verify(null); + result &= ix.verify(null, 1); } return result; } diff --git a/src/test/java/org/cojen/tupl/core/CursorNonTransformTest.java b/src/test/java/org/cojen/tupl/core/CursorNonTransformTest.java index 6ef4bf4b5..10732b73d 100644 --- a/src/test/java/org/cojen/tupl/core/CursorNonTransformTest.java +++ b/src/test/java/org/cojen/tupl/core/CursorNonTransformTest.java @@ -46,7 +46,7 @@ protected View openIndex(String name) throws Exception { @Override protected boolean verify(View view) throws Exception { - return mViews.get(view).verify(null); + return mViews.get(view).verify(null, 1); } @Override diff --git a/src/test/java/org/cojen/tupl/core/CursorTest.java b/src/test/java/org/cojen/tupl/core/CursorTest.java index 3d5b9e3cd..2117bab4d 100644 --- a/src/test/java/org/cojen/tupl/core/CursorTest.java +++ b/src/test/java/org/cojen/tupl/core/CursorTest.java @@ -59,7 +59,7 @@ protected View openIndex(String name) throws Exception { } protected boolean verify(View ix) throws Exception { - return ((Index) ix).verify(null); + return ((Index) ix).verify(null, 1); } protected T startAndWaitUntilBlocked(T t) throws InterruptedException { diff --git a/src/test/java/org/cojen/tupl/core/EnduranceTest.java b/src/test/java/org/cojen/tupl/core/EnduranceTest.java index c6beac5b1..7b2133ce7 100644 --- a/src/test/java/org/cojen/tupl/core/EnduranceTest.java +++ b/src/test/java/org/cojen/tupl/core/EnduranceTest.java @@ -470,7 +470,7 @@ public void testBasic() throws Exception { assertFalse(failureRatePercentage > 0.1); // System.out.printf("NumOperations=%d, FailureRate = %.2f%%\n", numOperations, failureRatePercentage); - assertTrue(mIx.verify(null)); + assertTrue(mIx.verify(null, 1)); } interface Worker extends Runnable { diff --git a/src/test/java/org/cojen/tupl/core/EvictionTest.java b/src/test/java/org/cojen/tupl/core/EvictionTest.java index c7e1f2aa1..0067a98ef 100644 --- a/src/test/java/org/cojen/tupl/core/EvictionTest.java +++ b/src/test/java/org/cojen/tupl/core/EvictionTest.java @@ -161,7 +161,7 @@ public void testEvictBase() throws IOException { assertEquals(0, evictionFilter.mValues.size()); txn.reset(); - assertTrue(ix.verify(null)); + assertTrue(ix.verify(null, 1)); } private String textOfLength(int prefix, char c, int len) { diff --git a/src/test/java/org/cojen/tupl/core/LargeKeyTest.java b/src/test/java/org/cojen/tupl/core/LargeKeyTest.java index e107a5cc4..23412efe9 100644 --- a/src/test/java/org/cojen/tupl/core/LargeKeyTest.java +++ b/src/test/java/org/cojen/tupl/core/LargeKeyTest.java @@ -59,7 +59,7 @@ public void largeBlanks() throws Exception { for (byte[] key : keys) { ix.store(null, key, value); - assertTrue(ix.verify(null)); + assertTrue(ix.verify(null, 1)); } for (byte[] key : keys) { @@ -100,7 +100,7 @@ private void storeMaxSize(final int pageSize) throws Exception { ix.store(null, key, value); } - assertTrue("Verification failed for page size of: " + pageSize, ix.verify(null)); + assertTrue("Verification failed for page size of: " + pageSize, ix.verify(null, 1)); for (byte[] key : keys) { byte[] v = ix.load(null, key); @@ -147,7 +147,7 @@ private void storeMaxSizeFull(final int pageSize) throws Exception { ix.store(null, key, value); } - assertTrue("Verification failed for page size of: " + pageSize, ix.verify(null)); + assertTrue("Verification failed for page size of: " + pageSize, ix.verify(null, 1)); for (byte[] key : keys) { byte[] v = ix.load(null, key); @@ -192,7 +192,7 @@ public void veryLargeKeys() throws Exception { } if (t == 0) { - assertTrue(ix.verify(null)); + assertTrue(ix.verify(null, 1)); } } } @@ -233,7 +233,7 @@ public void updateAgainstLargeKeys() throws Exception { ix.store(Transaction.BOGUS, key, value); } - ix.verify(null); + ix.verify(null, 1); rnd = new Random(seed); for (int i=0; i<1000; i++) { diff --git a/src/test/java/org/cojen/tupl/core/LargePageTest.java b/src/test/java/org/cojen/tupl/core/LargePageTest.java index e7cef6fa1..43977c659 100644 --- a/src/test/java/org/cojen/tupl/core/LargePageTest.java +++ b/src/test/java/org/cojen/tupl/core/LargePageTest.java @@ -96,7 +96,7 @@ public void deleteLast() throws Exception { db.checkpoint(); db = reopenTempDatabase(getClass(), db, config); - assertTrue(db.verify(null)); + assertTrue(db.verify(null, 1)); } @Test diff --git a/src/test/java/org/cojen/tupl/core/LargeValueFuzzTest.java b/src/test/java/org/cojen/tupl/core/LargeValueFuzzTest.java index f96255e61..ca5e57e12 100644 --- a/src/test/java/org/cojen/tupl/core/LargeValueFuzzTest.java +++ b/src/test/java/org/cojen/tupl/core/LargeValueFuzzTest.java @@ -147,7 +147,7 @@ private void doInsertFuzz(long seed, int iterations, byte[] key = rndBytes(rnd, keyMin, keyMax); byte[] value = rndBytes(rnd, valueMin, valueMax); ix.store(Transaction.BOGUS, key, value); - assertTrue(ix.verify(null)); + assertTrue(ix.verify(null, 1)); } mDb.deleteIndex(ix).run(); diff --git a/src/test/java/org/cojen/tupl/core/LargeValueTest.java b/src/test/java/org/cojen/tupl/core/LargeValueTest.java index 7c6eb125d..8c5eac03e 100644 --- a/src/test/java/org/cojen/tupl/core/LargeValueTest.java +++ b/src/test/java/org/cojen/tupl/core/LargeValueTest.java @@ -138,7 +138,7 @@ public void testUpdateLarger() throws Exception { // Assuming 4096 byte nodes, the inline content length is still between 1 and 128. ix.store(Transaction.BOGUS, k, new byte[4121]); - assertTrue(ix.verify(null)); + assertTrue(ix.verify(null, 0)); } @Test @@ -238,7 +238,7 @@ public void testInsertLargeKeyOverflow() throws Exception { ix.store(null, key(2500, 2), new byte[3000]); // Without the fix, this would fail because the garbage field didn't match actual usage. - assertTrue(ix.verify(new VerificationObserver())); + assertTrue(ix.verify(new VerificationObserver(), 0)); } @Test @@ -271,7 +271,7 @@ public void testInsertLargeKeyLateFragment() throws Exception { ix.store(null, key(3), new byte[3050]); - assertTrue(ix.verify(new VerificationObserver())); + assertTrue(ix.verify(new VerificationObserver(), 0)); } @Test @@ -289,7 +289,7 @@ public void testInsertLargeKeyLateFragment2() throws Exception { ix.store(null, key(1), new byte[3050]); - assertTrue(ix.verify(new VerificationObserver())); + assertTrue(ix.verify(new VerificationObserver(), -2)); } @Test @@ -307,7 +307,7 @@ public void testUpdateLargeKeyLateFragment() throws Exception { ix.store(null, key(3), new byte[3050]); - assertTrue(ix.verify(new VerificationObserver())); + assertTrue(ix.verify(new VerificationObserver(), 1)); } @Test @@ -324,7 +324,7 @@ public void testUpdateLargeKeyLateFragment2() throws Exception { ix.store(null, key(3), new byte[3050]); - assertTrue(ix.verify(new VerificationObserver())); + assertTrue(ix.verify(new VerificationObserver(), 1)); } @Test @@ -350,7 +350,7 @@ public void testUpdateLargeKeyLateFragment3() throws Exception { ix.store(null, filledKey(332, 1), filledValue(2672)); - assertTrue(ix.verify(new VerificationObserver())); + assertTrue(ix.verify(new VerificationObserver(), 1)); } @Test @@ -365,7 +365,7 @@ public void testInsertLargeValueEarlyFragment() throws Exception { ix.store(null, key(2026, 1), new byte[1060]); - assertTrue(ix.verify(new VerificationObserver())); + assertTrue(ix.verify(new VerificationObserver(), 1)); } private static byte[] key(int i) { @@ -491,6 +491,6 @@ public void testFragmentRollback() throws Exception { txns[i].reset(); } - assertTrue(ix.verify(new VerificationObserver())); + assertTrue(ix.verify(new VerificationObserver(), 0)); } } diff --git a/src/test/java/org/cojen/tupl/core/RecoverTest.java b/src/test/java/org/cojen/tupl/core/RecoverTest.java index 3e1b2fb23..c9e81f84b 100644 --- a/src/test/java/org/cojen/tupl/core/RecoverTest.java +++ b/src/test/java/org/cojen/tupl/core/RecoverTest.java @@ -527,12 +527,12 @@ private void testRecover(int count, boolean commit, boolean exit, int chkpnt) } mDb = reopenTempDatabase(getClass(), mDb, mConfig); - assertTrue(mDb.verify(null)); + assertTrue(mDb.verify(null, 1)); ix1 = mDb.openIndex("test1"); - assertTrue(ix1.verify(null)); + assertTrue(ix1.verify(null, 1)); ix2 = mDb.openIndex("test2"); - assertTrue(ix2.verify(null)); + assertTrue(ix2.verify(null, 1)); if (!commit) { assertEquals(0, CrudTest.count(ix1)); diff --git a/src/test/java/org/cojen/tupl/core/SnapshotMappedDirectTest.java b/src/test/java/org/cojen/tupl/core/SnapshotMappedDirectTest.java index 5e157a1e1..828e0eee7 100644 --- a/src/test/java/org/cojen/tupl/core/SnapshotMappedDirectTest.java +++ b/src/test/java/org/cojen/tupl/core/SnapshotMappedDirectTest.java @@ -163,7 +163,7 @@ public synchronized void close() throws IOException { Database restored = Database.open(restoredConfig); - assertTrue(restored.verify(null)); + assertTrue(restored.verify(null, 2)); ix = restored.findIndex("test"); assertNotNull(ix); diff --git a/src/test/java/org/cojen/tupl/core/SnapshotTest.java b/src/test/java/org/cojen/tupl/core/SnapshotTest.java index f12ce3e51..77e12283d 100644 --- a/src/test/java/org/cojen/tupl/core/SnapshotTest.java +++ b/src/test/java/org/cojen/tupl/core/SnapshotTest.java @@ -243,7 +243,7 @@ public void close() throws IOException { s.close(); t.join(); - assertTrue(db.verify(null)); + assertTrue(db.verify(null, 0)); db.close(); assertEquals(expectedLength, snapshot.length()); @@ -257,7 +257,7 @@ public void close() throws IOException { decorate(restoredConfig); final Database restored = Database.open(restoredConfig); - assertTrue(restored.verify(null)); + assertTrue(restored.verify(null, 1)); final Index restoredIx = restored.openIndex("test1"); for (int i=0; i<10000000; i++) { @@ -422,7 +422,7 @@ public void restoreToMappedFile() throws Exception { Database restored = Database.restoreFromSnapshot (restoredConfig, new FileInputStream(snapshotFile)); - assertTrue(restored.verify(null)); + assertTrue(restored.verify(null, 0)); Index restoredIx = restored.openIndex("test1"); assertEquals(indexId, restoredIx.id()); diff --git a/src/test/java/org/cojen/tupl/core/TransformerTest.java b/src/test/java/org/cojen/tupl/core/TransformerTest.java index d52de5ef2..d0cef6a6a 100644 --- a/src/test/java/org/cojen/tupl/core/TransformerTest.java +++ b/src/test/java/org/cojen/tupl/core/TransformerTest.java @@ -54,7 +54,7 @@ protected View openIndex(String name) throws Exception { } protected boolean verify(View ix) throws Exception { - return ((Index) ix).verify(null); + return ((Index) ix).verify(null, 1); } protected Database mDb; diff --git a/src/test/java/org/cojen/tupl/core/ValueAccessorTest.java b/src/test/java/org/cojen/tupl/core/ValueAccessorTest.java index 3358f7211..fa7b73b66 100644 --- a/src/test/java/org/cojen/tupl/core/ValueAccessorTest.java +++ b/src/test/java/org/cojen/tupl/core/ValueAccessorTest.java @@ -472,7 +472,7 @@ protected void extendExisting(int fromLen, long toLen, boolean fullCheck, boolea accessor.close(); if (fullCheck) { - assertTrue(ix.verify(null)); + assertTrue(ix.verify(null, 0)); } ix.delete(Transaction.BOGUS, key); @@ -577,7 +577,7 @@ private static void truncate(Database db, int from, int to, boolean undo) throws fastAssertArrayEquals(value, ix.load(null, key)); } - assertTrue(ix.verify(null)); + assertTrue(ix.verify(null, 1)); } @Test @@ -616,7 +616,7 @@ public void truncateFragmentedDirectExtend() throws Exception { assertEquals(0, loaded2[i]); } - assertTrue(ix.verify(null)); + assertTrue(ix.verify(null, -1)); } @Test @@ -770,7 +770,7 @@ private void truncateFragmentedIndirect(int oldLen, int newLen, } } - assertTrue(ix.verify(null)); + assertTrue(ix.verify(null, 0)); } @Test @@ -856,7 +856,7 @@ private static void writeNonFragmented(Database db, int size) throws Exception { fastAssertArrayEquals(expect, ix.load(null, key)); } - assertTrue(ix.verify(null)); + assertTrue(ix.verify(null, 1)); } @Test @@ -886,7 +886,7 @@ public void replaceGhost() throws Exception { fastAssertArrayEquals(value2, ix.load(Transaction.BOGUS, key)); - assertTrue(ix.verify(null)); + assertTrue(ix.verify(null, 1)); } @Test @@ -922,9 +922,9 @@ private void fieldIncreaseToIndirect(boolean checkpoint) throws Exception { fastAssertArrayEquals(value2, ix.load(null, key)); - assertTrue(ix.verify(null)); + assertTrue(ix.verify(null, 1)); ix.store(Transaction.BOGUS, key, null); - assertTrue(ix.verify(null)); + assertTrue(ix.verify(null, 1)); } @Test @@ -1207,7 +1207,7 @@ private void clearFragmented(int length, int clearPos, int clearLen, int pos2, i accessor.close(); - assertTrue(ix.verify(null)); + assertTrue(ix.verify(null, 1)); } @Test @@ -1790,7 +1790,7 @@ public void clearSparseValue() throws Exception { fastAssertArrayEquals(new byte[100_000], c.value()); c.close(); - assertTrue(ix.verify(null)); + assertTrue(ix.verify(null, 1)); } @Test @@ -1806,7 +1806,7 @@ public void truncateSparseValue() throws Exception { fastAssertArrayEquals(new byte[1], c.value()); c.close(); - assertTrue(ix.verify(null)); + assertTrue(ix.verify(null, 1)); // Do again with rollback. @@ -1824,7 +1824,7 @@ public void truncateSparseValue() throws Exception { fastAssertArrayEquals(expect, c.value()); c.close(); - assertTrue(ix.verify(null)); + assertTrue(ix.verify(null, 0)); } @Test @@ -1847,7 +1847,7 @@ public void fullyTruncateSparseValue() throws Exception { fastAssertArrayEquals(new byte[0], c.value()); c.close(); - assertTrue(ix.verify(null)); + assertTrue(ix.verify(null, 0)); // Do again with rollback. @@ -1865,7 +1865,7 @@ public void fullyTruncateSparseValue() throws Exception { fastAssertArrayEquals(expect, c.value()); c.close(); - assertTrue(ix.verify(null)); + assertTrue(ix.verify(null, -1)); } @Test diff --git a/src/test/java/org/cojen/tupl/core/ViewTest.java b/src/test/java/org/cojen/tupl/core/ViewTest.java index a492d8978..d80c2ff65 100644 --- a/src/test/java/org/cojen/tupl/core/ViewTest.java +++ b/src/test/java/org/cojen/tupl/core/ViewTest.java @@ -633,7 +633,7 @@ private void unmodifiable(int mode) throws Exception { } if (view instanceof Index) { - assertTrue(((Index) view).verify(null)); + assertTrue(((Index) view).verify(null, 1)); var obs = new VerificationObserver() { @Override @@ -649,7 +649,7 @@ public boolean indexComplete(Index index, boolean passed, String message) { } }; - assertTrue(((Index) view).verify(obs)); + assertTrue(((Index) view).verify(obs, 1)); } c.reset(); diff --git a/src/test/java/org/cojen/tupl/remote/RemoteTest.java b/src/test/java/org/cojen/tupl/remote/RemoteTest.java index 7c7de990a..37a377cbd 100644 --- a/src/test/java/org/cojen/tupl/remote/RemoteTest.java +++ b/src/test/java/org/cojen/tupl/remote/RemoteTest.java @@ -138,8 +138,8 @@ public void misc() throws Exception { var temp = mClientDb.newTemporaryIndex(); assertNull(temp.name()); - assertTrue(temp.verify(null)); - assertTrue(mClientDb.verify(null)); + assertTrue(temp.verify(null, 1)); + assertTrue(mClientDb.verify(null, 1)); assertTrue(temp.isEmpty()); byte[] key = "hello".getBytes(); @@ -178,14 +178,14 @@ public boolean indexNodePassed(long id, int level, } }; - assertTrue(temp.verify(vo)); + assertTrue(temp.verify(vo, 0)); assertFalse(vo.fail); assertTrue(vo.pass); vo.pass = false; ix.drop(); - assertTrue(mClientDb.verify(vo)); + assertTrue(mClientDb.verify(vo, 0)); assertFalse(vo.fail); assertTrue(vo.pass);