This is an automated email from the ASF dual-hosted git repository.

dcapwell pushed a commit to branch trunk
in repository https://gitbox.apache.org/repos/asf/cassandra-accord.git


The following commit(s) were added to refs/heads/trunk by this push:
     new bc81f81c CEP-15: (Accord) Migrate Accord away from JDK random to a new 
interface RandomSource
bc81f81c is described below

commit bc81f81c75f93c73989a30bbc51b5c241a893c1a
Author: David Capwell <[email protected]>
AuthorDate: Thu Jan 26 12:06:53 2023 -0800

    CEP-15: (Accord) Migrate Accord away from JDK random to a new interface 
RandomSource
    
    patch by David Capwell; reviewed by Blake Eggleston for CASSANDRA-18213
---
 accord-core/build.gradle                           |   3 -
 accord-core/src/main/java/accord/local/Node.java   |  10 +-
 .../src/main/java/accord/primitives/RangeDeps.java |   9 +-
 .../src/main/java/accord/utils/DefaultRandom.java  |  44 ++--
 .../src/main/java/accord/utils/RandomSource.java   | 269 +++++++++++++++++++++
 .../main/java/accord/utils/RelationMultiMap.java   |   4 +-
 .../java/accord/utils/WrappedRandomSource.java     |  97 ++++++++
 .../src/test/java/accord/burn/BurnTest.java        |  25 +-
 .../accord/burn/BurnTestConfigurationService.java  |  13 +-
 .../accord/burn/random/FrequentLargeRange.java     |  76 ++++++
 .../src/test/java/accord/burn/random/IntRange.java |  41 ++--
 .../test/java/accord/burn/random/RandomInt.java    |  36 +--
 .../test/java/accord/burn/random/RandomLong.java   |  34 +--
 .../java/accord/burn/random/RandomRangeTest.java   |  83 +++++++
 .../java/accord/burn/random/RandomWalkRange.java   |  61 +++++
 .../burn/random/SegmentedRandomRangeTest.java      | 182 ++++++++++++++
 .../tracking/FastPathTrackerReconciler.java        |   7 +-
 .../tracking/InvalidationTrackerReconciler.java    |   6 +-
 .../tracking/QuorumTrackerReconciler.java          |   6 +-
 .../coordinate/tracking/ReadTrackerReconciler.java |   6 +-
 .../tracking/RecoveryTrackerReconciler.java        |   6 +-
 .../coordinate/tracking/TrackerReconciler.java     |  18 +-
 .../coordinate/tracking/TrackerReconcilerTest.java |   6 +-
 .../src/test/java/accord/impl/basic/Cluster.java   |  10 +-
 .../src/test/java/accord/impl/basic/NodeSink.java  |   6 +-
 .../java/accord/impl/basic/RandomDelayQueue.java   |  13 +-
 .../java/accord/impl/basic/UniformRandomQueue.java |  13 +-
 .../test/java/accord/impl/mock/MockCluster.java    |   9 +-
 .../java/accord/local/ImmutableCommandTest.java    |   5 +-
 .../test/java/accord/messages/PreAcceptTest.java   |   4 +-
 .../test/java/accord/primitives/KeyDepsTest.java   |  41 ++--
 .../java/accord/topology/TopologyRandomizer.java   |  37 ++-
 .../src/test/java/accord/utils/AccordGens.java     | 154 ++++++++++++
 accord-core/src/test/java/accord/utils/Gen.java    | 108 +++------
 accord-core/src/test/java/accord/utils/Gens.java   | 145 ++++++++++-
 .../src/test/java/accord/utils/Property.java       |  58 ++++-
 accord-maelstrom/build.gradle                      |   2 +-
 .../src/main/java/accord/maelstrom/Cluster.java    |  24 +-
 .../src/main/java/accord/maelstrom/Datum.java      |  16 ++
 .../src/main/java/accord/maelstrom/Json.java       |  82 ++++---
 .../main/java/accord/maelstrom/MaelstromKey.java   |  16 ++
 .../src/main/java/accord/maelstrom/Main.java       |   4 +-
 .../src/test/java/accord/maelstrom/JsonTest.java   |  90 +++++++
 .../src/test/java/accord/maelstrom/Runner.java     | 244 +++++--------------
 .../java/accord/maelstrom/SimpleRandomTest.java    |  58 ++---
 .../src/main/groovy/accord.java-conventions.gradle |   2 +
 46 files changed, 1618 insertions(+), 565 deletions(-)

diff --git a/accord-core/build.gradle b/accord-core/build.gradle
index e92e62b5..3c1472bc 100644
--- a/accord-core/build.gradle
+++ b/accord-core/build.gradle
@@ -18,9 +18,6 @@
 
 plugins {
     id 'accord.java-conventions'
-    // export test classes for subprojects
-    id 'java-library'
-    id 'java-test-fixtures'
     id 'maven-publish'
 }
 
diff --git a/accord-core/src/main/java/accord/local/Node.java 
b/accord-core/src/main/java/accord/local/Node.java
index 344e10b9..fbf59874 100644
--- a/accord-core/src/main/java/accord/local/Node.java
+++ b/accord-core/src/main/java/accord/local/Node.java
@@ -18,7 +18,10 @@
 
 package accord.local;
 
-import java.util.*;
+import java.util.Collection;
+import java.util.HashSet;
+import java.util.Map;
+import java.util.Set;
 import java.util.concurrent.ConcurrentHashMap;
 import java.util.concurrent.atomic.AtomicReference;
 import java.util.function.BiConsumer;
@@ -35,6 +38,7 @@ import accord.utils.MapReduceConsume;
 import accord.utils.async.AsyncChain;
 import accord.utils.async.AsyncResult;
 import accord.utils.async.AsyncResults;
+import accord.utils.RandomSource;
 import com.google.common.annotations.VisibleForTesting;
 
 import accord.api.*;
@@ -119,7 +123,7 @@ public class Node implements ConfigurationService.Listener, 
NodeTimeService
     private final LongSupplier nowSupplier;
     private final AtomicReference<Timestamp> now;
     private final Agent agent;
-    private final Random random;
+    private final RandomSource random;
 
     // TODO (expected, consider): this really needs to be thought through some 
more, as it needs to be per-instance in some cases, and per-node in others
     private final Scheduler scheduler;
@@ -128,7 +132,7 @@ public class Node implements ConfigurationService.Listener, 
NodeTimeService
     private final Map<TxnId, AsyncResult<? extends Outcome>> coordinating = 
new ConcurrentHashMap<>();
 
     public Node(Id id, MessageSink messageSink, ConfigurationService 
configService, LongSupplier nowSupplier,
-                Supplier<DataStore> dataSupplier, ShardDistributor 
shardDistributor, Agent agent, Random random, Scheduler scheduler, 
TopologySorter.Supplier topologySorter,
+                Supplier<DataStore> dataSupplier, ShardDistributor 
shardDistributor, Agent agent, RandomSource random, Scheduler scheduler, 
TopologySorter.Supplier topologySorter,
                 Function<Node, ProgressLog.Factory> progressLogFactory, 
CommandStores.Factory factory)
     {
         this.id = id;
diff --git a/accord-core/src/main/java/accord/primitives/RangeDeps.java 
b/accord-core/src/main/java/accord/primitives/RangeDeps.java
index 3bef67e0..22d49080 100644
--- a/accord-core/src/main/java/accord/primitives/RangeDeps.java
+++ b/accord-core/src/main/java/accord/primitives/RangeDeps.java
@@ -612,14 +612,7 @@ public class RangeDeps implements 
Iterable<Map.Entry<Range, TxnId>>
 
     public static SymmetricComparator<? super Range> rangeComparator()
     {
-        return RangeDeps::compare;
-    }
-
-    public static int compare(Range left, Range right)
-    {
-        int c = left.start().compareTo(right.start());
-        if (c == 0) c = left.end().compareTo(right.end());
-        return c;
+        return Range::compare;
     }
 
     private static final RangeDepsAdapter ADAPTER = new RangeDepsAdapter();
diff --git a/accord-maelstrom/build.gradle 
b/accord-core/src/main/java/accord/utils/DefaultRandom.java
similarity index 53%
copy from accord-maelstrom/build.gradle
copy to accord-core/src/main/java/accord/utils/DefaultRandom.java
index cc08d3f4..8efff223 100644
--- a/accord-maelstrom/build.gradle
+++ b/accord-core/src/main/java/accord/utils/DefaultRandom.java
@@ -16,35 +16,29 @@
  * limitations under the License.
  */
 
-plugins {
-    id 'accord.java-conventions'
-}
+package accord.utils;
 
-dependencies {
-    implementation project(':accord-core')
-    implementation group: 'com.google.code.findbugs', name: 'jsr305', version: 
'3.0.2'
-    implementation 'com.google.code.gson:gson:2.8.7'
+import java.util.Random;
 
-    testImplementation(testFixtures(project(':accord-core')))
-}
+public class DefaultRandom extends Random implements RandomSource
+{
+    public DefaultRandom()
+    {
+    }
 
-jar {
-    manifest {
-        attributes(
-                'Main-Class': 'accord.maelstrom.Main',
-        )
+    public DefaultRandom(long seed)
+    {
+        super(seed);
+    }
+
+    @Override
+    public DefaultRandom fork() {
+        return new DefaultRandom(nextLong());
     }
-}
 
-task fatJar(type: Jar) {
-    manifest.from jar.manifest
-    archiveClassifier = 'all'
-    from {
-        configurations.runtimeClasspath.collect { it.isDirectory() ? it : 
zipTree(it) }
-    } {
-        exclude "META-INF/*.SF"
-        exclude "META-INF/*.DSA"
-        exclude "META-INF/*.RSA"
+    @Override
+    public Random asJdkRandom()
+    {
+        return this;
     }
-    with jar
 }
diff --git a/accord-core/src/main/java/accord/utils/RandomSource.java 
b/accord-core/src/main/java/accord/utils/RandomSource.java
new file mode 100644
index 00000000..56b6ef5e
--- /dev/null
+++ b/accord-core/src/main/java/accord/utils/RandomSource.java
@@ -0,0 +1,269 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package accord.utils;
+
+import java.util.Random;
+import java.util.stream.DoubleStream;
+import java.util.stream.IntStream;
+import java.util.stream.LongStream;
+
+public interface RandomSource
+{
+    static RandomSource wrap(Random random)
+    {
+        return new WrappedRandomSource(random);
+    }
+
+    void nextBytes(byte[] bytes);
+
+    boolean nextBoolean();
+
+    int nextInt();
+
+    default int nextInt(int maxExclusive)
+    {
+        return nextInt(0, maxExclusive);
+    }
+
+    default int nextInt(int minInclusive, int maxExclusive)
+    {
+        // this is diff behavior than ThreadLocalRandom, which returns nextInt
+        if (minInclusive >= maxExclusive)
+            throw new IllegalArgumentException(String.format("Min (%s) should 
be less than max (%d).", minInclusive, maxExclusive));
+
+        int result = nextInt();
+        int delta = maxExclusive - minInclusive;
+        int mask = delta - 1;
+        if ((delta & mask) == 0) // power of two
+            result = (result & mask) + minInclusive;
+        else if (delta > 0)
+        {
+            // reject over-represented candidates
+            for (int u = result >>> 1;                  // ensure nonnegative
+                 u + mask - (result = u % delta) < 0;   // rejection check
+                 u = nextInt() >>> 1)                   // retry
+                ;
+            result += minInclusive;
+        }
+        else
+        {
+            // range not representable as int
+            while (result < minInclusive || result >= maxExclusive)
+                result = nextInt();
+        }
+        return result;
+    }
+
+    default IntStream ints()
+    {
+        return IntStream.generate(this::nextInt);
+    }
+
+    default IntStream ints(int maxExclusive)
+    {
+        return IntStream.generate(() -> nextInt(maxExclusive));
+    }
+
+    default IntStream ints(int minInclusive, int maxExclusive)
+    {
+        return IntStream.generate(() -> nextInt(minInclusive, maxExclusive));
+    }
+
+    long nextLong();
+
+    default long nextLong(long maxExclusive)
+    {
+        return nextLong(0, maxExclusive);
+    }
+
+    default long nextLong(long minInclusive, long maxExclusive)
+    {
+        // this is diff behavior than ThreadLocalRandom, which returns nextLong
+        if (minInclusive >= maxExclusive)
+            throw new IllegalArgumentException(String.format("Min (%s) should 
be less than max (%d).", minInclusive, maxExclusive));
+
+        long result = nextLong();
+        long delta = maxExclusive - minInclusive;
+        long mask = delta - 1;
+        if ((delta & mask) == 0L) // power of two
+            result = (result & mask) + minInclusive;
+        else if (delta > 0L)
+        {
+            // reject over-represented candidates
+            for (long u = result >>> 1;                 // ensure nonnegative
+                 u + mask - (result = u % delta) < 0L;  // rejection check
+                 u = nextLong() >>> 1)                  // retry
+                ;
+            result += minInclusive;
+        }
+        else
+        {
+            // range not representable as long
+            while (result < minInclusive || result >= maxExclusive)
+                result = nextLong();
+        }
+        return result;
+    }
+
+    default LongStream longs()
+    {
+        return LongStream.generate(this::nextLong);
+    }
+
+    default LongStream longs(long maxExclusive)
+    {
+        return LongStream.generate(() -> nextLong(maxExclusive));
+    }
+
+    default LongStream longs(long minInclusive, long maxExclusive)
+    {
+        return LongStream.generate(() -> nextLong(minInclusive, maxExclusive));
+    }
+
+    float nextFloat();
+
+    double nextDouble();
+
+    default double nextDouble(double maxExclusive)
+    {
+        return nextDouble(0, maxExclusive);
+    }
+
+    default double nextDouble(double minInclusive, double maxExclusive)
+    {
+        if (minInclusive >= maxExclusive)
+            throw new IllegalArgumentException(String.format("Min (%s) should 
be less than max (%d).", minInclusive, maxExclusive));
+
+        double result = nextDouble();
+        result = result * (maxExclusive - minInclusive) + minInclusive;
+        if (result >= maxExclusive) // correct for rounding
+            result = 
Double.longBitsToDouble(Double.doubleToLongBits(maxExclusive) - 1);
+        return result;
+    }
+
+    default DoubleStream doubles()
+    {
+        return DoubleStream.generate(this::nextDouble);
+    }
+
+    default DoubleStream doubles(double maxExclusive)
+    {
+        return DoubleStream.generate(() -> nextDouble(maxExclusive));
+    }
+
+    default DoubleStream doubles(double minInclusive, double maxExclusive)
+    {
+        return DoubleStream.generate(() -> nextDouble(minInclusive, 
maxExclusive));
+    }
+
+    double nextGaussian();
+
+    void setSeed(long seed);
+
+    RandomSource fork();
+
+    /**
+     * Returns true with a probability of {@code chance}.  This logic is 
logically the same as
+     * <pre>{@code nextFloat() < chance}</pre>
+     *
+     * @param chance cumulative probability in range [0..1]
+     */
+    default boolean decide(float chance)
+    {
+        return nextFloat() < chance;
+    }
+
+    /**
+     * Returns true with a probability of {@code chance}.  This logic is 
logically the same as
+     * <pre>{@code nextDouble() < chance}</pre>
+     *
+     * @param chance cumulative probability in range [0..1]
+     */
+    default boolean decide(double chance)
+    {
+        return nextDouble() < chance;
+    }
+
+    default long reset()
+    {
+        long seed = nextLong();
+        setSeed(seed);
+        return seed;
+    }
+
+    default Random asJdkRandom()
+    {
+        return new Random()
+        {
+            @Override
+            public void setSeed(long seed)
+            {
+                RandomSource.this.setSeed(seed);
+            }
+
+            @Override
+            public void nextBytes(byte[] bytes)
+            {
+                RandomSource.this.nextBytes(bytes);
+            }
+
+            @Override
+            public int nextInt()
+            {
+                return RandomSource.this.nextInt();
+            }
+
+            @Override
+            public int nextInt(int bound)
+            {
+                return RandomSource.this.nextInt(bound);
+            }
+
+            @Override
+            public long nextLong()
+            {
+                return RandomSource.this.nextLong();
+            }
+
+            @Override
+            public boolean nextBoolean()
+            {
+                return RandomSource.this.nextBoolean();
+            }
+
+            @Override
+            public float nextFloat()
+            {
+                return RandomSource.this.nextFloat();
+            }
+
+            @Override
+            public double nextDouble()
+            {
+                return RandomSource.this.nextDouble();
+            }
+
+            @Override
+            public double nextGaussian()
+            {
+                return RandomSource.this.nextGaussian();
+            }
+        };
+    }
+}
diff --git a/accord-core/src/main/java/accord/utils/RelationMultiMap.java 
b/accord-core/src/main/java/accord/utils/RelationMultiMap.java
index cd3c8fb1..0e6ab273 100644
--- a/accord-core/src/main/java/accord/utils/RelationMultiMap.java
+++ b/accord-core/src/main/java/accord/utils/RelationMultiMap.java
@@ -221,7 +221,7 @@ public class RelationMultiMap
             {
                 sortedKeyIndexes = new int[keyCount];
                 sortedKeys = Arrays.copyOf(keys, keyCount);
-                Arrays.sort(sortedKeys);
+                Arrays.sort(sortedKeys, adapter.keyComparator());
 
                 for (int i = 1 ; i < keyCount ; ++i)
                 {
@@ -230,7 +230,7 @@ public class RelationMultiMap
                                 + Arrays.toString(Arrays.copyOf(keys, 
keyCount)) + ")");
                 }
                 for (int i = 0 ; i < keyCount ; ++i)
-                    sortedKeyIndexes[Arrays.binarySearch(sortedKeys, keys[i])] 
= i;
+                    sortedKeyIndexes[Arrays.binarySearch(sortedKeys, keys[i], 
adapter.keyComparator())] = i;
                 cachedKeys.forceDiscard(keys, keyCount);
             }
 
diff --git a/accord-core/src/main/java/accord/utils/WrappedRandomSource.java 
b/accord-core/src/main/java/accord/utils/WrappedRandomSource.java
new file mode 100644
index 00000000..3d02c101
--- /dev/null
+++ b/accord-core/src/main/java/accord/utils/WrappedRandomSource.java
@@ -0,0 +1,97 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package accord.utils;
+
+import java.util.Random;
+
+class WrappedRandomSource implements RandomSource
+{
+    private final Random random;
+
+    WrappedRandomSource(Random random)
+    {
+        this.random = random;
+    }
+
+    @Override
+    public Random asJdkRandom()
+    {
+        return random;
+    }
+
+    @Override
+    public void nextBytes(byte[] bytes)
+    {
+        random.nextBytes(bytes);
+    }
+
+    @Override
+    public boolean nextBoolean()
+    {
+        return random.nextBoolean();
+    }
+
+    @Override
+    public int nextInt()
+    {
+        return random.nextInt();
+    }
+
+    @Override
+    public int nextInt(int maxExclusive)
+    {
+        return random.nextInt(maxExclusive);
+    }
+
+    @Override
+    public long nextLong()
+    {
+        return random.nextLong();
+    }
+
+    @Override
+    public float nextFloat()
+    {
+        return random.nextFloat();
+    }
+
+    @Override
+    public double nextDouble()
+    {
+        return random.nextDouble();
+    }
+
+    @Override
+    public double nextGaussian()
+    {
+        return random.nextGaussian();
+    }
+
+    @Override
+    public void setSeed(long seed)
+    {
+        random.setSeed(seed);
+    }
+
+    @Override
+    public RandomSource fork()
+    {
+        return new WrappedRandomSource(new Random(nextLong()));
+    }
+}
diff --git a/accord-core/src/test/java/accord/burn/BurnTest.java 
b/accord-core/src/test/java/accord/burn/BurnTest.java
index f144032c..995a0285 100644
--- a/accord-core/src/test/java/accord/burn/BurnTest.java
+++ b/accord-core/src/test/java/accord/burn/BurnTest.java
@@ -23,7 +23,6 @@ import java.util.ArrayList;
 import java.util.Arrays;
 import java.util.Collections;
 import java.util.List;
-import java.util.Random;
 import java.util.Set;
 import java.util.TreeSet;
 import java.util.concurrent.ExecutionException;
@@ -38,6 +37,8 @@ import java.util.function.Consumer;
 import java.util.function.LongSupplier;
 import java.util.function.Predicate;
 
+import accord.utils.DefaultRandom;
+import accord.utils.RandomSource;
 import accord.impl.IntHashKey;
 import accord.impl.basic.Cluster;
 import accord.impl.basic.PropagatingPendingQueue;
@@ -66,7 +67,7 @@ public class BurnTest
 {
     private static final Logger logger = 
LoggerFactory.getLogger(BurnTest.class);
 
-    static List<Packet> generate(Random random, List<Id> clients, List<Id> 
nodes, int keyCount, int operations)
+    static List<Packet> generate(RandomSource random, List<Id> clients, 
List<Id> nodes, int keyCount, int operations)
     {
         List<Key> keys = new ArrayList<>();
         for (int i = 0 ; i < keyCount ; ++i)
@@ -128,17 +129,17 @@ public class BurnTest
         return packets;
     }
 
-    private static Key randomKey(Random random, List<Key> keys, Set<Key> notIn)
+    private static Key randomKey(RandomSource random, List<Key> keys, Set<Key> 
notIn)
     {
         return keys.get(randomKeyIndex(random, keys, notIn));
     }
 
-    private static int randomKeyIndex(Random random, List<Key> keys, Set<Key> 
notIn)
+    private static int randomKeyIndex(RandomSource random, List<Key> keys, 
Set<Key> notIn)
     {
         return randomKeyIndex(random, keys, notIn::contains);
     }
 
-    private static int randomKeyIndex(Random random, List<Key> keys, 
Predicate<Key> notIn)
+    private static int randomKeyIndex(RandomSource random, List<Key> keys, 
Predicate<Key> notIn)
     {
         int i;
         while (notIn.test(keys.get(i = random.nextInt(keys.size()))));
@@ -147,7 +148,7 @@ public class BurnTest
 
     static void burn(TopologyFactory topologyFactory, List<Id> clients, 
List<Id> nodes, int keyCount, int operations, int concurrency) throws 
IOException
     {
-        Random random = new Random();
+        RandomSource random = new DefaultRandom();
         long seed = random.nextLong();
         System.out.println(seed);
         random.setSeed(seed);
@@ -156,7 +157,7 @@ public class BurnTest
 
     static void burn(long seed, TopologyFactory topologyFactory, List<Id> 
clients, List<Id> nodes, int keyCount, int operations, int concurrency) throws 
IOException
     {
-        Random random = new Random();
+        RandomSource random = new DefaultRandom();
         System.out.println(seed);
         random.setSeed(seed);
         burn(random, topologyFactory, clients, nodes, keyCount, operations, 
concurrency);
@@ -166,7 +167,7 @@ public class BurnTest
     {
         ReconcilingLogger logReconciler = new ReconcilingLogger(logger);
 
-        Random random1 = new Random(), random2 = new Random();
+        RandomSource random1 = new DefaultRandom(), random2 = new 
DefaultRandom();
         random1.setSeed(seed);
         random2.setSeed(seed);
         ExecutorService exec = Executors.newFixedThreadPool(2);
@@ -188,7 +189,7 @@ public class BurnTest
         assert logReconciler.reconcile();
     }
 
-    static void burn(Random random, TopologyFactory topologyFactory, List<Id> 
clients, List<Id> nodes, int keyCount, int operations, int concurrency)
+    static void burn(RandomSource random, TopologyFactory topologyFactory, 
List<Id> clients, List<Id> nodes, int keyCount, int operations, int concurrency)
     {
         List<Throwable> failures = Collections.synchronizedList(new 
ArrayList<>());
         PendingQueue queue = new PropagatingPendingQueue(failures, new 
Factory(random).get());
@@ -268,7 +269,7 @@ public class BurnTest
         {
             Cluster.run(toArray(nodes, Id[]::new), () -> queue,
                         responseSink, failures::add,
-                        () -> new Random(random.nextLong()),
+                        () -> random.fork(),
                         () -> new AtomicLong()::incrementAndGet,
                         topologyFactory, () -> null);
         }
@@ -313,7 +314,7 @@ public class BurnTest
                     count = 1;
                     break;
                 case "--loop-seed":
-                    seedGenerator = new Random(Long.parseLong(args[i + 
1]))::nextLong;
+                    seedGenerator = new DefaultRandom(Long.parseLong(args[i + 
1]))::nextLong;
             }
         }
         while (count-- > 0)
@@ -332,7 +333,7 @@ public class BurnTest
     {
         logger.info("Seed: {}", seed);
         Cluster.trace.trace("Seed: {}", seed);
-        Random random = new Random(seed);
+        RandomSource random = new DefaultRandom(seed);
         try
         {
             List<Id> clients = generateIds(true, 1 + random.nextInt(4));
diff --git 
a/accord-core/src/test/java/accord/burn/BurnTestConfigurationService.java 
b/accord-core/src/test/java/accord/burn/BurnTestConfigurationService.java
index d2dd593b..2f86e935 100644
--- a/accord-core/src/test/java/accord/burn/BurnTestConfigurationService.java
+++ b/accord-core/src/test/java/accord/burn/BurnTestConfigurationService.java
@@ -18,9 +18,9 @@
 
 package accord.burn;
 
-import accord.api.ConfigurationService;
 import accord.api.MessageSink;
 import accord.api.TestableConfigurationService;
+import accord.utils.RandomSource;
 import accord.local.Node;
 import accord.messages.*;
 import accord.topology.Topology;
@@ -30,7 +30,10 @@ import accord.utils.async.AsyncResults;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 
-import java.util.*;
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
 import java.util.function.Function;
 import java.util.function.Supplier;
 
@@ -41,11 +44,11 @@ public class BurnTestConfigurationService implements 
TestableConfigurationServic
     private final Node.Id node;
     private final MessageSink messageSink;
     private final Function<Node.Id, Node> lookup;
-    private final Supplier<Random> randomSupplier;
+    private final Supplier<RandomSource> randomSupplier;
     private final Map<Long, FetchTopology> pendingEpochs = new HashMap<>();
 
     private final EpochHistory epochs = new EpochHistory();
-    private final List<ConfigurationService.Listener> listeners = new 
ArrayList<>();
+    private final List<Listener> listeners = new ArrayList<>();
     private final TopologyUpdates topologyUpdates;
 
     private static class EpochState
@@ -125,7 +128,7 @@ public class BurnTestConfigurationService implements 
TestableConfigurationServic
         }
     }
 
-    public BurnTestConfigurationService(Node.Id node, MessageSink messageSink, 
Supplier<Random> randomSupplier, Topology topology, Function<Node.Id, Node> 
lookup, TopologyUpdates topologyUpdates)
+    public BurnTestConfigurationService(Node.Id node, MessageSink messageSink, 
Supplier<RandomSource> randomSupplier, Topology topology, Function<Node.Id, 
Node> lookup, TopologyUpdates topologyUpdates)
     {
         this.node = node;
         this.messageSink = messageSink;
diff --git 
a/accord-core/src/test/java/accord/burn/random/FrequentLargeRange.java 
b/accord-core/src/test/java/accord/burn/random/FrequentLargeRange.java
new file mode 100644
index 00000000..42417980
--- /dev/null
+++ b/accord-core/src/test/java/accord/burn/random/FrequentLargeRange.java
@@ -0,0 +1,76 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package accord.burn.random;
+
+import accord.utils.Invariants;
+import accord.utils.RandomSource;
+
+public class FrequentLargeRange implements RandomLong
+{
+    private final RandomLong small, large;
+    private final double ratio;
+    private final int steps;
+    private final double lower, upper;
+    private int run = -1;
+    private long smallCount = 0, largeCount = 0;
+
+    public FrequentLargeRange(RandomLong small, RandomLong large, double ratio)
+    {
+        Invariants.checkArgument(ratio > 0 && ratio <= 1);
+        this.small = small;
+        this.large = large;
+        this.ratio = ratio;
+        this.steps = (int) (1 / ratio);
+        this.lower = ratio * .8;
+        this.upper = ratio * 1.2;
+    }
+
+    @Override
+    public long getLong(RandomSource randomSource)
+    {
+        if (run != -1)
+        {
+            run--;
+            largeCount++;
+            return large.getLong(randomSource);
+        }
+        double currentRatio = largeCount / (double) (smallCount + largeCount);
+        if (currentRatio < lower)
+        {
+            // not enough large
+            largeCount++;
+            return large.getLong(randomSource);
+        }
+        if (currentRatio > upper)
+        {
+            // not enough small
+            smallCount++;
+            return small.getLong(randomSource);
+        }
+        if (randomSource.nextDouble() < ratio)
+        {
+            run = randomSource.nextInt(steps);
+            run--;
+            largeCount++;
+            return large.getLong(randomSource);
+        }
+        smallCount++;
+        return small.getLong(randomSource);
+    }
+}
diff --git a/accord-maelstrom/build.gradle 
b/accord-core/src/test/java/accord/burn/random/IntRange.java
similarity index 53%
copy from accord-maelstrom/build.gradle
copy to accord-core/src/test/java/accord/burn/random/IntRange.java
index cc08d3f4..5ebc2cd2 100644
--- a/accord-maelstrom/build.gradle
+++ b/accord-core/src/test/java/accord/burn/random/IntRange.java
@@ -16,35 +16,26 @@
  * limitations under the License.
  */
 
-plugins {
-    id 'accord.java-conventions'
-}
+package accord.burn.random;
 
-dependencies {
-    implementation project(':accord-core')
-    implementation group: 'com.google.code.findbugs', name: 'jsr305', version: 
'3.0.2'
-    implementation 'com.google.code.gson:gson:2.8.7'
+import accord.utils.RandomSource;
 
-    testImplementation(testFixtures(project(':accord-core')))
-}
+public class IntRange implements RandomInt
+{
+    public final int min, max;
+    private final int maxDelta;
 
-jar {
-    manifest {
-        attributes(
-                'Main-Class': 'accord.maelstrom.Main',
-        )
+    public IntRange(int min, int max)
+    {
+        if (min >= max) throw new IllegalArgumentException(String.format("Min 
(%s) should be less than max (%d).", min, max));
+        this.min = min;
+        this.max = max;
+        this.maxDelta = max - min + 1;
     }
-}
 
-task fatJar(type: Jar) {
-    manifest.from jar.manifest
-    archiveClassifier = 'all'
-    from {
-        configurations.runtimeClasspath.collect { it.isDirectory() ? it : 
zipTree(it) }
-    } {
-        exclude "META-INF/*.SF"
-        exclude "META-INF/*.DSA"
-        exclude "META-INF/*.RSA"
+    @Override
+    public int getInt(RandomSource randomSource)
+    {
+        return min + randomSource.nextInt(maxDelta);
     }
-    with jar
 }
diff --git a/accord-maelstrom/build.gradle 
b/accord-core/src/test/java/accord/burn/random/RandomInt.java
similarity index 52%
copy from accord-maelstrom/build.gradle
copy to accord-core/src/test/java/accord/burn/random/RandomInt.java
index cc08d3f4..a8fbbd12 100644
--- a/accord-maelstrom/build.gradle
+++ b/accord-core/src/test/java/accord/burn/random/RandomInt.java
@@ -16,35 +16,17 @@
  * limitations under the License.
  */
 
-plugins {
-    id 'accord.java-conventions'
-}
-
-dependencies {
-    implementation project(':accord-core')
-    implementation group: 'com.google.code.findbugs', name: 'jsr305', version: 
'3.0.2'
-    implementation 'com.google.code.gson:gson:2.8.7'
+package accord.burn.random;
 
-    testImplementation(testFixtures(project(':accord-core')))
-}
+import accord.utils.RandomSource;
 
-jar {
-    manifest {
-        attributes(
-                'Main-Class': 'accord.maelstrom.Main',
-        )
-    }
-}
+public interface RandomInt extends RandomLong
+{
+    int getInt(RandomSource randomSource);
 
-task fatJar(type: Jar) {
-    manifest.from jar.manifest
-    archiveClassifier = 'all'
-    from {
-        configurations.runtimeClasspath.collect { it.isDirectory() ? it : 
zipTree(it) }
-    } {
-        exclude "META-INF/*.SF"
-        exclude "META-INF/*.DSA"
-        exclude "META-INF/*.RSA"
+    @Override
+    default long getLong(RandomSource randomSource)
+    {
+        return getInt(randomSource);
     }
-    with jar
 }
diff --git a/accord-maelstrom/build.gradle 
b/accord-core/src/test/java/accord/burn/random/RandomLong.java
similarity index 52%
copy from accord-maelstrom/build.gradle
copy to accord-core/src/test/java/accord/burn/random/RandomLong.java
index cc08d3f4..1bc8c482 100644
--- a/accord-maelstrom/build.gradle
+++ b/accord-core/src/test/java/accord/burn/random/RandomLong.java
@@ -16,35 +16,11 @@
  * limitations under the License.
  */
 
-plugins {
-    id 'accord.java-conventions'
-}
-
-dependencies {
-    implementation project(':accord-core')
-    implementation group: 'com.google.code.findbugs', name: 'jsr305', version: 
'3.0.2'
-    implementation 'com.google.code.gson:gson:2.8.7'
+package accord.burn.random;
 
-    testImplementation(testFixtures(project(':accord-core')))
-}
-
-jar {
-    manifest {
-        attributes(
-                'Main-Class': 'accord.maelstrom.Main',
-        )
-    }
-}
+import accord.utils.RandomSource;
 
-task fatJar(type: Jar) {
-    manifest.from jar.manifest
-    archiveClassifier = 'all'
-    from {
-        configurations.runtimeClasspath.collect { it.isDirectory() ? it : 
zipTree(it) }
-    } {
-        exclude "META-INF/*.SF"
-        exclude "META-INF/*.DSA"
-        exclude "META-INF/*.RSA"
-    }
-    with jar
+public interface RandomLong
+{
+    long getLong(RandomSource randomSource);
 }
diff --git a/accord-core/src/test/java/accord/burn/random/RandomRangeTest.java 
b/accord-core/src/test/java/accord/burn/random/RandomRangeTest.java
new file mode 100644
index 00000000..68c134a8
--- /dev/null
+++ b/accord-core/src/test/java/accord/burn/random/RandomRangeTest.java
@@ -0,0 +1,83 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package accord.burn.random;
+
+import accord.utils.Gen;
+import accord.utils.Gens;
+import accord.utils.RandomSource;
+import org.assertj.core.api.Assertions;
+import org.junit.jupiter.api.Test;
+
+import java.util.Arrays;
+
+import static accord.utils.Property.qt;
+
+class RandomRangeTest
+{
+    @Test
+    void intRange()
+    {
+        test((ignore, min, max) -> new IntRange(min, max));
+    }
+
+    @Test
+    void randomWalkRange()
+    {
+        test(RandomWalkRange::new);
+    }
+
+    void test(Factory factory)
+    {
+        int samples = 1000;
+        qt().forAll(Gens.random(), ranges()).check((rs, range) -> {
+            RandomLong randRange = factory.create(rs, range.min, range.max);
+            for (int i = 0; i < samples; i++)
+                Assertions.assertThat(randRange.getLong(rs)).isBetween((long) 
range.min, (long) range.max);
+        });
+    }
+
+    private static Gen<Range> ranges()
+    {
+        Range range = new Range();
+        Gen<Integer> numGen = Gens.ints().between(0, 1000);
+        int[] buffer = new int[2];
+        return rs -> {
+            buffer[0] = numGen.next(rs);
+            do
+            {
+                buffer[1] = numGen.next(rs);
+            }
+            while (buffer[0] == buffer[1]);
+            Arrays.sort(buffer);
+            range.min = buffer[0];
+            range.max = buffer[1];
+            return range;
+        };
+    }
+
+    private static class Range
+    {
+        int min, max;
+    }
+
+    private interface Factory
+    {
+        RandomLong create(RandomSource random, int min, int max);
+    }
+}
\ No newline at end of file
diff --git a/accord-core/src/test/java/accord/burn/random/RandomWalkRange.java 
b/accord-core/src/test/java/accord/burn/random/RandomWalkRange.java
new file mode 100644
index 00000000..c618cf7e
--- /dev/null
+++ b/accord-core/src/test/java/accord/burn/random/RandomWalkRange.java
@@ -0,0 +1,61 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package accord.burn.random;
+
+import accord.utils.RandomSource;
+
+public class RandomWalkRange implements RandomLong
+{
+    public final int min, max;
+    private final int maxStepSize;
+    long cur;
+
+    public RandomWalkRange(RandomSource random, int min, int max)
+    {
+        this.min = min;
+        this.max = max;
+        this.maxStepSize = maxStepSize(random, min, max);
+        this.cur = random.nextLong(min, max + 1);
+    }
+
+    @Override
+    public long getLong(RandomSource randomSource)
+    {
+        long step = randomSource.nextLong(-maxStepSize, maxStepSize + 1);
+        long cur = this.cur;
+        this.cur = step > 0 ? Math.min(max, cur + step)
+                            : Math.max(min, cur + step);
+        return cur;
+    }
+
+    private static int maxStepSize(RandomSource random, int min, int max)
+    {
+        switch (random.nextInt(3))
+        {
+            case 0:
+                return Math.max(1, (max/32) - (min/32));
+            case 1:
+                return Math.max(1, (max/256) - (min/256));
+            case 2:
+                return Math.max(1, (max/2048) - (min/2048));
+            default:
+                return Math.max(1, (max/16384) - (min/16384));
+        }
+    }
+}
diff --git 
a/accord-core/src/test/java/accord/burn/random/SegmentedRandomRangeTest.java 
b/accord-core/src/test/java/accord/burn/random/SegmentedRandomRangeTest.java
new file mode 100644
index 00000000..caddc241
--- /dev/null
+++ b/accord-core/src/test/java/accord/burn/random/SegmentedRandomRangeTest.java
@@ -0,0 +1,182 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package accord.burn.random;
+
+import accord.utils.Gen;
+import accord.utils.Gens;
+import accord.utils.RandomSource;
+import org.agrona.collections.Long2LongHashMap;
+import org.assertj.core.api.Assertions;
+import org.junit.jupiter.api.Test;
+
+import java.util.Arrays;
+import java.util.stream.LongStream;
+
+import static accord.utils.Property.qt;
+
+class SegmentedRandomRangeTest
+{
+    enum Type
+    {
+        IntRange,
+        RandomWalkRange
+    }
+    
+    private static final class TestCase
+    {
+        private final int minSmall, maxSmall, minLarge, maxLarge, largeRatio;
+        private final Type type;
+
+        private TestCase(int minSmall, int maxSmall, int minLarge, int 
maxLarge, int largeRatio, Type type)
+        {
+            this.minSmall = minSmall;
+            this.maxSmall = maxSmall;
+            this.minLarge = minLarge;
+            this.maxLarge = maxLarge;
+            this.largeRatio = largeRatio;
+            this.type = type;
+        }
+
+        RandomLong min(RandomSource random)
+        {
+            return create(random, minSmall, maxSmall);
+        }
+
+        RandomLong max(RandomSource random)
+        {
+            return create(random, minLarge, maxLarge);
+        }
+
+        private RandomLong create(RandomSource random, int min, int max)
+        {
+            switch (type)
+            {
+                case IntRange: return new IntRange(min, max);
+                case RandomWalkRange: return new RandomWalkRange(random, min, 
max);
+                default: throw new UnsupportedOperationException("type " + 
type);
+            }
+        }
+
+        double ratio()
+        {
+            return 1 / (double) largeRatio;
+        }
+
+        @Override
+        public String toString()
+        {
+            return "TestCase{" +
+                    "minSmall=" + minSmall +
+                    ", maxSmall=" + maxSmall +
+                    ", minLarge=" + minLarge +
+                    ", maxLarge=" + maxLarge +
+                    ", largeRatio=" + largeRatio +
+                    ", type=" + type +
+                    '}';
+        }
+    }
+
+    @Test
+    void disjoint()
+    {
+        Gen<Integer> range = Gens.ints().between(100, 100000000);
+        Gen<Integer> ratio = Gens.ints().between(6, 20);
+        Gen<Type> typeGen = Gens.enums().all(Type.class);
+        int[] ints = new int[3];
+        Gen<TestCase> test = rs -> {
+            ints[0] = range.next(rs);
+            ints[1] = range.next(rs);
+            ints[2] = range.next(rs);
+            Arrays.sort(ints);
+            int largeRatio = ratio.next(rs);
+            return new TestCase(0, ints[0], ints[1], ints[2], largeRatio, 
typeGen.next(rs));
+        };
+        qt().forAll(Gens.random(), test).check(SegmentedRandomRangeTest::test);
+    }
+
+    private static void test(RandomSource rs, TestCase tc)
+    {
+        double ratio = tc.ratio();
+        FrequentLargeRange period = new FrequentLargeRange(tc.min(rs), 
tc.max(rs), ratio);
+        int numSamples = 1000;
+        int maxResamples = 1000;
+
+        double target = ratio * 100.0D;
+        double upperBounds = target * 1.2;
+        double lowerBounds = target * .8;
+
+        int largeSeq = 0;
+        int largeCount = 0;
+        Long2LongHashMap largeSeqCounts = new Long2LongHashMap(0);
+        int resamples = 0;
+        for (int i = 0; i < numSamples; i++)
+        {
+            long size = period.getLong(rs);
+            if (size > tc.maxSmall)
+            {
+                largeCount++;
+                largeSeq++;
+            }
+            else
+            {
+                largeSeqCounts.compute(largeSeq, (ignore, accm) -> accm + 1);
+                largeSeq = 0;
+            }
+            if (i == numSamples - 1)
+            {
+                // keep going if ratio is off...
+                double actual = (double) largeCount / numSamples * 100.0;
+                if (actual < lowerBounds || actual  > upperBounds)
+                {
+                    if (resamples == maxResamples)
+                        throw new AssertionError(String.format("Unable to 
match target rate in %d re-samples; actual=%f, target=%f, bounds=(%f, %f)", 
resamples, actual, target, lowerBounds, upperBounds));
+                    numSamples += 100;
+                    resamples++;
+                }
+            }
+        }
+
+        checkSequences(largeSeqCounts);
+        assertRatio(numSamples, upperBounds, lowerBounds, largeCount);
+    }
+
+    private static void checkSequences(Long2LongHashMap largeSeqCounts)
+    {
+        long[] keys = new long[largeSeqCounts.size()];
+        Long2LongHashMap.KeyIterator it = largeSeqCounts.keySet().iterator();
+        int idx = 0;
+        while (it.hasNext())
+            keys[idx++] = it.nextValue();
+        if (LongStream.of(keys).anyMatch(seq -> seq > 5))
+            return;
+        StringBuilder sb = new StringBuilder("No large sequences detected; 
saw\n");
+        Arrays.sort(keys);
+        for (long key : keys)
+            
sb.append('\t').append(key).append('\t').append(largeSeqCounts.get(key)).append('\n');
+        throw new AssertionError(sb.toString());
+    }
+
+    private static void assertRatio(int numSamples, double upperBounds, double 
lowerBounds, long largeCount)
+    {
+        double largePercent = largeCount / (double) numSamples * 100.0;
+        Assertions.assertThat(largePercent)
+                .describedAs("Expected ratio was not respected")
+                .isBetween(lowerBounds, upperBounds);
+    }
+}
\ No newline at end of file
diff --git 
a/accord-core/src/test/java/accord/coordinate/tracking/FastPathTrackerReconciler.java
 
b/accord-core/src/test/java/accord/coordinate/tracking/FastPathTrackerReconciler.java
index 59c811c6..b6947225 100644
--- 
a/accord-core/src/test/java/accord/coordinate/tracking/FastPathTrackerReconciler.java
+++ 
b/accord-core/src/test/java/accord/coordinate/tracking/FastPathTrackerReconciler.java
@@ -18,25 +18,24 @@
 
 package accord.coordinate.tracking;
 
+import accord.utils.RandomSource;
 import accord.coordinate.tracking.FastPathTracker.FastPathShardTracker;
-import accord.coordinate.tracking.QuorumTracker.QuorumShardTracker;
 import accord.local.Node;
 import accord.topology.Topologies;
 import org.junit.jupiter.api.Assertions;
 
 import java.util.ArrayList;
-import java.util.Random;
 
 public class FastPathTrackerReconciler extends 
TrackerReconciler<FastPathShardTracker, FastPathTracker, 
FastPathTrackerReconciler.Rsp>
 {
     enum Rsp { FAST, SLOW, FAIL }
 
-    FastPathTrackerReconciler(Random random, Topologies topologies)
+    FastPathTrackerReconciler(RandomSource random, Topologies topologies)
     {
         this(random, new FastPathTracker(topologies));
     }
 
-    private FastPathTrackerReconciler(Random random, FastPathTracker tracker)
+    private FastPathTrackerReconciler(RandomSource random, FastPathTracker 
tracker)
     {
         super(random, Rsp.class, tracker, new ArrayList<>(tracker.nodes()));
     }
diff --git 
a/accord-core/src/test/java/accord/coordinate/tracking/InvalidationTrackerReconciler.java
 
b/accord-core/src/test/java/accord/coordinate/tracking/InvalidationTrackerReconciler.java
index 35f0a91e..34202a5f 100644
--- 
a/accord-core/src/test/java/accord/coordinate/tracking/InvalidationTrackerReconciler.java
+++ 
b/accord-core/src/test/java/accord/coordinate/tracking/InvalidationTrackerReconciler.java
@@ -18,24 +18,24 @@
 
 package accord.coordinate.tracking;
 
+import accord.utils.RandomSource;
 import accord.coordinate.tracking.InvalidationTracker.InvalidationShardTracker;
 import accord.local.Node;
 import accord.topology.Topologies;
 import org.junit.jupiter.api.Assertions;
 
 import java.util.ArrayList;
-import java.util.Random;
 
 public class InvalidationTrackerReconciler extends 
TrackerReconciler<InvalidationShardTracker, InvalidationTracker, 
InvalidationTrackerReconciler.Rsp>
 {
     enum Rsp { PROMISED_FAST, NOT_PROMISED_FAST, PROMISED_SLOW, 
NOT_PROMISED_SLOW, FAIL }
 
-    InvalidationTrackerReconciler(Random random, Topologies topologies)
+    InvalidationTrackerReconciler(RandomSource random, Topologies topologies)
     {
         this(random, new InvalidationTracker(topologies));
     }
 
-    private InvalidationTrackerReconciler(Random random, InvalidationTracker 
tracker)
+    private InvalidationTrackerReconciler(RandomSource random, 
InvalidationTracker tracker)
     {
         super(random, Rsp.class, tracker, new ArrayList<>(tracker.nodes()));
     }
diff --git 
a/accord-core/src/test/java/accord/coordinate/tracking/QuorumTrackerReconciler.java
 
b/accord-core/src/test/java/accord/coordinate/tracking/QuorumTrackerReconciler.java
index cc3046df..0d705501 100644
--- 
a/accord-core/src/test/java/accord/coordinate/tracking/QuorumTrackerReconciler.java
+++ 
b/accord-core/src/test/java/accord/coordinate/tracking/QuorumTrackerReconciler.java
@@ -18,24 +18,24 @@
 
 package accord.coordinate.tracking;
 
+import accord.utils.RandomSource;
 import accord.coordinate.tracking.QuorumTracker.QuorumShardTracker;
 import accord.local.Node;
 import accord.topology.Topologies;
 import org.junit.jupiter.api.Assertions;
 
 import java.util.ArrayList;
-import java.util.Random;
 
 public class QuorumTrackerReconciler extends 
TrackerReconciler<QuorumShardTracker, QuorumTracker, 
QuorumTrackerReconciler.Rsp>
 {
     enum Rsp { QUORUM, FAIL }
 
-    QuorumTrackerReconciler(Random random, Topologies topologies)
+    QuorumTrackerReconciler(RandomSource random, Topologies topologies)
     {
         this(random, new QuorumTracker(topologies));
     }
 
-    private QuorumTrackerReconciler(Random random, QuorumTracker tracker)
+    private QuorumTrackerReconciler(RandomSource random, QuorumTracker tracker)
     {
         super(random, Rsp.class, tracker, new ArrayList<>(tracker.nodes()));
     }
diff --git 
a/accord-core/src/test/java/accord/coordinate/tracking/ReadTrackerReconciler.java
 
b/accord-core/src/test/java/accord/coordinate/tracking/ReadTrackerReconciler.java
index 0c71d70e..c912c95b 100644
--- 
a/accord-core/src/test/java/accord/coordinate/tracking/ReadTrackerReconciler.java
+++ 
b/accord-core/src/test/java/accord/coordinate/tracking/ReadTrackerReconciler.java
@@ -18,6 +18,7 @@
 
 package accord.coordinate.tracking;
 
+import accord.utils.RandomSource;
 import accord.coordinate.tracking.ReadTracker.ReadShardTracker;
 import accord.local.Node;
 import accord.topology.Topologies;
@@ -25,7 +26,6 @@ import org.junit.jupiter.api.Assertions;
 
 import java.util.ArrayList;
 import java.util.List;
-import java.util.Random;
 
 public class ReadTrackerReconciler extends TrackerReconciler<ReadShardTracker, 
ReadTracker, ReadTrackerReconciler.Rsp>
 {
@@ -46,12 +46,12 @@ public class ReadTrackerReconciler extends 
TrackerReconciler<ReadShardTracker, R
         }
     }
 
-    ReadTrackerReconciler(Random random, Topologies topologies)
+    ReadTrackerReconciler(RandomSource random, Topologies topologies)
     {
         this(random, new InFlightCapturingReadTracker(topologies));
     }
 
-    private ReadTrackerReconciler(Random random, InFlightCapturingReadTracker 
tracker)
+    private ReadTrackerReconciler(RandomSource random, 
InFlightCapturingReadTracker tracker)
     {
         super(random, Rsp.class, tracker, tracker.inflight);
     }
diff --git 
a/accord-core/src/test/java/accord/coordinate/tracking/RecoveryTrackerReconciler.java
 
b/accord-core/src/test/java/accord/coordinate/tracking/RecoveryTrackerReconciler.java
index 4519fa63..5e2a2a7c 100644
--- 
a/accord-core/src/test/java/accord/coordinate/tracking/RecoveryTrackerReconciler.java
+++ 
b/accord-core/src/test/java/accord/coordinate/tracking/RecoveryTrackerReconciler.java
@@ -18,25 +18,25 @@
 
 package accord.coordinate.tracking;
 
+import accord.utils.RandomSource;
 import accord.coordinate.tracking.RecoveryTracker.RecoveryShardTracker;
 import accord.local.Node;
 import accord.topology.Topologies;
 import org.junit.jupiter.api.Assertions;
 
 import java.util.ArrayList;
-import java.util.Random;
 
 // TODO (required, testing): check fast path accounting
 public class RecoveryTrackerReconciler extends 
TrackerReconciler<RecoveryShardTracker, RecoveryTracker, 
RecoveryTrackerReconciler.Rsp>
 {
     enum Rsp { FAST, SLOW, FAIL }
 
-    RecoveryTrackerReconciler(Random random, Topologies topologies)
+    RecoveryTrackerReconciler(RandomSource random, Topologies topologies)
     {
         this(random, new RecoveryTracker(topologies));
     }
 
-    private RecoveryTrackerReconciler(Random random, RecoveryTracker tracker)
+    private RecoveryTrackerReconciler(RandomSource random, RecoveryTracker 
tracker)
     {
         super(random, Rsp.class, tracker, new ArrayList<>(tracker.nodes()));
     }
diff --git 
a/accord-core/src/test/java/accord/coordinate/tracking/TrackerReconciler.java 
b/accord-core/src/test/java/accord/coordinate/tracking/TrackerReconciler.java
index a265633c..3736dd8f 100644
--- 
a/accord-core/src/test/java/accord/coordinate/tracking/TrackerReconciler.java
+++ 
b/accord-core/src/test/java/accord/coordinate/tracking/TrackerReconciler.java
@@ -19,6 +19,8 @@
 package accord.coordinate.tracking;
 
 import accord.burn.TopologyUpdates;
+import accord.utils.DefaultRandom;
+import accord.utils.RandomSource;
 import accord.impl.IntHashKey;
 import accord.impl.SizeOfIntersectionSorter;
 import accord.impl.TopologyFactory;
@@ -28,20 +30,24 @@ import accord.topology.Topology;
 import accord.topology.TopologyRandomizer;
 import org.junit.jupiter.api.Assertions;
 
-import java.util.*;
+import java.util.ArrayDeque;
+import java.util.ArrayList;
+import java.util.Deque;
+import java.util.EnumMap;
+import java.util.List;
 import java.util.function.BiFunction;
 import java.util.stream.Collectors;
 import java.util.stream.Stream;
 
 public abstract class TrackerReconciler<ST extends ShardTracker, T extends 
AbstractTracker<ST, ?>, E extends Enum<E>>
 {
-    final Random random;
+    final RandomSource random;
     final E[] events;
     final EnumMap<E, Integer>[] counts;
     final T tracker;
     final List<Id> inflight;
 
-    protected TrackerReconciler(Random random, Class<E> events, T tracker, 
List<Id> inflight)
+    protected TrackerReconciler(RandomSource random, Class<E> events, T 
tracker, List<Id> inflight)
     {
         this.random = random;
         this.events = events.getEnumConstants();
@@ -86,17 +92,17 @@ public abstract class TrackerReconciler<ST extends 
ShardTracker, T extends Abstr
     abstract void validate(RequestStatus status);
 
     protected static <ST extends ShardTracker, T extends AbstractTracker<ST, 
?>, E extends Enum<E>>
-    List<TrackerReconciler<ST, T, E>> generate(long seed, BiFunction<Random, 
Topologies, ? extends TrackerReconciler<ST, T, E>> constructor)
+    List<TrackerReconciler<ST, T, E>> generate(long seed, 
BiFunction<RandomSource, Topologies, ? extends TrackerReconciler<ST, T, E>> 
constructor)
     {
         System.out.println("seed: " + seed);
-        Random random = new Random(seed);
+        RandomSource random = new DefaultRandom(seed);
         return topologies(random).map(topologies -> constructor.apply(random, 
topologies))
                 .collect(Collectors.toList());
     }
 
     // TODO (required, testing): generalise and parameterise topology 
generation a bit more
     //                           also, select a subset of the generated 
topologies to correctly simulate topology consumption logic
-    private static Stream<Topologies> topologies(Random random)
+    private static Stream<Topologies> topologies(RandomSource random)
     {
         TopologyFactory factory = new TopologyFactory(2 + random.nextInt(3), 
IntHashKey.ranges(4 + random.nextInt(12)));
         List<Id> nodes = cluster(factory.rf * (1 + 
random.nextInt(factory.shardRanges.length - 1)));
diff --git 
a/accord-core/src/test/java/accord/coordinate/tracking/TrackerReconcilerTest.java
 
b/accord-core/src/test/java/accord/coordinate/tracking/TrackerReconcilerTest.java
index 22b7425f..d10a48ba 100644
--- 
a/accord-core/src/test/java/accord/coordinate/tracking/TrackerReconcilerTest.java
+++ 
b/accord-core/src/test/java/accord/coordinate/tracking/TrackerReconcilerTest.java
@@ -18,10 +18,10 @@
 
 package accord.coordinate.tracking;
 
+import accord.utils.RandomSource;
 import accord.topology.Topologies;
 import org.junit.jupiter.api.Test;
 
-import java.util.Random;
 import java.util.function.BiFunction;
 
 public class TrackerReconcilerTest
@@ -57,7 +57,7 @@ public class TrackerReconcilerTest
     }
 
     static <ST extends ShardTracker, T extends AbstractTracker<ST, ?>, E 
extends Enum<E>>
-    void test(int count, BiFunction<Random, Topologies, ? extends 
TrackerReconciler<ST, T, E>> constructor)
+    void test(int count, BiFunction<RandomSource, Topologies, ? extends 
TrackerReconciler<ST, T, E>> constructor)
     {
         long seed = System.currentTimeMillis();
         while (--count >= 0)
@@ -65,7 +65,7 @@ public class TrackerReconcilerTest
     }
 
     static <ST extends ShardTracker, T extends AbstractTracker<ST, ?>, E 
extends Enum<E>>
-    void test(long seed, BiFunction<Random, Topologies, ? extends 
TrackerReconciler<ST, T, E>> constructor)
+    void test(long seed, BiFunction<RandomSource, Topologies, ? extends 
TrackerReconciler<ST, T, E>> constructor)
     {
         for (TrackerReconciler<?, ?, ?> test : 
TrackerReconciler.generate(seed, constructor))
             test.test();
diff --git a/accord-core/src/test/java/accord/impl/basic/Cluster.java 
b/accord-core/src/test/java/accord/impl/basic/Cluster.java
index 6ec6bf14..28983033 100644
--- a/accord-core/src/test/java/accord/impl/basic/Cluster.java
+++ b/accord-core/src/test/java/accord/impl/basic/Cluster.java
@@ -27,7 +27,6 @@ import java.util.LinkedHashMap;
 import java.util.LinkedHashSet;
 import java.util.List;
 import java.util.Map;
-import java.util.Random;
 import java.util.Set;
 import java.util.concurrent.TimeUnit;
 import java.util.function.Consumer;
@@ -50,6 +49,7 @@ import accord.messages.Reply;
 import accord.messages.Request;
 import accord.topology.TopologyRandomizer;
 import accord.topology.Topology;
+import accord.utils.RandomSource;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 
@@ -76,7 +76,7 @@ public class Cluster implements Scheduler
         this.partitionSet = new HashSet<>();
     }
 
-    NodeSink create(Id self, Random random)
+    NodeSink create(Id self, RandomSource random)
     {
         NodeSink sink = new NodeSink(self, lookup, this, random);
         sinks.put(self, sink);
@@ -206,7 +206,7 @@ public class Cluster implements Scheduler
         run.run();
     }
 
-    public static void run(Id[] nodes, Supplier<PendingQueue> queueSupplier, 
Consumer<Packet> responseSink, Consumer<Throwable> onFailure, Supplier<Random> 
randomSupplier, Supplier<LongSupplier> nowSupplier, TopologyFactory 
topologyFactory, Supplier<Packet> in)
+    public static void run(Id[] nodes, Supplier<PendingQueue> queueSupplier, 
Consumer<Packet> responseSink, Consumer<Throwable> onFailure, 
Supplier<RandomSource> randomSupplier, Supplier<LongSupplier> nowSupplier, 
TopologyFactory topologyFactory, Supplier<Packet> in)
     {
         TopologyUpdates topologyUpdates = new TopologyUpdates();
         Topology topology = topologyFactory.toTopology(nodes);
@@ -227,9 +227,9 @@ public class Cluster implements Scheduler
             }
 
             List<Id> nodesList = new ArrayList<>(Arrays.asList(nodes));
-            Random shuffleRandom = randomSupplier.get();
+            RandomSource shuffleRandom = randomSupplier.get();
             Scheduled chaos = sinks.recurring(() -> {
-                Collections.shuffle(nodesList, shuffleRandom);
+                Collections.shuffle(nodesList, shuffleRandom.asJdkRandom());
                 int partitionSize = 
shuffleRandom.nextInt((topologyFactory.rf+1)/2);
                 sinks.partitionSet = new LinkedHashSet<>(nodesList.subList(0, 
partitionSize));
             }, 5L, SECONDS);
diff --git a/accord-core/src/test/java/accord/impl/basic/NodeSink.java 
b/accord-core/src/test/java/accord/impl/basic/NodeSink.java
index 006fa13d..bad4ed09 100644
--- a/accord-core/src/test/java/accord/impl/basic/NodeSink.java
+++ b/accord-core/src/test/java/accord/impl/basic/NodeSink.java
@@ -20,10 +20,10 @@ package accord.impl.basic;
 
 import java.util.LinkedHashMap;
 import java.util.Map;
-import java.util.Random;
 import java.util.concurrent.TimeUnit;
 import java.util.function.Function;
 
+import accord.utils.RandomSource;
 import accord.coordinate.Timeout;
 import accord.local.Node;
 import accord.local.Node.Id;
@@ -40,12 +40,12 @@ public class NodeSink implements MessageSink
     final Id self;
     final Function<Id, Node> lookup;
     final Cluster parent;
-    final Random random;
+    final RandomSource random;
 
     int nextMessageId = 0;
     Map<Long, Callback> callbacks = new LinkedHashMap<>();
 
-    public NodeSink(Id self, Function<Id, Node> lookup, Cluster parent, Random 
random)
+    public NodeSink(Id self, Function<Id, Node> lookup, Cluster parent, 
RandomSource random)
     {
         this.self = self;
         this.lookup = lookup;
diff --git a/accord-core/src/test/java/accord/impl/basic/RandomDelayQueue.java 
b/accord-core/src/test/java/accord/impl/basic/RandomDelayQueue.java
index d7131e2c..7c95f418 100644
--- a/accord-core/src/test/java/accord/impl/basic/RandomDelayQueue.java
+++ b/accord-core/src/test/java/accord/impl/basic/RandomDelayQueue.java
@@ -18,8 +18,9 @@
 
 package accord.impl.basic;
 
+import accord.utils.RandomSource;
+
 import java.util.PriorityQueue;
-import java.util.Random;
 import java.util.concurrent.TimeUnit;
 import java.util.function.Supplier;
 
@@ -27,9 +28,9 @@ public class RandomDelayQueue<T> implements PendingQueue
 {
     public static class Factory implements Supplier<PendingQueue>
     {
-        final Random seeds;
+        final RandomSource seeds;
 
-        public Factory(Random seeds)
+        public Factory(RandomSource seeds)
         {
             this.seeds = seeds;
         }
@@ -37,7 +38,7 @@ public class RandomDelayQueue<T> implements PendingQueue
         @Override
         public PendingQueue get()
         {
-            return new RandomDelayQueue<>(new Random(seeds.nextLong()));
+            return new RandomDelayQueue<>(seeds.fork());
         }
     }
 
@@ -70,11 +71,11 @@ public class RandomDelayQueue<T> implements PendingQueue
     }
 
     final PriorityQueue<Item> queue = new PriorityQueue<>();
-    final Random random;
+    final RandomSource random;
     long now;
     int seq;
 
-    RandomDelayQueue(Random random)
+    RandomDelayQueue(RandomSource random)
     {
         this.random = random;
     }
diff --git 
a/accord-core/src/test/java/accord/impl/basic/UniformRandomQueue.java 
b/accord-core/src/test/java/accord/impl/basic/UniformRandomQueue.java
index 309e1938..45d55c30 100644
--- a/accord-core/src/test/java/accord/impl/basic/UniformRandomQueue.java
+++ b/accord-core/src/test/java/accord/impl/basic/UniformRandomQueue.java
@@ -18,8 +18,9 @@
 
 package accord.impl.basic;
 
+import accord.utils.RandomSource;
+
 import java.util.PriorityQueue;
-import java.util.Random;
 import java.util.concurrent.TimeUnit;
 import java.util.function.Supplier;
 
@@ -27,9 +28,9 @@ public class UniformRandomQueue<T> implements PendingQueue
 {
     public static class Factory implements Supplier<PendingQueue>
     {
-        final Random seeds;
+        final RandomSource seeds;
 
-        public Factory(Random seeds)
+        public Factory(RandomSource seeds)
         {
             this.seeds = seeds;
         }
@@ -37,7 +38,7 @@ public class UniformRandomQueue<T> implements PendingQueue
         @Override
         public PendingQueue get()
         {
-            return new UniformRandomQueue<>(new Random(seeds.nextLong()));
+            return new UniformRandomQueue<>(seeds.fork());
         }
     }
 
@@ -60,9 +61,9 @@ public class UniformRandomQueue<T> implements PendingQueue
     }
 
     final PriorityQueue<Item> queue = new PriorityQueue<>();
-    final Random random;
+    final RandomSource random;
 
-    public UniformRandomQueue(Random random)
+    public UniformRandomQueue(RandomSource random)
     {
         this.random = random;
     }
diff --git a/accord-core/src/test/java/accord/impl/mock/MockCluster.java 
b/accord-core/src/test/java/accord/impl/mock/MockCluster.java
index 90613ff4..2a888642 100644
--- a/accord-core/src/test/java/accord/impl/mock/MockCluster.java
+++ b/accord-core/src/test/java/accord/impl/mock/MockCluster.java
@@ -19,7 +19,6 @@
 package accord.impl.mock;
 
 import accord.NetworkFilter;
-import accord.api.Key;
 import accord.api.MessageSink;
 import accord.coordinate.Timeout;
 import accord.impl.*;
@@ -27,7 +26,9 @@ import accord.local.Node;
 import accord.local.Node.Id;
 import accord.local.ShardDistributor;
 import accord.primitives.Ranges;
+import accord.utils.DefaultRandom;
 import accord.utils.EpochFunction;
+import accord.utils.RandomSource;
 import accord.utils.ThreadPoolScheduler;
 import accord.primitives.TxnId;
 import accord.messages.Callback;
@@ -53,7 +54,7 @@ public class MockCluster implements Network, AutoCloseable, 
Iterable<Node>
 {
     private static final Logger logger = 
LoggerFactory.getLogger(MockCluster.class);
 
-    private final Random random;
+    private final RandomSource random;
     private final Config config;
     private final LongSupplier nowSupplier;
     private final Map<Id, Node> nodes = new ConcurrentHashMap<>();
@@ -68,7 +69,7 @@ public class MockCluster implements Network, AutoCloseable, 
Iterable<Node>
     private MockCluster(Builder builder)
     {
         this.config = new Config(builder);
-        this.random = new Random(config.seed);
+        this.random = new DefaultRandom(config.seed);
         this.nowSupplier = builder.nowSupplier;
         this.messageSinkFactory = builder.messageSinkFactory;
         this.onFetchTopology = builder.onFetchTopology;
@@ -110,7 +111,7 @@ public class MockCluster implements Network, AutoCloseable, 
Iterable<Node>
                         () -> store,
                         new ShardDistributor.EvenSplit(8, ignore -> new 
IntKey.Splitter()),
                         new TestAgent(),
-                        new Random(random.nextLong()),
+                        random.fork(),
                         new ThreadPoolScheduler(),
                         SizeOfIntersectionSorter.SUPPLIER,
                         SimpleProgressLog::new,
diff --git a/accord-core/src/test/java/accord/local/ImmutableCommandTest.java 
b/accord-core/src/test/java/accord/local/ImmutableCommandTest.java
index e8f965de..be0df4d3 100644
--- a/accord-core/src/test/java/accord/local/ImmutableCommandTest.java
+++ b/accord-core/src/test/java/accord/local/ImmutableCommandTest.java
@@ -30,12 +30,13 @@ import accord.local.Status.Known;
 import accord.primitives.*;
 import accord.topology.Topology;
 import com.google.common.collect.Lists;
+import accord.utils.DefaultRandom;
+
 import org.junit.jupiter.api.Assertions;
 import org.junit.jupiter.api.Test;
 
 import javax.annotation.Nullable;
 import java.util.List;
-import java.util.Random;
 import java.util.Set;
 import java.util.concurrent.ExecutionException;
 import java.util.concurrent.atomic.AtomicReference;
@@ -95,7 +96,7 @@ public class ImmutableCommandTest
     private static Node createNode(Id id, CommandStoreSupport storeSupport)
     {
         return new Node(id, null, new MockConfigurationService(null, (epoch, 
service) -> { }, storeSupport.local.get()),
-                        new MockCluster.Clock(100), () -> storeSupport.data, 
new ShardDistributor.EvenSplit(8, ignore -> new IntKey.Splitter()), new 
TestAgent(), new Random(), null,
+                        new MockCluster.Clock(100), () -> storeSupport.data, 
new ShardDistributor.EvenSplit(8, ignore -> new IntKey.Splitter()), new 
TestAgent(), new DefaultRandom(), null,
                         SizeOfIntersectionSorter.SUPPLIER, ignore -> ignore2 
-> new NoOpProgressLog(), InMemoryCommandStores.Synchronized::new);
     }
 
diff --git a/accord-core/src/test/java/accord/messages/PreAcceptTest.java 
b/accord-core/src/test/java/accord/messages/PreAcceptTest.java
index 23b7b822..ae339a1e 100644
--- a/accord-core/src/test/java/accord/messages/PreAcceptTest.java
+++ b/accord-core/src/test/java/accord/messages/PreAcceptTest.java
@@ -31,6 +31,7 @@ import accord.api.Scheduler;
 import accord.impl.mock.MockCluster.Clock;
 import accord.primitives.*;
 import accord.topology.Topology;
+import accord.utils.DefaultRandom;
 import accord.utils.EpochFunction;
 import accord.utils.ThreadPoolScheduler;
 import accord.local.*;
@@ -39,7 +40,6 @@ import org.junit.jupiter.api.Assertions;
 import org.junit.jupiter.api.Test;
 
 import java.util.List;
-import java.util.Random;
 import java.util.function.BiFunction;
 import java.util.stream.Stream;
 
@@ -75,7 +75,7 @@ public class PreAcceptTest
                         () -> store,
                         new ShardDistributor.EvenSplit(8, ignore -> new 
IntKey.Splitter()),
                         new TestAgent(),
-                        new Random(),
+                        new DefaultRandom(),
                         scheduler,
                         SizeOfIntersectionSorter.SUPPLIER,
                         SimpleProgressLog::new,
diff --git a/accord-core/src/test/java/accord/primitives/KeyDepsTest.java 
b/accord-core/src/test/java/accord/primitives/KeyDepsTest.java
index 53e566fd..953626da 100644
--- a/accord-core/src/test/java/accord/primitives/KeyDepsTest.java
+++ b/accord-core/src/test/java/accord/primitives/KeyDepsTest.java
@@ -18,7 +18,14 @@
 
 package accord.primitives;
 
-import java.util.*;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
+import java.util.Map;
+import java.util.NavigableSet;
+import java.util.Set;
+import java.util.TreeMap;
+import java.util.TreeSet;
 import java.util.concurrent.ExecutionException;
 import java.util.concurrent.ExecutorService;
 import java.util.concurrent.Executors;
@@ -31,8 +38,10 @@ import java.util.stream.IntStream;
 
 import accord.impl.IntHashKey.Hash;
 import accord.primitives.KeyDeps.Builder;
+import accord.utils.DefaultRandom;
 import accord.utils.Gen;
 import accord.utils.Gens;
+import accord.utils.RandomSource;
 import accord.utils.RelationMultiMap.Entry;
 import com.google.common.collect.Sets;
 import org.junit.jupiter.api.Assertions;
@@ -73,7 +82,7 @@ public class KeyDepsTest
     private static void testMerge(long seed, int uniqueTxnIdsRange, int 
epochRange, int hlcRange, int nodeRange,
                                  int uniqueKeysRange, int emptyKeysRange, int 
keyRange, int totalCountRange, int mergeCountRange)
     {
-        Random random = random(seed);
+        RandomSource random = random(seed);
         Supplier<Deps> supplier = supplier(random, uniqueTxnIdsRange, 
epochRange, hlcRange, 0, nodeRange,
                                            uniqueKeysRange, emptyKeysRange, 
keyRange, totalCountRange);
         int count = 1 + random.nextInt(mergeCountRange);
@@ -92,7 +101,7 @@ public class KeyDepsTest
     private static void testWith(long seed, int uniqueTxnIdsRange, int 
epochRange, int hlcRange, int nodeRange,
                                  int uniqueKeysRange, int emptyKeysRange, int 
keyRange, int totalCountRange, int mergeCountRange)
     {
-        Random random = random(seed);
+        RandomSource random = random(seed);
         Supplier<Deps> supplier = supplier(random, uniqueTxnIdsRange, 
epochRange, hlcRange, 0, nodeRange,
                                            uniqueKeysRange, emptyKeysRange, 
keyRange, totalCountRange);
         Deps cur = supplier.get();
@@ -283,7 +292,7 @@ public class KeyDepsTest
                 {
                     builder.nextKey(key);
                     List<TxnId> ids = new ArrayList<>(deps.canonical.get(key));
-                    Collections.shuffle(ids, random);
+                    Collections.shuffle(ids, random.asJdkRandom());
                     ids.forEach(builder::add);
                 }
 
@@ -303,24 +312,24 @@ public class KeyDepsTest
             this.test = test;
         }
 
-        static Deps generate(Gen.Random random)
+        static Deps generate(RandomSource random)
         {
             int epochRange = 3;
             int hlcRange = 500;
             double uniqueTxnIdsPercentage = 0.66D;
-            int uniqueTxnIds = random.nextPositive((int) ((hlcRange * 
epochRange) * uniqueTxnIdsPercentage));
+            int uniqueTxnIds = random.nextInt(1, (int) ((hlcRange * 
epochRange) * uniqueTxnIdsPercentage));
 
-            int nodeRange = random.nextPositive(4);
+            int nodeRange = random.nextInt(1, 4);
             int uniqueKeys = random.nextInt(2, 200);
             int emptyKeys = random.nextInt(0, 10);
             int keyRange = random.nextInt(uniqueKeys + emptyKeys, 400);
-            int totalCount = random.nextPositive(1000);
+            int totalCount = random.nextInt(1, 1000);
             Deps deps = generate(random, uniqueTxnIds, epochRange, hlcRange, 
0, nodeRange, uniqueKeys, emptyKeys, keyRange, totalCount);
             deps.testSimpleEquality();
             return deps;
         }
 
-        static Deps generate(Random random, int uniqueTxnIds, int epochRange, 
int hlcRange, int flagsRange, int nodeRange,
+        static Deps generate(RandomSource random, int uniqueTxnIds, int 
epochRange, int hlcRange, int flagsRange, int nodeRange,
                              int uniqueKeys, int emptyKeys, int keyRange, int 
totalCount)
         {
             // populateKeys is a subset of keys
@@ -446,7 +455,7 @@ public class KeyDepsTest
         }
     }
 
-    private static Ranges randomKeyRanges(Random random, int countRange, int 
valueRange)
+    private static Ranges randomKeyRanges(RandomSource random, int countRange, 
int valueRange)
     {
         int count = countRange == 1 ? 1 : 1 + random.nextInt(countRange - 1);
         Hash[] hashes;
@@ -468,14 +477,14 @@ public class KeyDepsTest
     private static void testOneRandom(long seed, int uniqueTxnIds, int 
epochRange, int hlcRange, int nodeRange,
                                       int uniqueKeys, int emptyKeys, int 
keyRange, int totalCountRange)
     {
-        Random random = random(seed);
+        RandomSource random = random(seed);
         int totalCount = 1 + random.nextInt(totalCountRange - 1);
         testOneDeps(random,
                     KeyDepsTest.Deps.generate(random, uniqueTxnIds, 
epochRange, hlcRange, 0, nodeRange, uniqueKeys, emptyKeys, keyRange, 
totalCount),
                     keyRange);
     }
 
-    private static Supplier<Deps> supplier(Random random, int 
uniqueTxnIdsRange, int epochRange, int hlcRange, int flagRange, int nodeRange,
+    private static Supplier<Deps> supplier(RandomSource random, int 
uniqueTxnIdsRange, int epochRange, int hlcRange, int flagRange, int nodeRange,
                                            int uniqueKeysRange, int 
emptyKeysRange, int keyRange, int totalCountRange)
     {
         return () -> {
@@ -492,7 +501,7 @@ public class KeyDepsTest
         };
     }
 
-    private static void testOneDeps(Random random, Deps deps, int keyRange)
+    private static void testOneDeps(RandomSource random, Deps deps, int 
keyRange)
     {
         deps.testSimpleEquality();
         {
@@ -559,12 +568,10 @@ public class KeyDepsTest
         return ThreadLocalRandom.current().nextLong();
     }
 
-    private static Random random(long seed)
+    private static RandomSource random(long seed)
     {
         logger.info("Seed {}", seed);
-        Random random = new Random();
-        random.setSeed(seed);
-        return random;
+        return new DefaultRandom(seed);
     }
 
     public static void main(String[] args)
diff --git a/accord-core/src/test/java/accord/topology/TopologyRandomizer.java 
b/accord-core/src/test/java/accord/topology/TopologyRandomizer.java
index 56e86560..73d6143e 100644
--- a/accord-core/src/test/java/accord/topology/TopologyRandomizer.java
+++ b/accord-core/src/test/java/accord/topology/TopologyRandomizer.java
@@ -19,6 +19,7 @@
 package accord.topology;
 
 import accord.burn.TopologyUpdates;
+import accord.utils.RandomSource;
 import accord.impl.IntHashKey;
 import accord.impl.IntHashKey.Hash;
 import accord.local.Node;
@@ -27,24 +28,34 @@ import accord.primitives.Ranges;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 
-import java.util.*;
-import java.util.function.*;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+import java.util.function.BiConsumer;
+import java.util.function.BiFunction;
+import java.util.function.Function;
+import java.util.function.Supplier;
+
 
 // TODO (required, testing): add change replication factor
 public class TopologyRandomizer
 {
     private static final Logger logger = 
LoggerFactory.getLogger(TopologyRandomizer.class);
-    private final Random random;
+    private final RandomSource random;
     private final BiConsumer<Node.Id, Topology> notifier;
     private final List<Topology> epochs = new ArrayList<>();
     private final Map<Node.Id, Ranges> previouslyReplicated = new HashMap<>();
     private final TopologyUpdates topologyUpdates;
 
-    public TopologyRandomizer(Supplier<Random> randomSupplier, Topology 
initialTopology, TopologyUpdates topologyUpdates, Function<Node.Id, Node> 
lookup)
+    public TopologyRandomizer(Supplier<RandomSource> randomSupplier, Topology 
initialTopology, TopologyUpdates topologyUpdates, Function<Node.Id, Node> 
lookup)
     {
         this(randomSupplier, initialTopology, topologyUpdates, (id, topology) 
-> topologyUpdates.notify(lookup.apply(id), topology.nodes(), topology));
     }
-    public TopologyRandomizer(Supplier<Random> randomSupplier, Topology 
initialTopology, TopologyUpdates topologyUpdates, BiConsumer<Node.Id, Topology> 
notifier)
+    public TopologyRandomizer(Supplier<RandomSource> randomSupplier, Topology 
initialTopology, TopologyUpdates topologyUpdates, BiConsumer<Node.Id, Topology> 
notifier)
     {
         this.random = randomSupplier.get();
         this.topologyUpdates = topologyUpdates;
@@ -61,26 +72,26 @@ public class TopologyRandomizer
         MEMBERSHIP(TopologyRandomizer::updateMembership),
         FASTPATH(TopologyRandomizer::updateFastPath);
 
-        private final BiFunction<Shard[], Random, Shard[]> function;
+        private final BiFunction<Shard[], RandomSource, Shard[]> function;
 
-        UpdateType(BiFunction<Shard[], Random, Shard[]> function)
+        UpdateType(BiFunction<Shard[], RandomSource, Shard[]> function)
         {
             this.function = function;
         }
 
-        public Shard[] apply(Shard[] shards, Random random)
+        public Shard[] apply(Shard[] shards, RandomSource random)
         {
             return function.apply(shards, random);
         }
 
-        static UpdateType kind(Random random)
+        static UpdateType kind(RandomSource random)
         {
             int idx = random.nextInt(values().length);
             return values()[idx];
         }
     }
 
-    private static Shard[] updateBoundary(Shard[] shards, Random random)
+    private static Shard[] updateBoundary(Shard[] shards, RandomSource random)
     {
         int idx = random.nextInt(shards.length - 1);
         Shard left = shards[idx];
@@ -104,7 +115,7 @@ public class TopologyRandomizer
         return shards;
     }
 
-    private static Shard[] updateMembership(Shard[] shards, Random random)
+    private static Shard[] updateMembership(Shard[] shards, RandomSource 
random)
     {
         if (shards.length <= 1)
             return shards;
@@ -156,7 +167,7 @@ public class TopologyRandomizer
         return shards;
     }
 
-    private static Set<Node.Id> newFastPath(List<Node.Id> nodes, Random random)
+    private static Set<Node.Id> newFastPath(List<Node.Id> nodes, RandomSource 
random)
     {
         List<Node.Id> available = new ArrayList<>(nodes);
         int rf = available.size();
@@ -174,7 +185,7 @@ public class TopologyRandomizer
         return fastPath;
     }
 
-    private static Shard[] updateFastPath(Shard[] shards, Random random)
+    private static Shard[] updateFastPath(Shard[] shards, RandomSource random)
     {
         int idx = random.nextInt(shards.length);
         Shard shard = shards[idx];
diff --git a/accord-core/src/test/java/accord/utils/AccordGens.java 
b/accord-core/src/test/java/accord/utils/AccordGens.java
new file mode 100644
index 00000000..175a5e39
--- /dev/null
+++ b/accord-core/src/test/java/accord/utils/AccordGens.java
@@ -0,0 +1,154 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package accord.utils;
+
+import java.util.Arrays;
+import java.util.HashSet;
+import java.util.Objects;
+import java.util.Set;
+import java.util.function.BiFunction;
+
+import accord.api.Key;
+import accord.api.RoutingKey;
+import accord.impl.IntHashKey;
+import accord.impl.IntKey;
+import accord.local.Node;
+import accord.primitives.Deps;
+import accord.primitives.KeyDeps;
+import accord.primitives.Range;
+import accord.primitives.RangeDeps;
+import accord.primitives.Routable;
+import accord.primitives.Timestamp;
+import accord.primitives.Txn;
+import accord.primitives.TxnId;
+
+public class AccordGens
+{
+    public static Gen.LongGen epochs()
+    {
+        return Gens.longs().between(0, Timestamp.MAX_EPOCH);
+    }
+
+    public static Gen<TxnId> txnIds()
+    {
+        return txnIds(epochs()::nextLong, RandomSource::nextLong, 
RandomSource::nextInt);
+    }
+
+    public static Gen<TxnId> txnIds(Gen.LongGen epochs, Gen.LongGen hlcs, 
Gen.IntGen nodes)
+    {
+        Gen<Txn.Kind> kinds = Gens.enums().all(Txn.Kind.class);
+        Gen<Routable.Domain> domains = Gens.enums().all(Routable.Domain.class);
+        return rs -> new TxnId(epochs.nextLong(rs), hlcs.nextLong(rs), 
kinds.next(rs), domains.next(rs), new Node.Id(nodes.nextInt(rs)));
+    }
+
+    public static Gen<Key> intKeys()
+    {
+        return rs -> new IntKey.Raw(rs.nextInt());
+    }
+
+    public static Gen<Key> intHashKeys()
+    {
+        return rs -> IntHashKey.key(rs.nextInt());
+    }
+
+    public static Gen<KeyDeps> keyDeps(Gen<Key> keyGen)
+    {
+        return keyDeps(keyGen, txnIds());
+    }
+
+    public static Gen<KeyDeps> keyDeps(Gen<Key> keyGen, Gen<TxnId> idGen)
+    {
+        double emptyProb = .2D;
+        return rs -> {
+            if (rs.decide(emptyProb)) return KeyDeps.NONE;
+            Set<Key> seenKeys = new HashSet<>();
+            Set<TxnId> seenTxn = new HashSet<>();
+            Gen<Key> uniqKeyGen = keyGen.filter(seenKeys::add);
+            Gen<TxnId> uniqIdGen = idGen.filter(seenTxn::add);
+            try (KeyDeps.Builder builder = KeyDeps.builder())
+            {
+                for (int i = 0, numKeys = rs.nextInt(1, 10); i < numKeys; i++)
+                {
+                    builder.nextKey(uniqKeyGen.next(rs));
+                    seenTxn.clear();
+                    for (int j = 0, numTxn = rs.nextInt(1, 10); j < numTxn; 
j++)
+                        builder.add(uniqIdGen.next(rs));
+                }
+                return builder.build();
+            }
+        };
+    }
+
+    public interface RangeFactory
+    {
+        Range create(RandomSource rs, RoutingKey a, RoutingKey b);
+    }
+
+    public static Gen<Range> ranges(Gen<RoutingKey> keyGen, BiFunction<? super 
RoutingKey, ? super RoutingKey, ? extends Range> factory)
+    {
+        return ranges(keyGen, (ignore, a, b) -> factory.apply(a, b));
+    }
+
+    public static Gen<Range> ranges(Gen<RoutingKey> keyGen)
+    {
+        return ranges(keyGen, (rs, a, b) -> {
+            boolean left = rs.nextBoolean();
+            return Range.range(a, b, left, !left);
+        });
+    }
+
+    public static Gen<Range> ranges(Gen<RoutingKey> keyGen, RangeFactory 
factory)
+    {
+        RoutingKey[] keys = new RoutingKey[2];
+        return rs -> {
+            keys[0] = keyGen.next(rs);
+            // range doesn't allow a=b
+            do keys[1] = keyGen.next(rs);
+            while (Objects.equals(keys[0], keys[1]));
+            Arrays.sort(keys);
+            return factory.create(rs, keys[0], keys[1]);
+        };
+    }
+
+    public static Gen<RangeDeps> rangeDeps(Gen<Range> rangeGen)
+    {
+        return rangeDeps(rangeGen, txnIds());
+    }
+
+    public static Gen<RangeDeps> rangeDeps(Gen<Range> rangeGen, Gen<TxnId> 
idGen)
+    {
+        double emptyProb = .2D;
+        return rs -> {
+            if (rs.decide(emptyProb)) return RangeDeps.NONE;
+            RangeDeps.Builder builder = RangeDeps.builder();
+            for (int i = 0, numKeys = rs.nextInt(1, 10); i < numKeys; i++)
+            {
+                builder.nextKey(rangeGen.next(rs));
+                for (int j = 0, numTxn = rs.nextInt(1, 10); j < numTxn; j++)
+                    builder.add(idGen.next(rs));
+            }
+            return builder.build();
+        };
+    }
+
+    public static Gen<Deps> deps(Gen<KeyDeps> keyDepsGen, Gen<RangeDeps> 
rangeDepsGen)
+    {
+        return rs -> new Deps(keyDepsGen.next(rs), rangeDepsGen.next(rs));
+    }
+}
diff --git a/accord-core/src/test/java/accord/utils/Gen.java 
b/accord-core/src/test/java/accord/utils/Gen.java
index f4827f9e..f1b00e24 100644
--- a/accord-core/src/test/java/accord/utils/Gen.java
+++ b/accord-core/src/test/java/accord/utils/Gen.java
@@ -20,6 +20,8 @@ package accord.utils;
 
 import java.util.function.BiFunction;
 import java.util.function.Function;
+import java.util.function.IntPredicate;
+import java.util.function.LongPredicate;
 import java.util.function.Predicate;
 
 public interface Gen<A> {
@@ -32,14 +34,14 @@ public interface Gen<A> {
         return fn;
     }
 
-    A next(Random random);
+    A next(RandomSource random);
 
     default <B> Gen<B> map(Function<A, B> fn)
     {
         return r -> fn.apply(this.next(r));
     }
 
-    default <B> Gen<B> map(BiFunction<Random, A, B> fn)
+    default <B> Gen<B> map(BiFunction<RandomSource, A, B> fn)
     {
         return r -> fn.apply(r, this.next(r));
     }
@@ -57,93 +59,63 @@ public interface Gen<A> {
         };
     }
 
-    class Random extends java.util.Random
+    interface IntGen extends Gen<Integer>
     {
-        public Random(long seed) {
-            super(seed);
-        }
-
-        public int nextInt(int origin, int bound)
-        {
-            if (origin >= bound)
-                throw new IllegalArgumentException("bound (" + bound + ") must 
be greater than origin (" + origin + ")");
-            int r = nextInt();
-            if (origin < bound) {
-                int n = bound - origin, m = n - 1;
-                if ((n & m) == 0)
-                    r = (r & m) + origin;
-                else if (n > 0) {
-                    for (int u = r >>> 1;
-                         u + m - (r = u % n) < 0;
-                         u = nextInt() >>> 1)
-                        ;
-                    r += origin;
-                }
-                else {
-                    while (r < origin || r >= bound)
-                        r = nextInt();
-                }
-            }
-            return r;
-        }
-
-        public int allPositive()
-        {
-            return nextInt(1, Integer.MAX_VALUE);
-        }
+        int nextInt(RandomSource random);
 
-        public int nextPositive(int upper)
+        @Override
+        default Integer next(RandomSource random)
         {
-            return nextInt(1, upper);
+            return nextInt(random);
         }
 
-        public long nextLong(long bound)
+        default Gen.IntGen filterInt(IntPredicate fn)
         {
-            return nextLong(0, bound);
+            return rs -> {
+                int value;
+                do
+                {
+                    value = nextInt(rs);
+                }
+                while (!fn.test(value));
+                return value;
+            };
         }
 
-        public long nextLong(long origin, long bound)
+        @Override
+        default Gen.IntGen filter(Predicate<Integer> fn)
         {
-            if (origin >= bound)
-                throw new IllegalArgumentException("bound must be greater than 
origin");
-            long r = nextLong();
-            long n = bound - origin, m = n - 1;
-            if ((n & m) == 0L)  // power of two
-                r = (r & m) + origin;
-            else if (n > 0L) {  // reject over-represented candidates
-                for (long u = r >>> 1;            // ensure nonnegative
-                     u + m - (r = u % n) < 0L;    // rejection check
-                     u = nextLong() >>> 1) // retry
-                    ;
-                r += origin;
-            }
-            else {              // range not representable as long
-                while (r < origin || r >= bound)
-                    r = nextLong();
-            }
-            return r;
+            return filterInt(i -> fn.test(i));
         }
     }
 
-    interface IntGen extends Gen<Integer>
+    interface LongGen extends Gen<Long>
     {
-        int nextInt(Random random);
+        long nextLong(RandomSource random);
 
         @Override
-        default Integer next(Random random)
+        default Long next(RandomSource random)
         {
-            return nextInt(random);
+            return nextLong(random);
         }
-    }
 
-    interface LongGen extends Gen<Long>
-    {
-        long nextLong(Random random);
+        default Gen.LongGen filterLong(LongPredicate fn)
+        {
+            return rs -> {
+                long value;
+                do
+                {
+                    value = nextLong(rs);
+                }
+                while (!fn.test(value));
+                return value;
+            };
+        }
 
         @Override
-        default Long next(Random random)
+        default Gen.LongGen filter(Predicate<Long> fn)
         {
-            return nextLong(random);
+            return filterLong(i -> fn.test(i));
         }
     }
 }
diff --git a/accord-core/src/test/java/accord/utils/Gens.java 
b/accord-core/src/test/java/accord/utils/Gens.java
index 0aa7d0f4..7cb12f74 100644
--- a/accord-core/src/test/java/accord/utils/Gens.java
+++ b/accord-core/src/test/java/accord/utils/Gens.java
@@ -25,8 +25,10 @@ import java.util.HashSet;
 import java.util.List;
 import java.util.Objects;
 import java.util.Set;
+import java.util.function.Function;
 import java.util.function.Supplier;
 
+
 public class Gens {
     private Gens() {
     }
@@ -52,10 +54,45 @@ public class Gens {
         return rs -> ts.get(offset.nextInt(rs));
     }
 
-    public static Gen<Gen.Random> random() {
+    public static Gen<char[]> charArray(Gen.IntGen sizes, char[] domain)
+    {
+        return charArray(sizes, domain, (a, b) -> true);
+    }
+
+    public interface IntCharBiPredicate
+    {
+        boolean test(int a, char b);
+    }
+
+    public static Gen<char[]> charArray(Gen.IntGen sizes, char[] domain, 
IntCharBiPredicate fn)
+    {
+        Gen.IntGen indexGen = ints().between(0, domain.length - 1);
+        return rs -> {
+            int size = sizes.nextInt(rs);
+            char[] is = new char[size];
+            for (int i = 0; i != size; i++)
+            {
+                char c;
+                do
+                {
+                    c = domain[indexGen.nextInt(rs)];
+                }
+                while (!fn.test(i, c));
+                is[i] = c;
+            }
+            return is;
+        };
+    }
+
+    public static Gen<RandomSource> random() {
         return r -> r;
     }
 
+    public static BooleanDSL bools()
+    {
+        return new BooleanDSL();
+    }
+
     public static IntDSL ints()
     {
         return new IntDSL();
@@ -86,6 +123,19 @@ public class Gens {
         return new EnumDSL();
     }
 
+    public static StringDSL strings()
+    {
+        return new StringDSL();
+    }
+
+    public static class BooleanDSL
+    {
+        public Gen<Boolean> all()
+        {
+            return RandomSource::nextBoolean;
+        }
+    }
+
     public static class IntDSL
     {
         public Gen.IntGen of(int value)
@@ -95,12 +145,12 @@ public class Gens {
 
         public Gen.IntGen all()
         {
-            return Gen.Random::nextInt;
+            return RandomSource::nextInt;
         }
 
         public Gen.IntGen between(int min, int max)
         {
-            Invariants.checkArgument(max >= min);
+            Invariants.checkArgument(max >= min, "max (%d) < min (%d)", max, 
min);
             if (min == max)
                 return of(min);
             // since bounds is exclusive, if max == max_value unable to do +1 
to include... so will return a gen
@@ -118,7 +168,7 @@ public class Gens {
         }
 
         public Gen.LongGen all() {
-            return Gen.Random::nextLong;
+            return RandomSource::nextLong;
         }
 
         public Gen.LongGen between(long min, long max) {
@@ -140,6 +190,87 @@ public class Gens {
             return pick(klass.getEnumConstants());
         }
     }
+    
+    public static class StringDSL
+    {
+        public Gen<String> of(Gen.IntGen sizes, char[] domain)
+        {
+            // note, map is overloaded so String::new is ambugious to javac, 
so need a lambda here
+            return charArray(sizes, domain).map(c -> new String(c));
+        }
+
+        public SizeBuilder<String> of(char[] domain)
+        {
+            return new SizeBuilder<>(sizes -> of(sizes, domain));
+        }
+
+        public Gen<String> of(Gen.IntGen sizes, char[] domain, 
IntCharBiPredicate fn)
+        {
+            // note, map is overloaded so String::new is ambugious to javac, 
so need a lambda here
+            return charArray(sizes, domain, fn).map(c -> new String(c));
+        }
+
+        public SizeBuilder<String> of(char[] domain, IntCharBiPredicate fn)
+        {
+            return new SizeBuilder<>(sizes -> of(sizes, domain, fn));
+        }
+
+        public Gen<String> all(Gen.IntGen sizes)
+        {
+            return betweenCodePoints(sizes, Character.MIN_CODE_POINT, 
Character.MAX_CODE_POINT);
+        }
+
+        public SizeBuilder<String> all()
+        {
+            return new SizeBuilder<>(this::all);
+        }
+
+        public Gen<String> ascii(Gen.IntGen sizes)
+        {
+            return betweenCodePoints(sizes, 0, 127);
+        }
+
+        public SizeBuilder<String> ascii()
+        {
+            return new SizeBuilder<>(this::ascii);
+        }
+
+        public Gen<String> betweenCodePoints(Gen.IntGen sizes, int min, int 
max)
+        {
+            Gen.IntGen codePointGen = ints().between(min, 
max).filter(Character::isDefined);
+            return rs -> {
+                int[] array = new int[sizes.nextInt(rs)];
+                for (int i = 0; i < array.length; i++)
+                    array[i] = codePointGen.nextInt(rs);
+                return new String(array, 0, array.length);
+            };
+        }
+
+        public SizeBuilder<String> betweenCodePoints(int min, int max)
+        {
+            return new SizeBuilder<>(sizes -> betweenCodePoints(sizes, min, 
max));
+        }
+    }
+
+    public static class SizeBuilder<T>
+    {
+        private final Function<Gen.IntGen, Gen<T>> fn;
+
+        public SizeBuilder(Function<Gen.IntGen, Gen<T>> fn)
+        {
+            this.fn = fn;
+        }
+
+        public Gen<T> ofLength(int fixed)
+        {
+            return ofLengthBetween(fixed, fixed);
+        }
+
+        public Gen<T> ofLengthBetween(int min, int max)
+        {
+            return fn.apply(ints().between(min, max));
+        }
+    }
 
     public static class ListDSL<T> implements BaseSequenceDSL<ListDSL<T>, 
List<T>> {
         private final Gen<T> fn;
@@ -285,7 +416,7 @@ public class Gens {
         }
 
         @Override
-        public T next(Random random)
+        public T next(RandomSource random)
         {
             T value;
             while (!seen.add((value = fn.next(random)))) {}
@@ -308,7 +439,7 @@ public class Gens {
             this.base = new GenReset<>(fn);
         }
         @Override
-        public int nextInt(Random random) {
+        public int nextInt(RandomSource random) {
             return base.next(random);
         }
 
@@ -327,7 +458,7 @@ public class Gens {
             this.base = new GenReset<>(fn);
         }
         @Override
-        public long nextLong(Random random) {
+        public long nextLong(RandomSource random) {
             return base.next(random);
         }
 
diff --git a/accord-core/src/test/java/accord/utils/Property.java 
b/accord-core/src/test/java/accord/utils/Property.java
index 9bbbc6d6..3e9cb5d6 100644
--- a/accord-core/src/test/java/accord/utils/Property.java
+++ b/accord-core/src/test/java/accord/utils/Property.java
@@ -18,13 +18,9 @@
 
 package accord.utils;
 
-import accord.utils.Gen.Random;
-
 import java.util.Arrays;
 import java.util.Objects;
 import java.util.concurrent.ThreadLocalRandom;
-import java.util.function.BiConsumer;
-import java.util.function.Consumer;
 
 public class Property
 {
@@ -76,6 +72,11 @@ public class Property
         {
             return new DoubleBuilder<>(a, b, this);
         }
+
+        public <A, B, C> TrippleBuilder<A, B, C> forAll(Gen<A> a, Gen<B> b, 
Gen<C> c)
+        {
+            return new TrippleBuilder<>(a, b, c, this);
+        }
     }
 
     private static Object normalizeValue(Object value)
@@ -149,7 +150,7 @@ public class Property
 
         public void check(FailingConsumer<T> fn)
         {
-            Random random = new Random(seed);
+            RandomSource random = new DefaultRandom(seed);
             for (int i = 0; i < examples; i++)
             {
                 T value = null;
@@ -189,7 +190,7 @@ public class Property
 
         public void check(FailingBiConsumer<A, B> fn)
         {
-            Random random = new Random(seed);
+            RandomSource random = new DefaultRandom(seed);
             for (int i = 0; i < examples; i++)
             {
                 A a = null;
@@ -212,6 +213,51 @@ public class Property
         }
     }
 
+    public interface FailingTriConsumer<A, B, C>
+    {
+        void accept(A a, B b, C c) throws Exception;
+    }
+
+    public static class TrippleBuilder<A, B, C> extends 
Common<TrippleBuilder<A, B, C>>
+    {
+        private final Gen<A> as;
+        private final Gen<B> bs;
+        private final Gen<C> cs;
+
+        public TrippleBuilder(Gen<A> as, Gen<B> bs, Gen<C> cs, Common<?> other)
+        {
+            super(other);
+            this.as = as;
+            this.bs = bs;
+            this.cs = cs;
+        }
+
+        public void check(FailingTriConsumer<A, B, C> fn)
+        {
+            RandomSource random = new DefaultRandom(seed);
+            for (int i = 0; i < examples; i++)
+            {
+                A a = null;
+                B b = null;
+                C c = null;
+                try
+                {
+                    checkInterrupted();
+                    fn.accept(a = as.next(random), b = bs.next(random), c = 
cs.next(random));
+                }
+                catch (Throwable t)
+                {
+                    throw new PropertyError(propertyError(this, t, a, b, c), 
t);
+                }
+                if (pure)
+                {
+                    seed = random.nextLong();
+                    random.setSeed(seed);
+                }
+            }
+        }
+    }
+
     private static void checkInterrupted() throws InterruptedException {
         if (Thread.currentThread().isInterrupted())
             throw new InterruptedException();
diff --git a/accord-maelstrom/build.gradle b/accord-maelstrom/build.gradle
index cc08d3f4..2499217a 100644
--- a/accord-maelstrom/build.gradle
+++ b/accord-maelstrom/build.gradle
@@ -25,7 +25,7 @@ dependencies {
     implementation group: 'com.google.code.findbugs', name: 'jsr305', version: 
'3.0.2'
     implementation 'com.google.code.gson:gson:2.8.7'
 
-    testImplementation(testFixtures(project(':accord-core')))
+    testImplementation project(':accord-core').sourceSets.test.output
 }
 
 jar {
diff --git a/accord-maelstrom/src/main/java/accord/maelstrom/Cluster.java 
b/accord-maelstrom/src/main/java/accord/maelstrom/Cluster.java
index 95130f5c..74de7eb1 100644
--- a/accord-maelstrom/src/main/java/accord/maelstrom/Cluster.java
+++ b/accord-maelstrom/src/main/java/accord/maelstrom/Cluster.java
@@ -24,7 +24,15 @@ import java.io.InputStream;
 import java.io.InputStreamReader;
 import java.io.OutputStream;
 import java.io.PrintWriter;
-import java.util.*;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.LinkedHashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
 import java.util.concurrent.TimeUnit;
 import java.util.function.Consumer;
 import java.util.function.Function;
@@ -33,7 +41,6 @@ import java.util.function.Supplier;
 
 import accord.coordinate.Timeout;
 import accord.impl.SizeOfIntersectionSorter;
-import accord.local.CommandStores;
 import accord.impl.SimpleProgressLog;
 import accord.impl.InMemoryCommandStores;
 import accord.local.Node;
@@ -46,6 +53,7 @@ import accord.messages.ReplyContext;
 import accord.messages.Request;
 import accord.api.Scheduler;
 import accord.topology.Topology;
+import accord.utils.RandomSource;
 
 // TODO (low priority, testing): merge with accord.impl.basic.Cluster
 public class Cluster implements Scheduler
@@ -68,12 +76,12 @@ public class Cluster implements Scheduler
         final Id self;
         final Function<Id, Node> lookup;
         final Cluster parent;
-        final Random random;
+        final RandomSource random;
 
         int nextMessageId = 0;
         Map<Long, Callback> callbacks = new LinkedHashMap<>();
 
-        public InstanceSink(Id self, Function<Id, Node> lookup, Cluster 
parent, Random random)
+        public InstanceSink(Id self, Function<Id, Node> lookup, Cluster 
parent, RandomSource random)
         {
             this.self = self;
             this.lookup = lookup;
@@ -125,7 +133,7 @@ public class Cluster implements Scheduler
         this.partitionSet = new HashSet<>();
     }
 
-    InstanceSink create(Id self, Random random)
+    InstanceSink create(Id self, RandomSource random)
     {
         InstanceSink sink = new InstanceSink(self, lookup, this, random);
         sinks.put(self, sink);
@@ -272,7 +280,7 @@ public class Cluster implements Scheduler
         run.run();
     }
 
-    public static void run(Id[] nodes, QueueSupplier queueSupplier, 
Consumer<Packet> responseSink, Supplier<Random> randomSupplier, 
Supplier<LongSupplier> nowSupplier, TopologyFactory topologyFactory, 
InputStream stdin, OutputStream stderr) throws IOException
+    public static void run(Id[] nodes, QueueSupplier queueSupplier, 
Consumer<Packet> responseSink, Supplier<RandomSource> randomSupplier, 
Supplier<LongSupplier> nowSupplier, TopologyFactory topologyFactory, 
InputStream stdin, OutputStream stderr) throws IOException
     {
         try (BufferedReader in = new BufferedReader(new 
InputStreamReader(stdin)))
         {
@@ -289,7 +297,7 @@ public class Cluster implements Scheduler
         }
     }
 
-    public static void run(Id[] nodes, QueueSupplier queueSupplier, 
Consumer<Packet> responseSink, Supplier<Random> randomSupplier, 
Supplier<LongSupplier> nowSupplier, TopologyFactory topologyFactory, 
Supplier<Packet> in, OutputStream stderr)
+    public static void run(Id[] nodes, QueueSupplier queueSupplier, 
Consumer<Packet> responseSink, Supplier<RandomSource> randomSupplier, 
Supplier<LongSupplier> nowSupplier, TopologyFactory topologyFactory, 
Supplier<Packet> in, OutputStream stderr)
     {
         Topology topology = topologyFactory.toTopology(nodes);
         Map<Id, Node> lookup = new HashMap<>();
@@ -309,7 +317,7 @@ public class Cluster implements Scheduler
             List<Id> nodesList = new ArrayList<>(Arrays.asList(nodes));
             sinks.recurring(() ->
                             {
-                                Collections.shuffle(nodesList, 
randomSupplier.get());
+                                Collections.shuffle(nodesList, 
randomSupplier.get().asJdkRandom());
                                 int partitionSize = 
randomSupplier.get().nextInt((topologyFactory.rf+1)/2);
                                 sinks.partitionSet = new 
HashSet<>(nodesList.subList(0, partitionSize));
                             }, 5L, TimeUnit.SECONDS);
diff --git a/accord-maelstrom/src/main/java/accord/maelstrom/Datum.java 
b/accord-maelstrom/src/main/java/accord/maelstrom/Datum.java
index 696f7d61..90f93e08 100644
--- a/accord-maelstrom/src/main/java/accord/maelstrom/Datum.java
+++ b/accord-maelstrom/src/main/java/accord/maelstrom/Datum.java
@@ -19,6 +19,7 @@
 package accord.maelstrom;
 
 import java.io.IOException;
+import java.util.Objects;
 import java.util.function.BiFunction;
 import java.util.zip.CRC32;
 
@@ -44,6 +45,21 @@ public class Datum implements Comparable<Datum>
             return Integer.compare(this.hash, that.hash);
         }
 
+        @Override
+        public boolean equals(Object o)
+        {
+            if (this == o) return true;
+            if (o == null || getClass() != o.getClass()) return false;
+            Hash hash1 = (Hash) o;
+            return hash == hash1.hash;
+        }
+
+        @Override
+        public int hashCode()
+        {
+            return Objects.hash(hash);
+        }
+
         public String toString()
         {
             return "#" + hash;
diff --git a/accord-maelstrom/src/main/java/accord/maelstrom/Json.java 
b/accord-maelstrom/src/main/java/accord/maelstrom/Json.java
index fcb56dfc..63b757c3 100644
--- a/accord-maelstrom/src/main/java/accord/maelstrom/Json.java
+++ b/accord-maelstrom/src/main/java/accord/maelstrom/Json.java
@@ -79,8 +79,10 @@ public class Json
     {
         switch (id.charAt(0))
         {
-            case 'c': return new Id(-Integer.parseInt(id.substring(1)));
-            case 'n':return  new Id( Integer.parseInt(id.substring(1)));
+            //TODO(Review) - toString idn't remove the - so doing - parseInt 
makes the value positive, which changes
+            // the value
+            case 'c': return new Id( Integer.parseInt(id.substring(1)));
+            case 'n': return new Id( Integer.parseInt(id.substring(1)));
             default: throw new IllegalStateException();
         }
     }
@@ -314,6 +316,8 @@ public class Json
         @Override
         public void write(JsonWriter out, Deps value) throws IOException
         {
+            out.beginObject();
+            out.name("keyDeps");
             out.beginArray();
             for (Map.Entry<Key, TxnId> e : value.keyDeps)
             {
@@ -323,6 +327,7 @@ public class Json
                 out.endArray();
             }
             out.endArray();
+            out.name("rangeDeps");
             out.beginArray();
             for (Map.Entry<Range, TxnId> e : value.rangeDeps)
             {
@@ -333,42 +338,61 @@ public class Json
                 out.endArray();
             }
             out.endArray();
+            out.endObject();
         }
 
         @Override
         public Deps read(JsonReader in) throws IOException
         {
-            KeyDeps keyDeps;
-            try (KeyDeps.Builder builder = KeyDeps.builder())
-            {
-                in.beginArray();
-                while (in.hasNext())
-                {
-                    in.beginArray();
-                    Key key = MaelstromKey.readKey(in);
-                    TxnId txnId = GSON.fromJson(in, TxnId.class);
-                    builder.add(key, txnId);
-                    in.endArray();
-                }
-                in.endArray();
-                keyDeps = builder.build();
-            }
-            RangeDeps rangeDeps;
-            try (RangeDeps.Builder builder = RangeDeps.builder())
+            KeyDeps keyDeps = KeyDeps.NONE;
+            RangeDeps rangeDeps = RangeDeps.NONE;
+            in.beginObject();
+            while (in.hasNext())
             {
-                in.beginArray();
-                while (in.hasNext())
+                String name;
+                switch (name = in.nextName())
                 {
-                    in.beginArray();
-                    RoutingKey start = MaelstromKey.readRouting(in);
-                    RoutingKey end = MaelstromKey.readRouting(in);
-                    TxnId txnId = GSON.fromJson(in, TxnId.class);
-                    builder.add(new MaelstromKey.Range(start, end), txnId);
-                    in.endArray();
+                    case "keyDeps":
+                    {
+                        try (KeyDeps.Builder builder = KeyDeps.builder())
+                        {
+                            in.beginArray();
+                            while (in.hasNext())
+                            {
+                                in.beginArray();
+                                Key key = MaelstromKey.readKey(in);
+                                TxnId txnId = GSON.fromJson(in, TxnId.class);
+                                builder.add(key, txnId);
+                                in.endArray();
+                            }
+                            in.endArray();
+                            keyDeps = builder.build();
+                        }
+                    }
+                    break;
+                    case "rangeDeps":
+                    {
+                        try (RangeDeps.Builder builder = RangeDeps.builder())
+                        {
+                            in.beginArray();
+                            while (in.hasNext())
+                            {
+                                in.beginArray();
+                                RoutingKey start = 
MaelstromKey.readRouting(in);
+                                RoutingKey end = MaelstromKey.readRouting(in);
+                                TxnId txnId = GSON.fromJson(in, TxnId.class);
+                                builder.add(new MaelstromKey.Range(start, 
end), txnId);
+                                in.endArray();
+                            }
+                            in.endArray();
+                            rangeDeps = builder.build();
+                        }
+                    }
+                    break;
+                    default: throw new AssertionError("Unknown name: " + name);
                 }
-                in.endArray();
-                rangeDeps = builder.build();
             }
+            in.endObject();
             return new Deps(keyDeps, rangeDeps);
         }
     };
diff --git a/accord-maelstrom/src/main/java/accord/maelstrom/MaelstromKey.java 
b/accord-maelstrom/src/main/java/accord/maelstrom/MaelstromKey.java
index b42af64c..7dbca6c7 100644
--- a/accord-maelstrom/src/main/java/accord/maelstrom/MaelstromKey.java
+++ b/accord-maelstrom/src/main/java/accord/maelstrom/MaelstromKey.java
@@ -19,6 +19,7 @@
 package accord.maelstrom;
 
 import java.io.IOException;
+import java.util.Objects;
 
 import accord.api.RoutingKey;
 
@@ -223,6 +224,21 @@ public class MaelstromKey implements RoutableKey
         return new Routing(datum.value.hashCode());
     }
 
+    @Override
+    public boolean equals(Object o)
+    {
+        if (this == o) return true;
+        if (o == null || getClass() != o.getClass()) return false;
+        MaelstromKey that = (MaelstromKey) o;
+        return Objects.equals(datum, that.datum);
+    }
+
+    @Override
+    public int hashCode()
+    {
+        return Objects.hash(datum);
+    }
+
     @Override
     public String toString()
     {
diff --git a/accord-maelstrom/src/main/java/accord/maelstrom/Main.java 
b/accord-maelstrom/src/main/java/accord/maelstrom/Main.java
index 1df71373..955645c8 100644
--- a/accord-maelstrom/src/main/java/accord/maelstrom/Main.java
+++ b/accord-maelstrom/src/main/java/accord/maelstrom/Main.java
@@ -24,7 +24,6 @@ import java.io.InputStream;
 import java.io.InputStreamReader;
 import java.io.PrintStream;
 import java.util.Map;
-import java.util.Random;
 import java.util.concurrent.ConcurrentHashMap;
 import java.util.concurrent.TimeUnit;
 import java.util.concurrent.atomic.AtomicLong;
@@ -41,6 +40,7 @@ import accord.api.Scheduler;
 import accord.local.ShardDistributor;
 import accord.messages.ReplyContext;
 import accord.topology.Topology;
+import accord.utils.DefaultRandom;
 import accord.utils.ThreadPoolScheduler;
 import accord.maelstrom.Packet.Type;
 import accord.api.MessageSink;
@@ -162,7 +162,7 @@ public class Main
             sink = new StdoutSink(System::currentTimeMillis, scheduler, start, 
init.self, out, err);
             on = new Node(init.self, sink, new SimpleConfigService(topology), 
System::currentTimeMillis,
                           MaelstromStore::new, new 
ShardDistributor.EvenSplit(8, ignore -> new MaelstromKey.Splitter()),
-                          MaelstromAgent.INSTANCE, new Random(), scheduler, 
SizeOfIntersectionSorter.SUPPLIER,
+                          MaelstromAgent.INSTANCE, new DefaultRandom(), 
scheduler, SizeOfIntersectionSorter.SUPPLIER,
                           SimpleProgressLog::new, 
InMemoryCommandStores.SingleThread::new);
             err.println("Initialized node " + init.self);
             err.flush();
diff --git a/accord-maelstrom/src/test/java/accord/maelstrom/JsonTest.java 
b/accord-maelstrom/src/test/java/accord/maelstrom/JsonTest.java
new file mode 100644
index 00000000..af73e245
--- /dev/null
+++ b/accord-maelstrom/src/test/java/accord/maelstrom/JsonTest.java
@@ -0,0 +1,90 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package accord.maelstrom;
+
+import org.junit.jupiter.api.Test;
+
+import accord.api.Key;
+import accord.primitives.Deps;
+import accord.primitives.Range;
+import accord.primitives.RangeDeps;
+import accord.utils.AccordGens;
+import accord.utils.Gen;
+import accord.utils.Gens;
+import org.assertj.core.api.Assertions;
+
+import static accord.utils.Property.qt;
+
+class JsonTest
+{
+    @Test
+    void serdeDeps()
+    {
+        qt().forAll(depsGen()).check(JsonTest::serde);
+    }
+
+    private static <T> void serde(T expected)
+    {
+        String json = Json.GSON.toJson(expected);
+        T parsed;
+        try
+        {
+            parsed = Json.GSON.<T>fromJson(json, expected.getClass());
+        }
+        catch (Throwable t)
+        {
+            throw new AssertionError("Unable to parse json: " + json, t);
+        }
+        Assertions.assertThat(parsed)
+                  .isEqualTo(expected);
+    }
+
+    static Gen<Key> keyGen()
+    {
+        Gen<Datum.Kind> kindGen = Gens.enums().all(Datum.Kind.class);
+        Gen<String> strings = Gens.strings().all().ofLengthBetween(0, 10);
+        return rs -> {
+            Datum.Kind next = kindGen.next(rs);
+            switch (next)
+            {
+                case STRING: return new MaelstromKey.Key(Datum.Kind.STRING, 
strings.next(rs));
+                case LONG: return new MaelstromKey.Key(Datum.Kind.LONG, 
rs.nextLong());
+                case HASH: return new MaelstromKey.Key(Datum.Kind.HASH, new 
Datum.Hash(rs.nextInt()));
+                case DOUBLE: return new MaelstromKey.Key(Datum.Kind.DOUBLE, 
rs.nextDouble());
+                default: throw new AssertionError("Unknown kind: " + next);
+            }
+        };
+    }
+
+    static Gen<Range> rangeGen()
+    {
+        return AccordGens.ranges(keyGen().map(Key::toUnseekable), 
MaelstromKey.Range::new);
+    }
+
+    static Gen<RangeDeps> rangeDepsGen()
+    {
+        return AccordGens.rangeDeps(rangeGen());
+    }
+
+    static Gen<Deps> depsGen()
+    {
+        return AccordGens.deps(AccordGens.keyDeps(keyGen()), rangeDepsGen());
+    }
+
+}
\ No newline at end of file
diff --git a/accord-maelstrom/src/test/java/accord/maelstrom/Runner.java 
b/accord-maelstrom/src/test/java/accord/maelstrom/Runner.java
index f67b5bf9..70b120b7 100644
--- a/accord-maelstrom/src/test/java/accord/maelstrom/Runner.java
+++ b/accord-maelstrom/src/test/java/accord/maelstrom/Runner.java
@@ -18,32 +18,36 @@
 
 package accord.maelstrom;
 
-import java.io.BufferedReader;
 import java.io.IOException;
 import java.util.ArrayList;
 import java.util.List;
 import java.util.PriorityQueue;
-import java.util.Random;
 import java.util.concurrent.TimeUnit;
 import java.util.concurrent.atomic.AtomicLong;
-import java.util.function.Function;
 import java.util.function.Supplier;
 
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
 import accord.local.Node.Id;
 import accord.maelstrom.Cluster.Queue;
 import accord.maelstrom.Cluster.QueueSupplier;
+import accord.utils.DefaultRandom;
+import accord.utils.RandomSource;
 
 import static accord.utils.Utils.toArray;
 
 public class Runner
 {
+    private static final Logger logger = LoggerFactory.getLogger(Runner.class);
+
     static class StandardQueue<T> implements Queue<T>
     {
         static class Factory implements QueueSupplier
         {
-            final Random seeds;
+            final RandomSource seeds;
 
-            Factory(Random seeds)
+            Factory(RandomSource seeds)
             {
                 this.seeds = seeds;
             }
@@ -51,7 +55,7 @@ public class Runner
             @Override
             public <T> Queue<T> get()
             {
-                return new StandardQueue<>(new Random(seeds.nextLong()));
+                return new StandardQueue<>(seeds.fork());
             }
         }
 
@@ -78,29 +82,29 @@ public class Runner
         }
 
         final PriorityQueue<Item<T>> queue = new PriorityQueue<>();
-        final Random random;
+        final RandomSource random;
         long now;
         int seq;
 
-        StandardQueue(Random random)
+        StandardQueue(RandomSource random)
         {
             this.random = random;
         }
 
         @Override
-        public void add(T item)
+        public synchronized void add(T item)
         {
             add(item, random.nextInt(500), TimeUnit.MILLISECONDS);
         }
 
         @Override
-        public void add(T item, long delay, TimeUnit units)
+        public synchronized void add(T item, long delay, TimeUnit units)
         {
             queue.add(new Item<>(now + units.toMillis(delay), seq++, item));
         }
 
         @Override
-        public T poll()
+        public synchronized T poll()
         {
             Item<T> item = queue.poll();
             if (item == null)
@@ -110,210 +114,78 @@ public class Runner
         }
 
         @Override
-        public int size()
+        public synchronized int size()
         {
             return queue.size();
         }
     }
 
-    static class RandomQueue<T> implements Queue<T>
+    static void run(int nodeCount, QueueSupplier queueSupplier, 
Supplier<RandomSource> randomSupplier, TopologyFactory factory, String ... 
commands) throws IOException
     {
-        static class Factory implements QueueSupplier
+        run(nodeCount, queueSupplier, randomSupplier, factory, new 
Supplier<Packet>()
         {
-            final Random seeds;
-
-            Factory(Random seeds)
-            {
-                this.seeds = seeds;
-            }
-
+            int i = 0;
             @Override
-            public <T> Queue<T> get()
+            public Packet get()
             {
-                return new RandomQueue<>(new Random(seeds.nextLong()));
+                return i == commands.length ? null : 
Packet.parse(commands[i++]);
             }
-        }
-
-        static class Entry<T> implements Comparable<Entry<T>>
-        {
-            final double priority;
-            final T value;
+        });
+    }
 
-            Entry(double priority, T value)
-            {
-                this.priority = priority;
-                this.value = value;
-            }
+    static void run(int nodeCount, QueueSupplier queueSupplier, 
Supplier<RandomSource> randomSupplier, TopologyFactory factory, 
Supplier<Packet> commands) throws IOException
+    {
+        List<Id> nodes = new ArrayList<>();
+        for (int i = 1 ; i <= nodeCount ; ++i)
+            nodes.add(new Id(i));
 
-            @Override
-            public int compareTo(Entry<T> that)
-            {
-                return Double.compare(this.priority, that.priority);
-            }
-        }
+        Cluster.run(toArray(nodes, Id[]::new), queueSupplier, ignore -> {}, 
randomSupplier, () -> new AtomicLong()::incrementAndGet, factory, commands, 
System.err);
+    }
 
-        final PriorityQueue<Entry<T>> queue = new PriorityQueue<>();
-        final Random random;
+    public static Builder test()
+    {
+        return new 
Builder(Thread.currentThread().getStackTrace()[2].getMethodName());
+    }
 
-        public RandomQueue(Random random)
-        {
-            this.random = random;
-        }
+    public static class Builder
+    {
+        public static final String ACCORD_TEST_SEED = "accord.test.%s.seed";
 
-        @Override
-        public int size()
-        {
-            return queue.size();
-        }
+        private final String key;
+        private long seed;
+        private int nodeCount = 3;
+        private TopologyFactory factory = new TopologyFactory(4, 3);
 
-        @Override
-        public void add(T item)
+        private Builder(String name)
         {
-            queue.add(new Entry<>(random.nextDouble(), item));
+            key = String.format(ACCORD_TEST_SEED, name);
+            String userSeed = System.getProperty(key, null);
+            seed = userSeed != null ? Long.parseLong(userSeed) : 
System.nanoTime();
         }
 
-        @Override
-        public void add(T item, long delay, TimeUnit units)
+        Builder seed(long seed)
         {
-            queue.add(new Entry<>(random.nextDouble(), item));
+            this.seed = seed;
+            return this;
         }
 
-        @Override
-        public T poll()
+        Builder nodeCount(int nodeCount)
         {
-            return unwrap(queue.poll());
+            this.nodeCount = nodeCount;
+            return this;
         }
 
-        private static <T> T unwrap(Entry<T> e)
+        Builder factory(TopologyFactory factory)
         {
-            return e == null ? null : e.value;
+            this.factory = factory;
+            return this;
         }
-    }
-
-    static <T> Supplier<T> parseOutput(boolean delay, String output, 
Function<String, T> parse)
-    {
-        return parseOutput(delay, output.split("\n"), parse);
-    }
-
-    static <T> Supplier<T> parseOutput(boolean delay, BufferedReader output, 
Function<String, T> parse)
-    {
-        return parseOutput(delay, output.lines().toArray(String[]::new), 
parse);
-    }
 
-    static <T> Supplier<T> parseOutput(boolean delay, String[] output, 
Function<String, T> parse)
-    {
-        long[] nanos = new long[output.length];
-        String[] commands = new String[output.length];
-
-        for (int i = 0 ; i < output.length ; ++i)
+        void run(String ... commands) throws IOException
         {
-            String command = output[i];
-            long at = 
TimeUnit.MILLISECONDS.toNanos(Long.parseLong(command.substring(0, 
command.indexOf(' '))));
-            command = command.substring(command.indexOf(' ') + 1);
-            if (i > 0 && at <= nanos[i-1]) at = nanos[i-1] + 1;
-            nanos[i] = at;
-            commands[i] = command;
+            logger.info("Seed {}; rerun with -D{}={}", seed, key, seed);
+            RandomSource randomSource = new DefaultRandom(seed);
+            Runner.run(nodeCount, new StandardQueue.Factory(randomSource), 
randomSource::fork, factory, commands);
         }
-
-        long start = System.nanoTime();
-        return new Supplier<T>()
-        {
-            int i = 0;
-            @Override
-            public T get()
-            {
-                if (i == commands.length)
-                    return null;
-
-                while (delay)
-                {
-                    long wait = start + nanos[i] - System.nanoTime();
-                    if (wait <= 0) break;
-                    try
-                    {
-                        TimeUnit.NANOSECONDS.sleep(wait);
-                    }
-                    catch (InterruptedException e)
-                    {
-                        throw new IllegalStateException(e);
-                    }
-                }
-
-                return parse.apply(commands[i++]);
-            }
-        };
-    }
-
-    static void parseNode(TopologyFactory factory, boolean delay, String 
output) throws IOException
-    {
-        Main.listen(factory, parseOutput(delay, output, Function.identity()), 
System.out, System.err);
-    }
-
-    // TODO (low priority, maelstrom): we need to align response ids with the 
input; for now replies are broken
-    static void replay(int nodeCount, TopologyFactory factory, boolean delay, 
Supplier<Packet> input) throws IOException
-    {
-        run(nodeCount, new QueueSupplier()
-        {
-            @Override
-            public <T> Queue<T> get()
-            {
-                return new Queue<T>()
-                {
-                    @Override
-                    public void add(T t)
-                    {
-                    }
-
-                    @Override
-                    public void add(T item, long delay, TimeUnit units)
-                    {
-                    }
-
-                    @Override
-                    public T poll()
-                    {
-                        return (T)input.get();
-                    }
-
-                    @Override
-                    public int size()
-                    {
-                        return 0;
-                    }
-                };
-            }
-        }, Random::new, factory, () -> null);
-    }
-
-    static void run(TopologyFactory factory, String ... commands) throws 
IOException
-    {
-        run(3, factory, commands);
-    }
-
-    static void run(int nodeCount, TopologyFactory factory, String ... 
commands) throws IOException
-    {
-        run(nodeCount, new StandardQueue.Factory(new Random()), Random::new, 
factory, commands);
-    }
-
-    static void run(int nodeCount, QueueSupplier queueSupplier, 
Supplier<Random> randomSupplier, TopologyFactory factory, String ... commands) 
throws IOException
-    {
-        run(nodeCount, queueSupplier, randomSupplier, factory, new 
Supplier<Packet>()
-        {
-            int i = 0;
-            @Override
-            public Packet get()
-            {
-                return i == commands.length ? null : 
Packet.parse(commands[i++]);
-            }
-        });
-    }
-
-    static void run(int nodeCount, QueueSupplier queueSupplier, 
Supplier<Random> randomSupplier, TopologyFactory factory, Supplier<Packet> 
commands) throws IOException
-    {
-        List<Id> nodes = new ArrayList<>();
-        for (int i = 1 ; i <= nodeCount ; ++i)
-            nodes.add(new Id(i));
-
-        Cluster.run(toArray(nodes, Id[]::new), queueSupplier, ignore -> {}, 
randomSupplier, () -> new AtomicLong()::incrementAndGet, factory, commands, 
System.err);
     }
 }
diff --git 
a/accord-maelstrom/src/test/java/accord/maelstrom/SimpleRandomTest.java 
b/accord-maelstrom/src/test/java/accord/maelstrom/SimpleRandomTest.java
index 94dd163f..e7772f75 100644
--- a/accord-maelstrom/src/test/java/accord/maelstrom/SimpleRandomTest.java
+++ b/accord-maelstrom/src/test/java/accord/maelstrom/SimpleRandomTest.java
@@ -19,65 +19,51 @@
 package accord.maelstrom;
 
 import java.io.IOException;
-import java.util.Random;
 
 import org.junit.jupiter.api.Test;
 
-import accord.maelstrom.Runner.StandardQueue.Factory;
-
-import static accord.maelstrom.Runner.run;
+import static accord.maelstrom.Runner.test;
 
 public class SimpleRandomTest
 {
     @Test
     public void testLaunch() throws IOException
     {
-        run(new TopologyFactory(4, 3));
+        test().run();
     }
 
     @Test
     public void testEmptyRead() throws IOException
     {
-        run(new TopologyFactory(4, 3),
-            
"{\"src\":\"c1\",\"dest\":\"n1\",\"body\":{\"type\":\"txn\",\"msg_id\":1,\"txn\":"
-            + "[[\"r\", 1, null]]}}"
-        );
+        
test().run("{\"src\":\"c1\",\"dest\":\"n1\",\"body\":{\"type\":\"txn\",\"msg_id\":1,\"txn\":"
+                   + "[[\"r\", 1, null]]}}");
     }
 
     @Test
     public void testReadAndWrite() throws IOException
     {
-        run(new TopologyFactory(4, 3),
-            
"{\"src\":\"c1\",\"dest\":\"n1\",\"body\":{\"type\":\"txn\",\"msg_id\":1,\"txn\":"
-            + "[[\"r\", 1, null],[\"append\", 1, 1]]}}",
-            
"{\"src\":\"c1\",\"dest\":\"n1\",\"body\":{\"type\":\"txn\",\"msg_id\":1,\"txn\":"
-            + "[[\"r\", 1, null],[\"append\", 1, 2]]}}",
-            
"{\"src\":\"c1\",\"dest\":\"n1\",\"body\":{\"type\":\"txn\",\"msg_id\":1,\"txn\":"
-            + "[[\"r\", 1, null]]}}"
-        );
+        
test().run("{\"src\":\"c1\",\"dest\":\"n1\",\"body\":{\"type\":\"txn\",\"msg_id\":1,\"txn\":"
+                   + "[[\"r\", 1, null],[\"append\", 1, 1]]}}",
+                   
"{\"src\":\"c1\",\"dest\":\"n1\",\"body\":{\"type\":\"txn\",\"msg_id\":1,\"txn\":"
+                   + "[[\"r\", 1, null],[\"append\", 1, 2]]}}",
+                   
"{\"src\":\"c1\",\"dest\":\"n1\",\"body\":{\"type\":\"txn\",\"msg_id\":1,\"txn\":"
+                   + "[[\"r\", 1, null]]}}");
     }
 
     @Test
     public void testReadAndWriteRandomMultiKey() throws IOException
     {
-        Random random = new Random();
-        long seed = random.nextLong();
-        System.out.println(seed);
-        random.setSeed(seed);
-
-        run(5, new Factory(random), () -> new Random(random.nextLong()), new 
TopologyFactory(4, 3),
-            
"{\"src\":\"c1\",\"dest\":\"n1\",\"body\":{\"type\":\"txn\",\"msg_id\":1,\"txn\":"
-                + "[[\"r\", 1, null],[\"r\", 2, null],[\"r\", 3, 
null],[\"append\", 1, 1],[\"append\", 2, 3]]}}",
-            
"{\"src\":\"c1\",\"dest\":\"n2\",\"body\":{\"type\":\"txn\",\"msg_id\":2,\"txn\":"
-                + "[[\"r\", 1, null],[\"r\", 2, null],[\"r\", 3, 
null],[\"append\", 1, 2],[\"append\", 3, 3]]}}",
-            
"{\"src\":\"c1\",\"dest\":\"n3\",\"body\":{\"type\":\"txn\",\"msg_id\":3,\"txn\":"
-                + "[[\"r\", 1, null],[\"r\", 2, null],[\"r\", 3, 
null],[\"append\", 1, 3],[\"append\", 2, 2]]}}",
-            
"{\"src\":\"c1\",\"dest\":\"n1\",\"body\":{\"type\":\"txn\",\"msg_id\":4,\"txn\":"
-                + "[[\"r\", 1, null],[\"r\", 2, null],[\"r\", 3, 
null],[\"append\", 1, 4],[\"append\", 3, 2]]}}",
-            
"{\"src\":\"c1\",\"dest\":\"n2\",\"body\":{\"type\":\"txn\",\"msg_id\":5,\"txn\":"
-                + "[[\"r\", 1, null],[\"r\", 2, null],[\"r\", 3, 
null],[\"append\", 1, 5],[\"append\", 2, 1]]}}",
-            
"{\"src\":\"c1\",\"dest\":\"n3\",\"body\":{\"type\":\"txn\",\"msg_id\":6,\"txn\":"
-                + "[[\"r\", 1, null],[\"r\", 2, null],[\"r\", 3, 
null],[\"append\", 1, 6],[\"append\", 3, 1]]}}"
-        );
+        
test().nodeCount(5).run("{\"src\":\"c1\",\"dest\":\"n1\",\"body\":{\"type\":\"txn\",\"msg_id\":1,\"txn\":"
+                                + "[[\"r\", 1, null],[\"r\", 2, null],[\"r\", 
3, null],[\"append\", 1, 1],[\"append\", 2, 3]]}}",
+                                
"{\"src\":\"c1\",\"dest\":\"n2\",\"body\":{\"type\":\"txn\",\"msg_id\":2,\"txn\":"
+                                + "[[\"r\", 1, null],[\"r\", 2, null],[\"r\", 
3, null],[\"append\", 1, 2],[\"append\", 3, 3]]}}",
+                                
"{\"src\":\"c1\",\"dest\":\"n3\",\"body\":{\"type\":\"txn\",\"msg_id\":3,\"txn\":"
+                                + "[[\"r\", 1, null],[\"r\", 2, null],[\"r\", 
3, null],[\"append\", 1, 3],[\"append\", 2, 2]]}}",
+                                
"{\"src\":\"c1\",\"dest\":\"n1\",\"body\":{\"type\":\"txn\",\"msg_id\":4,\"txn\":"
+                                + "[[\"r\", 1, null],[\"r\", 2, null],[\"r\", 
3, null],[\"append\", 1, 4],[\"append\", 3, 2]]}}",
+                                
"{\"src\":\"c1\",\"dest\":\"n2\",\"body\":{\"type\":\"txn\",\"msg_id\":5,\"txn\":"
+                                + "[[\"r\", 1, null],[\"r\", 2, null],[\"r\", 
3, null],[\"append\", 1, 5],[\"append\", 2, 1]]}}",
+                                
"{\"src\":\"c1\",\"dest\":\"n3\",\"body\":{\"type\":\"txn\",\"msg_id\":6,\"txn\":"
+                                + "[[\"r\", 1, null],[\"r\", 2, null],[\"r\", 
3, null],[\"append\", 1, 6],[\"append\", 3, 1]]}}");
     }
 }
diff --git a/buildSrc/src/main/groovy/accord.java-conventions.gradle 
b/buildSrc/src/main/groovy/accord.java-conventions.gradle
index 3ca1d577..2465a8bc 100644
--- a/buildSrc/src/main/groovy/accord.java-conventions.gradle
+++ b/buildSrc/src/main/groovy/accord.java-conventions.gradle
@@ -44,6 +44,8 @@ dependencies {
     testImplementation 'org.junit.jupiter:junit-jupiter-api:5.7.0'
     testImplementation 'ch.qos.logback:logback-classic:1.2.3'
     testRuntimeOnly 'org.junit.jupiter:junit-jupiter-engine:5.7.0'
+
+    testImplementation group: 'org.assertj', name: 'assertj-core', version: 
'3.24.2'
 }
 
 task copyMainDependencies(type: Copy) {


---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]

Reply via email to