You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@cassandra.apache.org by if...@apache.org on 2023/12/23 14:16:27 UTC

(cassandra) branch trunk updated: Bring Harry into C* Tree

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

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


The following commit(s) were added to refs/heads/trunk by this push:
     new 439d1b122a Bring Harry into C* Tree
439d1b122a is described below

commit 439d1b122af334bf68c159b82ef4e4879c210bd5
Author: Alex Petrov <ol...@gmail.com>
AuthorDate: Sun Dec 10 18:30:26 2023 +0100

    Bring Harry into C* Tree
    
    Patch by Alex Petrov; reviewed by Caleb Rackliffe and Marcus Eriksson for CASSANDRA-19210.
---
 .build/cassandra-build-deps-template.xml           |    4 -
 .build/parent-pom-template.xml                     |    6 -
 build.xml                                          |    2 +
 ide/idea-iml-file.xml                              |    1 +
 ide/nbproject/project.xml                          |   13 +-
 .../config/CassandraRelevantProperties.java        |    1 +
 test/conf/harry-generic.yaml                       |   92 --
 .../ConcurrentQuiescentCheckerIntegrationTest.java |  123 --
 .../cassandra/distributed/fuzz/FuzzTestBase.java   |  132 --
 .../fuzz/InJVMTokenAwareVisitorExecutor.java       |  111 --
 .../distributed/fuzz/QueryingNoOpChecker.java      |   66 -
 .../distributed/fuzz/SSTableGenerator.java         |  363 ------
 .../distributed/fuzz/SSTableLoadingVisitor.java    |  113 --
 .../fuzz/test/SSTableGeneratorTest.java            |  131 --
 .../cassandra/distributed/shared/ClusterUtils.java |    7 +-
 .../distributed/test/log/BounceGossipTest.java     |   41 +-
 .../distributed/test/log/CMSTestBase.java          |    5 +-
 .../distributed/test/log/CoordinatorPathTest.java  |   32 +-
 .../test/log/CoordinatorPathTestBase.java          |   26 +-
 .../distributed/test/log/FailedLeaveTest.java      |   38 +-
 .../distributed/test/log/FuzzTestBase.java         |   96 +-
 .../test/log/MetadataChangeSimulationTest.java     |   86 +-
 .../cassandra/distributed/test/log/ModelState.java |   76 +-
 .../test/log/OperationalEquivalenceTest.java       |   43 +-
 .../distributed/test/log/PlacementSimulator.java   |  765 +-----------
 .../test/log/PlacementSimulatorTest.java           |   33 +-
 .../distributed/test/log/ResumableStartupTest.java |   47 +-
 .../distributed/test/log/SimulatedOperation.java   |    6 +-
 .../test/log/SystemKeyspaceStorageTest.java        |    3 +-
 .../harry/examples/RangeTombstoneBurnTest.java     |  144 +++
 .../fuzz/harry/gen/DataGeneratorsTest.java         |  504 ++++++++
 .../fuzz/harry/gen/EntropySourceTest.java          |  158 +++
 .../harry/gen/ExtensionsTest.java}                 |   35 +-
 .../cassandra/fuzz/harry/gen/SurjectionsTest.java  |   63 +
 .../harry/integration/QuickTheoriesAdapter.java    |  100 ++
 .../fuzz/harry/integration/ddl/SchemaGenTest.java  |  216 ++++
 .../dsl/HistoryBuilderIntegrationTest.java         |  148 +++
 .../generators/DataGeneratorsIntegrationTest.java  |  179 +++
 .../ConcurrentQuiescentCheckerIntegrationTest.java |   71 ++
 .../model/InJVMTokenAwareExecutorTest.java         |   98 ++
 .../integration/model/IntegrationTestBase.java     |  116 ++
 .../fuzz/harry/integration/model/MockSchema.java   |  117 ++
 .../harry/integration/model/ModelTestBase.java     |  138 +++
 .../model/QuerySelectorNegativeTest.java           |  157 +++
 .../harry/integration/model/QuerySelectorTest.java |  163 +++
 .../model/QuiescentCheckerIntegrationTest.java     |  224 ++++
 .../QuiescentLocalStateCheckerIntegrationTest.java |   79 ++
 .../model/ReconcilerIntegrationTest.java           |   94 ++
 .../model/reconciler/SimpleReconcilerTest.java     |  341 ++++++
 .../fuzz/harry/integration/op/RowVisitorTest.java  |   90 ++
 .../fuzz/harry/model/ApproximateClockTest.java     |  180 +++
 .../fuzz/harry/model/OpSelectorsTest.java          |  418 +++++++
 .../fuzz/harry/operations/RelationTest.java        |  204 ++++
 .../fuzz/harry/runner/LockingDataTrackerTest.java  |  133 +++
 .../cassandra/fuzz/harry/util/BitSetTest.java      |   66 +
 .../cassandra/fuzz/harry/util/RangesTest.java      |  102 ++
 .../ring}/ClusterMetadataUpgradeHarryTest.java     |   55 +-
 .../ring/ConsistentBootstrapTest.java              |  142 +--
 .../log => fuzz/ring}/ConsistentLeaveTest.java     |   63 +-
 .../test/log => fuzz/ring}/ConsistentMoveTest.java |   67 +-
 test/harry/main/README.md                          |  647 ++++++++++
 .../org/apache/cassandra/harry}/HarryHelper.java   |   52 +-
 .../cassandra/harry/checker/ModelChecker.java      |  230 ++++
 .../cassandra/harry/clock/ApproximateClock.java    |  279 +++++
 .../apache/cassandra/harry/clock/OffsetClock.java  |   94 ++
 .../apache/cassandra/harry/core/Configuration.java | 1255 ++++++++++++++++++++
 .../cassandra/harry/core/MetricReporter.java       |   51 +
 .../main/org/apache/cassandra/harry/core/Run.java  |   75 ++
 .../harry/corruptor/AddExtraRowCorruptor.java      |   90 ++
 .../harry/corruptor/ChangeValueCorruptor.java      |   85 ++
 .../harry/corruptor/HideRowCorruptor.java          |   49 +
 .../harry/corruptor/HideValueCorruptor.java        |  109 ++
 .../harry/corruptor/QueryResponseCorruptor.java    |  108 ++
 .../cassandra/harry/corruptor/RowCorruptor.java    |   56 +
 .../harry/corruptor/ShowValueCorruptor.java        |   77 ++
 .../apache/cassandra/harry/data/ResultSetRow.java  |  137 +++
 .../org/apache/cassandra/harry/ddl/ColumnSpec.java |  438 +++++++
 .../cassandra/harry/ddl/SchemaGenerators.java      |  512 ++++++++
 .../org/apache/cassandra/harry/ddl/SchemaSpec.java |  492 ++++++++
 .../harry/dsl/BatchOperationBuilder.java}          |   14 +-
 .../cassandra/harry/dsl/BatchVisitBuilder.java     |  138 +++
 .../apache/cassandra/harry/dsl/HistoryBuilder.java |  598 ++++++++++
 .../cassandra/harry/dsl/PartitionVisitState.java   |   35 +
 .../harry/dsl/ReplayingHistoryBuilder.java         |  117 ++
 .../harry/dsl/SingleOperationBuilder.java          |   53 +
 .../harry/dsl/SingleOperationVisitBuilder.java     |  295 +++++
 .../harry/dsl/ValueDescriptorIndexGenerator.java   |   78 ++
 .../org/apache/cassandra/harry/gen/Bijections.java |  457 +++++++
 .../cassandra/harry/gen/BooleanGenerator.java      |   29 +
 .../main/org/apache/cassandra/harry/gen/Bytes.java |   42 +
 .../apache/cassandra/harry/gen/Collections.java    |  249 ++++
 .../apache/cassandra/harry/gen/DataGenerators.java |  499 ++++++++
 .../apache/cassandra/harry/gen/EntropySource.java  |   95 ++
 .../org/apache/cassandra/harry/gen/Generator.java  |  133 +++
 .../org/apache/cassandra/harry/gen/Generators.java |   86 ++
 .../cassandra/harry/gen/StringBijection.java       |  172 +++
 .../apache/cassandra/harry/gen/Surjections.java    |  170 +++
 .../harry/gen/distribution/Distribution.java       |  136 +++
 .../harry/gen/rng/JdkRandomEntropySource.java      |   83 ++
 .../cassandra/harry/gen/rng/PCGFastPure.java       |  145 +++
 .../apache/cassandra/harry/gen/rng/PcgRSUFast.java |  133 +++
 .../apache/cassandra/harry/gen/rng/RngUtils.java   |  172 +++
 .../cassandra/harry/model/AgainstSutChecker.java   |   99 ++
 .../harry/model/AlwaysSamePartitionSelector.java   |   92 ++
 .../org/apache/cassandra/harry/model/Model.java    |   47 +
 .../apache/cassandra/harry/model/NoOpChecker.java  |   40 +
 .../apache/cassandra/harry/model/OpSelectors.java  |  903 ++++++++++++++
 .../harry/model/QueryingNoOpValidator.java         |   65 +
 .../cassandra/harry/model/QuiescentChecker.java    |  296 +++++
 .../model/QuiescentLocalStateCheckerBase.java      |  123 ++
 .../harry/model/RepairingLocalStateValidator.java  |   92 ++
 .../apache/cassandra/harry/model/SelectHelper.java |  337 ++++++
 .../harry/model/reconciler/PartitionState.java     |  311 +++++
 .../harry/model/reconciler/Reconciler.java         |  331 ++++++
 .../harry/model/ring/PlacementSimulator.java       | 1052 ++++++++++++++++
 .../harry/operations/CompiledStatement.java        |   88 ++
 .../cassandra/harry/operations/DeleteHelper.java   |  190 +++
 .../cassandra/harry/operations/FilteringQuery.java |   68 ++
 .../apache/cassandra/harry/operations/Query.java   |  501 ++++++++
 .../cassandra/harry/operations/QueryGenerator.java |  176 +++
 .../cassandra/harry/operations/Relation.java       |  299 +++++
 .../cassandra/harry/operations/WriteHelper.java    |  166 +++
 .../cassandra/harry/runner/EarlyExitException.java |   27 +
 .../cassandra/harry/runner}/FlaggedRunner.java     |   19 +-
 .../apache/cassandra/harry/runner/HarryRunner.java |  115 ++
 .../org/apache/cassandra/harry/runner/Runner.java  |  479 ++++++++
 .../cassandra/harry/runner/StagedRunner.java       |   92 ++
 .../cassandra/harry/runner/TrivialShrinker.java    |  189 +++
 .../cassandra/harry/runner/UpToLtsRunner.java      |  111 ++
 .../cassandra/harry/sut/DoubleWritingSut.java      |   60 +
 .../org/apache/cassandra/harry/sut/PrintlnSut.java |   67 ++
 .../cassandra/harry/sut/QueryModifyingSut.java     |   80 ++
 .../cassandra/harry/sut/SystemUnderTest.java       |  105 ++
 .../cassandra/harry/sut/TokenPlacementModel.java   |  891 ++++++++++++++
 .../cassandra/harry/sut/injvm}/ClusterState.java   |    2 +-
 .../harry/sut/injvm}/ExistingClusterSUT.java       |    6 +-
 .../sut/injvm/InJVMTokenAwareVisitExecutor.java    |  163 +++
 .../cassandra/harry/sut/injvm}/InJvmSut.java       |   16 +-
 .../cassandra/harry/sut/injvm}/InJvmSutBase.java   |   50 +-
 .../sut/injvm/QuiescentLocalStateChecker.java      |   87 ++
 .../cassandra/harry/tracker/DataTracker.java       |   66 +
 .../harry/tracker/DefaultDataTracker.java          |  193 +++
 .../harry/tracker/LockingDataTracker.java          |  286 +++++
 .../org/apache/cassandra/harry/util/BitSet.java    |  244 ++++
 .../org/apache/cassandra/harry/util/ByteUtils.java |  162 +++
 .../cassandra/harry/util/DescriptorRanges.java     |  150 +++
 .../apache/cassandra/harry/util/StringUtils.java   |   35 +
 .../apache/cassandra/harry/util/TestRunner.java    |  106 ++
 .../cassandra/harry/util/ThrowingRunnable.java     |   33 +
 .../org/apache/cassandra/harry/util/TokenUtil.java |  150 +++
 .../harry/visitors/AllPartitionsValidator.java     |  150 +++
 .../harry/visitors/CorruptingVisitor.java          |   89 ++
 .../harry/visitors/DoubleWriteVisitExecutor.java   |   56 +
 .../harry/visitors/FaultInjectingVisitor.java      |   97 ++
 .../harry/visitors/GeneratingVisitor.java          |  232 ++++
 .../cassandra/harry/visitors/LoggingVisitor.java   |   85 ++
 .../cassandra/harry/visitors/LtsVisitor.java       |   83 ++
 .../harry/visitors/MutatingRowVisitor.java         |  115 ++
 .../cassandra/harry/visitors/MutatingVisitor.java  |  220 ++++
 .../harry/visitors/OperationExecutor.java          |   78 ++
 .../harry/visitors/ParallelRecentValidator.java    |  149 +++
 .../harry/visitors/ParallelValidator.java          |  103 ++
 .../cassandra/harry/visitors/QueryLogger.java      |   97 ++
 .../harry/visitors/RandomPartitionValidator.java   |   58 +
 .../cassandra/harry/visitors/RandomValidator.java  |   93 ++
 .../cassandra/harry/visitors/RecentValidator.java  |   96 ++
 .../cassandra/harry/visitors/ReplayingVisitor.java |   73 ++
 .../apache/cassandra/harry/visitors/Sampler.java   |   97 ++
 .../cassandra/harry/visitors/SingleValidator.java  |   66 +
 .../cassandra/harry/visitors/SkippingVisitor.java  |   56 +
 .../cassandra/harry/visitors/VisitExecutor.java    |  122 ++
 .../apache/cassandra/harry/visitors/Visitor.java   |   33 +
 .../simulator/AlwaysDeliverNetworkScheduler.java   |    6 +-
 .../simulator/cluster/KeyspaceActions.java         |   50 +-
 .../simulator/harry/HarryValidatingQuery.java      |   18 +-
 .../simulator/test/HarrySimulatorTest.java         |   57 +-
 .../simulator/test/SimulatedVisitExectuor.java     |   29 +-
 .../apache/cassandra/tcm/ClusterMetadataTest.java  |    4 +-
 .../UniformRangePlacementIntegrationTest.java      |   14 +-
 .../tcm/sequences/ProgressBarrierTest.java         |   46 +-
 180 files changed, 25046 insertions(+), 2578 deletions(-)

diff --git a/.build/cassandra-build-deps-template.xml b/.build/cassandra-build-deps-template.xml
index b620cda8f5..9f9a70000f 100644
--- a/.build/cassandra-build-deps-template.xml
+++ b/.build/cassandra-build-deps-template.xml
@@ -106,10 +106,6 @@
       <groupId>org.apache.ant</groupId>
       <artifactId>ant-junit</artifactId>
     </dependency>
-    <dependency>
-      <groupId>org.apache.cassandra</groupId>
-      <artifactId>harry-core</artifactId>
-    </dependency>
     <dependency>
       <groupId>org.junit</groupId>
       <artifactId>junit-bom</artifactId>
diff --git a/.build/parent-pom-template.xml b/.build/parent-pom-template.xml
index 4b55268bfd..5440085c97 100644
--- a/.build/parent-pom-template.xml
+++ b/.build/parent-pom-template.xml
@@ -513,12 +513,6 @@
           </exclusion>
         </exclusions>
       </dependency>
-      <dependency>
-        <groupId>org.apache.cassandra</groupId>
-        <artifactId>harry-core</artifactId>
-        <version>0.0.2</version>
-        <scope>test</scope>
-      </dependency>
       <dependency>
         <groupId>org.reflections</groupId>
         <artifactId>reflections</artifactId>
diff --git a/build.xml b/build.xml
index 162c137984..79fdbda8d9 100644
--- a/build.xml
+++ b/build.xml
@@ -87,6 +87,7 @@
     <property name="test.simulator-asm.src" value="${test.dir}/simulator/asm"/>
     <property name="test.simulator-bootstrap.src" value="${test.dir}/simulator/bootstrap"/>
     <property name="test.simulator-test.src" value="${test.dir}/simulator/test"/>
+    <property name="test.harry.src" value="${test.dir}/harry/main"/>
     <property name="test.driver.connection_timeout_ms" value="10000"/>
     <property name="test.driver.read_timeout_ms" value="24000"/>
     <property name="test.jvm.args" value="" />
@@ -1043,6 +1044,7 @@
      <src path="${test.simulator-asm.src}"/>
      <src path="${test.simulator-bootstrap.src}"/>
      <src path="${test.simulator-test.src}"/>
+     <src path="${test.harry.src}"/>
      <compilerarg line="${jdk11plus-javac-exports}"/>
     </javac>
 
diff --git a/ide/idea-iml-file.xml b/ide/idea-iml-file.xml
index 01c071a728..13e66fa613 100644
--- a/ide/idea-iml-file.xml
+++ b/ide/idea-iml-file.xml
@@ -41,6 +41,7 @@
             <sourceFolder url="file://$MODULE_DIR$/test/simulator/bootstrap" isTestSource="true" />
             <sourceFolder url="file://$MODULE_DIR$/test/simulator/main" isTestSource="true" />
             <sourceFolder url="file://$MODULE_DIR$/test/simulator/test" isTestSource="true" />
+            <sourceFolder url="file://$MODULE_DIR$/test/harry/main" isTestSource="true" />
             <sourceFolder url="file://$MODULE_DIR$/test/resources" type="java-test-resource" />
             <sourceFolder url="file://$MODULE_DIR$/test/conf" type="java-test-resource" />
             <excludeFolder url="file://$MODULE_DIR$/.idea" />
diff --git a/ide/nbproject/project.xml b/ide/nbproject/project.xml
index 3250cb78db..ea9a486aa5 100644
--- a/ide/nbproject/project.xml
+++ b/ide/nbproject/project.xml
@@ -7,7 +7,7 @@
             <properties>
                 <property name="project.dir">..</property>
                 <!-- the compile classpaths should be distinct per compilation unit… but it is kept simple and the build will catch errors -->
-                <property name="cassandra.classpath.jars">${project.dir}/build/lib/jars/AmazonCorrettoCryptoProvider-2.2.0-linux-x86_64.jar:${project.dir}/build/lib/jars/HdrHistogram-2.1.9.jar:${project.dir}/build/lib/jars/ST4-4.0.8.jar:${project.dir}/build/lib/jars/affinity-3.23.3.jar:${project.dir}/build/lib/jars/agrona-1.17.1.jar:${project.dir}/build/lib/jars/airline-0.8.jar:${project.dir}/build/lib/jars/antlr-3.5.2.jar:${project.dir}/build/lib/jars/antlr-runtime-3.5.2.jar:${project.d [...]
+                <property name="cassandra.classpath.jars">${project.dir}/build/lib/jars/AmazonCorrettoCryptoProvider-2.2.0-linux-x86_64.jar:${project.dir}/build/lib/jars/HdrHistogram-2.1.9.jar:${project.dir}/build/lib/jars/ST4-4.0.8.jar:${project.dir}/build/lib/jars/affinity-3.23.3.jar:${project.dir}/build/lib/jars/agrona-1.17.1.jar:${project.dir}/build/lib/jars/airline-0.8.jar:${project.dir}/build/lib/jars/antlr-3.5.2.jar:${project.dir}/build/lib/jars/antlr-runtime-3.5.2.jar:${project.d [...]
             </properties>
             <folders>
                 <source-folder>
@@ -112,6 +112,12 @@
                     <location>${project.dir}/test/simulator/test</location>
                     <encoding>UTF-8</encoding>
                 </source-folder>
+                <source-folder>
+                    <label>test/fuzz/main</label>
+                    <type>java</type>
+                    <location>${project.dir}/test/harry/main</location>
+                    <encoding>UTF-8</encoding>
+                </source-folder>
                 <source-folder>
                     <label>apache-cassandra</label>
                     <location>${project.dir}</location>
@@ -259,6 +265,10 @@
                         <label>test/simulator/test</label>
                         <location>${project.dir}/test/simulator/test</location>
                     </source-folder>
+                    <source-folder style="packages">
+                        <label>test/harry/main</label>
+                        <location>${project.dir}/test/harry/main</location>
+                    </source-folder>
                     <source-file>
                         <location>${project.dir}/build.xml</location>
                     </source-file>
@@ -300,6 +310,7 @@
                 <package-root>${project.dir}/test/simulator/bootstrap</package-root>
                 <package-root>${project.dir}/test/simulator/main</package-root>
                 <package-root>${project.dir}/test/simulator/test</package-root>
+                <package-root>${project.dir}/test/harry/main</package-root>
                 <unit-tests/>
                 <classpath mode="compile">${cassandra.classpath.jars}:${project.dir}/build/classes/main:${project.dir}/build/classes/fqltool/:${project.dir}/build/classes/stress/</classpath>
                 <built-to>${project.dir}/build/test/classes</built-to>
diff --git a/src/java/org/apache/cassandra/config/CassandraRelevantProperties.java b/src/java/org/apache/cassandra/config/CassandraRelevantProperties.java
index db18c45eb8..5f9289e048 100644
--- a/src/java/org/apache/cassandra/config/CassandraRelevantProperties.java
+++ b/src/java/org/apache/cassandra/config/CassandraRelevantProperties.java
@@ -566,6 +566,7 @@ public enum CassandraRelevantProperties
      * faster. Note that this is disabled for unit tests but if an individual test requires schema to be flushed, it
      * can be also done manually for that particular case: {@code flush(SchemaConstants.SCHEMA_KEYSPACE_NAME);}. */
     TEST_FLUSH_LOCAL_SCHEMA_CHANGES("cassandra.test.flush_local_schema_changes", "true"),
+    TEST_HARRY_SWITCH_AFTER("cassandra.test.harry.progression.switch-after", "1"),
     TEST_IGNORE_SIGAR("cassandra.test.ignore_sigar"),
     TEST_INVALID_LEGACY_SSTABLE_ROOT("invalid-legacy-sstable-root"),
     TEST_JVM_DTEST_DISABLE_SSL("cassandra.test.disable_ssl"),
diff --git a/test/conf/harry-generic.yaml b/test/conf/harry-generic.yaml
deleted file mode 100644
index a0a34bbb30..0000000000
--- a/test/conf/harry-generic.yaml
+++ /dev/null
@@ -1,92 +0,0 @@
-# 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.
-#
-#
-seed: 1
-
-# Default schema provider generates random schema
-schema_provider:
-  default: {}
-
-drop_schema: false
-create_schema: true
-truncate_table: false
-
-clock:
-  approximate_monotonic:
-    history_size: 7300
-    epoch_length: 1
-    epoch_time_unit: "SECONDS"
-
-system_under_test:
-  println: {}
-
-partition_descriptor_selector:
-  default:
-    window_size: 100
-    slide_after_repeats: 10
-
-clustering_descriptor_selector:
-  default:
-    modifications_per_lts:
-      type: "constant"
-      constant: 2
-    rows_per_modification:
-      type: "constant"
-      constant: 2
-    operation_kind_weights:
-      DELETE_RANGE: 1
-      DELETE_SLICE: 1
-      DELETE_ROW: 1
-      DELETE_COLUMN: 1
-      DELETE_PARTITION: 1
-      INSERT: 50
-      UPDATE: 50
-      DELETE_COLUMN_WITH_STATICS: 1
-      INSERT_WITH_STATICS: 1
-      UPDATE_WITH_STATICS: 1
-    column_mask_bitsets: null
-    max_partition_size: 100
-
-data_tracker:
-  default:
-    max_seen_lts: -1
-    max_complete_lts: -1
-
-runner:
-  sequential:
-    run_time: 60
-    run_time_unit: "MINUTES"
-    visitors:
-      - logging:
-          row_visitor:
-            mutating: {}
-      - sampler:
-          trigger_after: 100000
-          sample_partitions: 10
-      - validate_recent_partitions:
-          partition_count: 5
-          trigger_after: 10000
-          model:
-            querying_no_op_checker: {}
-      - validate_all_partitions:
-          concurrency: 5
-          trigger_after: 10000
-          model:
-            querying_no_op_checker: {}
-
-metric_reporter:
-  no_op: {}
\ No newline at end of file
diff --git a/test/distributed/org/apache/cassandra/distributed/fuzz/ConcurrentQuiescentCheckerIntegrationTest.java b/test/distributed/org/apache/cassandra/distributed/fuzz/ConcurrentQuiescentCheckerIntegrationTest.java
deleted file mode 100644
index 8c43b46a84..0000000000
--- a/test/distributed/org/apache/cassandra/distributed/fuzz/ConcurrentQuiescentCheckerIntegrationTest.java
+++ /dev/null
@@ -1,123 +0,0 @@
-/*
- * 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 org.apache.cassandra.distributed.fuzz;
-
-import java.io.File;
-import java.util.concurrent.TimeUnit;
-import java.util.function.Supplier;
-
-import org.junit.Test;
-
-import harry.core.Configuration;
-import harry.core.Run;
-import harry.ddl.SchemaSpec;
-import harry.generators.Surjections;
-import harry.model.sut.injvm.InJvmSut;
-import harry.runner.LockingDataTracker;
-import harry.runner.Runner;
-import harry.visitors.MutatingVisitor;
-import harry.visitors.QueryLogger;
-import harry.visitors.RandomPartitionValidator;
-import org.apache.cassandra.distributed.api.ICluster;
-import org.apache.cassandra.distributed.api.IInvokableInstance;
-import org.apache.cassandra.distributed.test.TestBaseImpl;
-
-import static harry.core.Configuration.VisitorPoolConfiguration.pool;
-import static java.util.Arrays.asList;
-import static org.apache.cassandra.distributed.api.Feature.GOSSIP;
-import static org.apache.cassandra.distributed.api.Feature.NATIVE_PROTOCOL;
-import static org.apache.cassandra.distributed.api.Feature.NETWORK;
-
-public class ConcurrentQuiescentCheckerIntegrationTest extends TestBaseImpl
-{
-
-    private static Supplier<SchemaSpec> schemaSupplier()
-    {
-        Surjections.Surjection<SchemaSpec> schemaGen = HarryHelper.schemaSpecGen("harry", "table_");
-        return new Supplier<SchemaSpec>()
-        {
-            int schemaIdx = 0;
-            public SchemaSpec get()
-            {
-                while (true)
-                {
-                    SchemaSpec schema = schemaGen.inflate(schemaIdx++);
-                    try
-                    {
-                        schema.validate();
-                        return schema;
-                    }
-                    catch (AssertionError e)
-                    {
-                        continue;
-                    }
-                }
-            }
-        };
-    }
-
-    @Test
-    public void testWithConcurrentQuiescentChecker() throws Throwable
-    {
-        final int writeThreads = 2;
-        final int readThreads = 2;
-
-        try (ICluster<IInvokableInstance> cluster = builder().withNodes(3)
-                                                       .withConfig(config -> config.with(GOSSIP, NETWORK, NATIVE_PROTOCOL))
-                                                       .start())
-        {
-            Supplier<SchemaSpec> schemaSupplier = schemaSupplier();
-
-            for (int i = 0; i < 3; i++)
-            {
-                SchemaSpec schema = schemaSupplier.get();
-                Configuration config = HarryHelper
-                                       .defaultConfiguration()
-                                       .setSeed(1L)
-                                       .setClock(new Configuration.ApproximateMonotonicClockConfiguration((int) TimeUnit.MINUTES.toSeconds(20), 1, TimeUnit.SECONDS))
-                                       .setSUT(() -> new InJvmSut(cluster))
-                                       .setKeyspaceDdl("CREATE KEYSPACE IF NOT EXISTS " + schema.keyspace + " WITH replication = {'class': 'SimpleStrategy', 'replication_factor': 3};")
-                                       .setSchemaProvider((seed, sut) -> schema)
-                                       .setCreateSchema(true)
-                                       .setDropSchema(true)
-                                       .setDataTracker(LockingDataTracker::new)
-                                       .build();
-
-                Run run = config.createRun();
-                try
-                {
-                    new Runner.ConcurrentRunner(config.createRun(), config,
-                                                asList(pool("Writer", writeThreads, MutatingVisitor::new),
-                                                       pool("Reader", readThreads, (run_) -> new RandomPartitionValidator(run_, new Configuration.QuiescentCheckerConfig(), QueryLogger.NO_OP))),
-                                                1, TimeUnit.MINUTES)
-                    .run();
-                }
-                catch (Throwable e)
-                {
-                    Configuration.toFile(new File(String.format("failure-%d.yaml", config.seed)),
-                                         config.unbuild()
-                                               .setClock(run.clock.toConfig())
-                                               .setDataTracker(run.tracker.toConfig())
-                                               .build());
-                    throw e;
-                }
-            }
-        }
-    }
-}
\ No newline at end of file
diff --git a/test/distributed/org/apache/cassandra/distributed/fuzz/FuzzTestBase.java b/test/distributed/org/apache/cassandra/distributed/fuzz/FuzzTestBase.java
deleted file mode 100644
index 13e4f8ce64..0000000000
--- a/test/distributed/org/apache/cassandra/distributed/fuzz/FuzzTestBase.java
+++ /dev/null
@@ -1,132 +0,0 @@
-/*
- * 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 org.apache.cassandra.distributed.fuzz;
-
-import java.util.Collection;
-
-import org.junit.AfterClass;
-import org.junit.BeforeClass;
-
-import harry.core.Configuration;
-import harry.core.Run;
-import harry.ddl.SchemaSpec;
-import harry.model.clock.OffsetClock;
-import org.apache.cassandra.Util;
-import org.apache.cassandra.db.ColumnFamilyStore;
-import org.apache.cassandra.db.Keyspace;
-import org.apache.cassandra.distributed.Cluster;
-import org.apache.cassandra.distributed.test.TestBaseImpl;
-import org.apache.cassandra.io.sstable.format.SSTableReader;
-
-public abstract class FuzzTestBase extends TestBaseImpl
-{
-    protected static Configuration configuration;
-    public static final int RF = 2;
-    static
-    {
-        try
-        {
-            HarryHelper.init();
-            configuration = HarryHelper.defaultConfiguration().build();
-        }
-        catch (Exception e)
-        {
-            throw new RuntimeException(e);
-        }
-    }
-
-    protected static Cluster cluster;
-
-    @BeforeClass
-    public static void beforeClassOverride() throws Throwable
-    {
-        cluster = Cluster.build(2)
-                         .start();
-
-        init(cluster, RF);
-    }
-
-    @AfterClass
-    public static void afterClass()
-    {
-        if (cluster != null)
-            cluster.close();
-    }
-
-    public void reset()
-    {
-        cluster.schemaChange("DROP KEYSPACE IF EXISTS " + KEYSPACE);
-        init(cluster, RF);
-    }
-
-    /**
-     * Helped method to generate a {@code number} of sstables for the given {@code schemaSpec}.
-     */
-    @SuppressWarnings("unused")
-    public static void generateTables(SchemaSpec schemaSpec, int number)
-    {
-        Run run = configuration.unbuild()
-                               .setSeed(1)
-                               .setSchemaProvider(new FixedSchemaProviderConfiguration(schemaSpec))
-                               .setClock(() -> new OffsetClock(10000L))
-                               .setDropSchema(false)
-                               .setCreateSchema(false)
-                               .build()
-                               .createRun();
-
-        ColumnFamilyStore store = Keyspace.open(schemaSpec.keyspace).getColumnFamilyStore(schemaSpec.table);
-        store.disableAutoCompaction();
-
-        SSTableGenerator gen = new SSTableGenerator(run, store);
-        SSTableGenerator largePartitionGen = new SSTableWithLargePartition(run, store);
-        for (int i = 0; i < number; i++)
-        {
-            if (i % 3 == 0)
-                largePartitionGen.gen(1_000);
-            else
-                gen.gen(1_000);
-        }
-    }
-
-    /**
-     * Helper class to force generation of a fixed partition size.
-     */
-    private static class SSTableWithLargePartition extends SSTableGenerator
-    {
-        public SSTableWithLargePartition(Run run, ColumnFamilyStore store)
-        {
-            super(run, store);
-        }
-
-        @Override
-        public Collection<SSTableReader> gen(int rows)
-        {
-            long lts = 0;
-            for (int i = 0; i < rows; i++)
-            {
-                long current = lts++;
-                write(current, current, current, current, true).applyUnsafe();
-                if (schema.staticColumns != null)
-                    writeStatic(current, 0, current, current, true).applyUnsafe();
-            }
-            Util.flush(store);
-            return null;
-        }
-    }
-}
diff --git a/test/distributed/org/apache/cassandra/distributed/fuzz/InJVMTokenAwareVisitorExecutor.java b/test/distributed/org/apache/cassandra/distributed/fuzz/InJVMTokenAwareVisitorExecutor.java
deleted file mode 100644
index 05e48ef25a..0000000000
--- a/test/distributed/org/apache/cassandra/distributed/fuzz/InJVMTokenAwareVisitorExecutor.java
+++ /dev/null
@@ -1,111 +0,0 @@
-/*
- * 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 org.apache.cassandra.distributed.fuzz;
-
-import java.util.concurrent.CompletableFuture;
-import java.util.concurrent.TimeUnit;
-
-import com.fasterxml.jackson.annotation.JsonCreator;
-import com.fasterxml.jackson.annotation.JsonProperty;
-import com.fasterxml.jackson.annotation.JsonTypeName;
-import harry.core.Run;
-import harry.ddl.SchemaSpec;
-import harry.model.sut.SystemUnderTest;
-import harry.operations.CompiledStatement;
-import harry.visitors.*;
-import org.apache.cassandra.distributed.test.log.RngUtils;
-
-public class InJVMTokenAwareVisitorExecutor extends LoggingVisitor.LoggingVisitorExecutor
-{
-    public static void init()
-    {
-        harry.core.Configuration.registerSubtypes(Configuration.class);
-    }
-
-    private final InJvmSut sut;
-    private final SystemUnderTest.ConsistencyLevel cl;
-    private final SchemaSpec schema;
-    private final int maxRetries = 10;
-
-    // TODO: as of now, token aware executor only performs operations against natrual, but not pending replicas.
-    public InJVMTokenAwareVisitorExecutor(Run run,
-                                          OperationExecutor.RowVisitorFactory rowVisitorFactory,
-                                          SystemUnderTest.ConsistencyLevel cl)
-    {
-        super(run, rowVisitorFactory.make(run));
-        this.sut = (InJvmSut) run.sut;
-        this.schema = run.schemaSpec;
-        this.cl = cl;
-    }
-
-    @Override
-    protected void executeAsyncWithRetries(long lts, long pd, CompletableFuture<Object[][]> future, CompiledStatement statement)
-    {
-        executeAsyncWithRetries(lts, pd, future, statement, 0);
-    }
-
-    private void executeAsyncWithRetries(long lts, long pd, CompletableFuture<Object[][]> future, CompiledStatement statement, int retries)
-    {
-        if (sut.isShutdown())
-            throw new IllegalStateException("System under test is shut down");
-
-        if (retries > this.maxRetries)
-            throw new IllegalStateException(String.format("Can not execute statement %s after %d retries", statement, retries));
-
-        Object[] partitionKey =  schema.inflatePartitionKey(pd);
-        int[] replicas = sut.getReadReplicasFor(partitionKey, schema.keyspace, schema.table);
-        int replica = replicas[RngUtils.asInt(lts, 0, replicas.length - 1)];
-        if (cl == SystemUnderTest.ConsistencyLevel.NODE_LOCAL)
-        {
-            future.complete(sut.cluster.get(replica).executeInternal(statement.cql(), statement.bindings()));
-        }
-        else
-        {
-            CompletableFuture.supplyAsync(() -> sut.cluster.coordinator(replica).execute(statement.cql(), InJvmSut.toApiCl(cl), statement.bindings()), executor)
-                             .whenComplete((res, t) ->
-                                           {
-                                               if (t != null)
-                                                   executor.schedule(() -> executeAsyncWithRetries(lts, pd, future, statement, retries + 1), 1, TimeUnit.SECONDS);
-                                               else
-                                                   future.complete(res);
-                                           });
-        }
-    }
-
-    @JsonTypeName("in_jvm_token_aware")
-    public static class Configuration implements harry.core.Configuration.VisitorConfiguration
-    {
-        public final harry.core.Configuration.RowVisitorConfiguration row_visitor;
-        public final SystemUnderTest.ConsistencyLevel consistency_level;
-
-        @JsonCreator
-        public Configuration(@JsonProperty("row_visitor") harry.core.Configuration.RowVisitorConfiguration rowVisitor,
-                             @JsonProperty("consistency_level") SystemUnderTest.ConsistencyLevel consistencyLevel)
-        {
-            this.row_visitor = rowVisitor;
-            this.consistency_level = consistencyLevel;
-        }
-
-        @Override
-        public Visitor make(Run run)
-        {
-            return new GeneratingVisitor(run, new InJVMTokenAwareVisitorExecutor(run, row_visitor, consistency_level));
-        }
-    }
-}
\ No newline at end of file
diff --git a/test/distributed/org/apache/cassandra/distributed/fuzz/QueryingNoOpChecker.java b/test/distributed/org/apache/cassandra/distributed/fuzz/QueryingNoOpChecker.java
deleted file mode 100644
index 77d19a2a7e..0000000000
--- a/test/distributed/org/apache/cassandra/distributed/fuzz/QueryingNoOpChecker.java
+++ /dev/null
@@ -1,66 +0,0 @@
-/*
- * 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 org.apache.cassandra.distributed.fuzz;
-
-import com.fasterxml.jackson.annotation.JsonCreator;
-import com.fasterxml.jackson.annotation.JsonTypeName;
-import harry.core.Configuration;
-import harry.core.Run;
-import harry.model.Model;
-import harry.model.sut.SystemUnderTest;
-import harry.operations.CompiledStatement;
-import harry.operations.Query;
-
-public class QueryingNoOpChecker implements Model
-{
-    public static void init()
-    {
-        Configuration.registerSubtypes(QueryingNoOpCheckerConfig.class);
-    }
-
-    private final Run run;
-
-    public QueryingNoOpChecker(Run run)
-    {
-        this.run = run;
-    }
-
-    @Override
-    public void validate(Query query)
-    {
-        CompiledStatement compiled = query.toSelectStatement();
-        run.sut.execute(compiled.cql(),
-                        SystemUnderTest.ConsistencyLevel.ALL,
-                        compiled.bindings());
-    }
-
-    @JsonTypeName("querying_no_op_checker")
-    public static class QueryingNoOpCheckerConfig implements Configuration.ModelConfiguration
-    {
-        @JsonCreator
-        public QueryingNoOpCheckerConfig()
-        {
-        }
-
-        public Model make(Run run)
-        {
-            return new QueryingNoOpChecker(run);
-        }
-    }
-}
diff --git a/test/distributed/org/apache/cassandra/distributed/fuzz/SSTableGenerator.java b/test/distributed/org/apache/cassandra/distributed/fuzz/SSTableGenerator.java
deleted file mode 100644
index 54fe2303ad..0000000000
--- a/test/distributed/org/apache/cassandra/distributed/fuzz/SSTableGenerator.java
+++ /dev/null
@@ -1,363 +0,0 @@
-/*
- * 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 org.apache.cassandra.distributed.fuzz;
-
-import java.nio.ByteBuffer;
-import java.util.ArrayList;
-import java.util.Collection;
-import java.util.Collections;
-import java.util.HashSet;
-import java.util.List;
-import java.util.Set;
-import java.util.SortedSet;
-
-import harry.core.Run;
-import harry.ddl.SchemaSpec;
-import harry.model.OpSelectors;
-import harry.operations.Relation;
-import harry.operations.Query;
-import harry.operations.QueryGenerator;
-import harry.util.BitSet;
-import org.apache.cassandra.Util;
-import org.apache.cassandra.cql3.AbstractMarker;
-import org.apache.cassandra.cql3.ColumnIdentifier;
-import org.apache.cassandra.cql3.Operator;
-import org.apache.cassandra.cql3.QueryOptions;
-import org.apache.cassandra.cql3.SingleColumnRelation;
-import org.apache.cassandra.cql3.VariableSpecifications;
-import org.apache.cassandra.cql3.WhereClause;
-import org.apache.cassandra.cql3.restrictions.StatementRestrictions;
-import org.apache.cassandra.cql3.statements.Bound;
-import org.apache.cassandra.cql3.statements.DeleteStatement;
-import org.apache.cassandra.cql3.statements.StatementType;
-import org.apache.cassandra.db.ClusteringBound;
-import org.apache.cassandra.db.ColumnFamilyStore;
-import org.apache.cassandra.db.ConsistencyLevel;
-import org.apache.cassandra.db.DecoratedKey;
-import org.apache.cassandra.db.DeletionTime;
-import org.apache.cassandra.db.Mutation;
-import org.apache.cassandra.db.RangeTombstone;
-import org.apache.cassandra.db.RowUpdateBuilder;
-import org.apache.cassandra.db.Slices;
-import org.apache.cassandra.db.marshal.ByteBufferAccessor;
-import org.apache.cassandra.db.marshal.CompositeType;
-import org.apache.cassandra.db.partitions.PartitionUpdate;
-import org.apache.cassandra.io.sstable.format.SSTableReader;
-import org.apache.cassandra.schema.ColumnMetadata;
-import org.apache.cassandra.schema.TableMetadata;
-import org.apache.cassandra.utils.ByteBufferUtil;
-import org.apache.cassandra.utils.FBUtilities;
-
-import static harry.generators.DataGenerators.UNSET_VALUE;
-
-public class SSTableGenerator
-{
-    protected final SchemaSpec schema;
-    protected final OpSelectors.DescriptorSelector descriptorSelector;
-    protected final OpSelectors.MonotonicClock clock;
-    protected final ColumnFamilyStore store;
-    protected final TableMetadata metadata;
-    protected final QueryGenerator rangeSelector;
-
-    private final Set<SSTableReader> sstables = new HashSet<>();
-
-    private long lts = 0;
-
-    public SSTableGenerator(Run run,
-                            ColumnFamilyStore store)
-    {
-        this.schema = run.schemaSpec;
-        this.descriptorSelector = run.descriptorSelector;
-        this.clock = run.clock;
-        this.store = store;
-        // We assume metadata can not change over the lifetime of sstable generator
-        this.metadata = store.metadata.get();
-        this.rangeSelector = new QueryGenerator(schema, run.pdSelector, descriptorSelector, run.rng);
-        store.disableAutoCompaction();
-    }
-
-    public Collection<SSTableReader> gen(int rows)
-    {
-        mark();
-        for (int i = 0; i < rows; i++)
-        {
-            long current = lts++;
-            write(current, current, current, current, true).applyUnsafe();
-            if (schema.staticColumns != null)
-                writeStatic(current, current, current, current, true).applyUnsafe();
-        }
-
-        return flush();
-    }
-
-    public void mark()
-    {
-        sstables.clear();
-        sstables.addAll(store.getLiveSSTables());
-    }
-
-    public Collection<SSTableReader> flush()
-    {
-        Util.flush(store);
-        sstables.removeAll(store.getLiveSSTables());
-
-        Set<SSTableReader> ret = new HashSet<>(sstables);
-        mark();
-        return ret;
-    }
-
-    public Mutation write(long lts, long pd, long cd, long opId, boolean withRowMarker)
-    {
-        long[] vds = descriptorSelector.vds(pd, cd, lts, opId, OpSelectors.OperationKind.INSERT, schema);
-
-        Object[] partitionKey = schema.inflatePartitionKey(pd);
-        Object[] clusteringKey = schema.inflateClusteringKey(cd);
-        Object[] regularColumns = schema.inflateRegularColumns(vds);
-
-        RowUpdateBuilder builder = new RowUpdateBuilder(metadata,
-                                                        FBUtilities.nowInSeconds(),
-                                                        clock.rts(lts),
-                                                        metadata.params.defaultTimeToLive,
-                                                        serializePartitionKey(store, partitionKey))
-                                   .clustering(clusteringKey);
-
-        if (!withRowMarker)
-            builder.noRowMarker();
-
-        for (int i = 0; i < regularColumns.length; i++)
-        {
-            Object value = regularColumns[i];
-            if (value == UNSET_VALUE)
-                continue;
-
-            ColumnMetadata def = metadata.getColumn(new ColumnIdentifier(schema.regularColumns.get(i).name, false));
-            builder.add(def, value);
-        }
-
-        return builder.build();
-    }
-
-    public Mutation writeStatic(long lts, long pd, long cd, long opId, boolean withRowMarker)
-    {
-        long[] sds = descriptorSelector.sds(pd, cd, lts, opId, OpSelectors.OperationKind.INSERT_WITH_STATICS, schema);
-
-        Object[] partitionKey = schema.inflatePartitionKey(pd);
-        Object[] staticColumns = schema.inflateStaticColumns(sds);
-
-        RowUpdateBuilder builder = new RowUpdateBuilder(metadata,
-                                                        FBUtilities.nowInSeconds(),
-                                                        clock.rts(lts),
-                                                        metadata.params.defaultTimeToLive,
-                                                        serializePartitionKey(store, partitionKey));
-
-        if (!withRowMarker)
-            builder.noRowMarker();
-
-        for (int i = 0; i < staticColumns.length; i++)
-        {
-            Object value = staticColumns[i];
-            if (value == UNSET_VALUE)
-                continue;
-            ColumnMetadata def = metadata.getColumn(new ColumnIdentifier(schema.staticColumns.get(i).name, false));
-            builder.add(def, staticColumns[i]);
-        }
-
-        return builder.build();
-    }
-
-    public Mutation deleteColumn(long lts, long pd, long cd, long opId)
-    {
-        Object[] partitionKey = schema.inflatePartitionKey(pd);
-        Object[] clusteringKey = schema.inflateClusteringKey(cd);
-
-        RowUpdateBuilder builder = new RowUpdateBuilder(metadata,
-                                                        FBUtilities.nowInSeconds(),
-                                                        clock.rts(lts),
-                                                        metadata.params.defaultTimeToLive,
-                                                        serializePartitionKey(store, partitionKey))
-                                   .noRowMarker()
-                                   .clustering(clusteringKey);
-
-        BitSet columns = descriptorSelector.columnMask(pd, lts, opId, OpSelectors.OperationKind.DELETE_COLUMN);
-        BitSet mask = schema.regularColumnsMask();
-
-        if (columns == null || columns.allUnset(mask))
-            throw new IllegalArgumentException("Can't have a delete column query with no columns set. Column mask: " + columns);
-
-        columns.eachSetBit((idx) -> {
-            if (idx < schema.regularColumnsOffset)
-                throw new RuntimeException("Can't delete parts of partition or clustering key");
-
-            if (idx > schema.allColumns.size())
-                throw new IndexOutOfBoundsException(String.format("Index %d is out of bounds. Max index: %d", idx, schema.allColumns.size()));
-
-            builder.delete(schema.allColumns.get(idx).name);
-        }, mask);
-
-        return builder.build();
-    }
-
-    public Mutation deleteStatic(long lts, long pd, long opId)
-    {
-        Object[] partitionKey = schema.inflatePartitionKey(pd);
-
-        RowUpdateBuilder builder = new RowUpdateBuilder(metadata,
-                                                        FBUtilities.nowInSeconds(),
-                                                        clock.rts(lts),
-                                                        metadata.params.defaultTimeToLive,
-                                                        serializePartitionKey(store, partitionKey))
-                                   .noRowMarker();
-
-
-        BitSet columns = descriptorSelector.columnMask(pd, lts, opId, OpSelectors.OperationKind.DELETE_COLUMN_WITH_STATICS);
-        BitSet mask = schema.staticColumnsMask();
-
-        if (columns == null || columns.allUnset(mask))
-            throw new IllegalArgumentException("Can't have a delete column query with no columns set. Column mask: " + columns);
-
-        columns.eachSetBit((idx) -> {
-            if (idx < schema.staticColumnsOffset)
-                throw new RuntimeException(String.format("Can't delete parts of partition or clustering key %d (%s)",
-                                                         idx, schema.allColumns.get(idx)));
-
-            if (idx > schema.allColumns.size())
-                throw new IndexOutOfBoundsException(String.format("Index %d is out of bounds. Max index: %d", idx, schema.allColumns.size()));
-
-            builder.delete(schema.allColumns.get(idx).name);
-        }, mask);
-
-        return builder.build();
-    }
-
-    public Mutation deletePartition(long lts, long pd)
-    {
-        Object[] partitionKey = schema.inflatePartitionKey(pd);
-
-        PartitionUpdate update = PartitionUpdate.fullPartitionDelete(metadata,
-                                                                     serializePartitionKey(store, partitionKey),
-                                                                     clock.rts(lts),
-                                                                     FBUtilities.nowInSeconds());
-
-        return new Mutation(update);
-    }
-
-    public Mutation deleteRow(long lts, long pd, long cd)
-    {
-        Object[] partitionKey = schema.inflatePartitionKey(pd);
-        Object[] clusteringKey = schema.inflateClusteringKey(cd);
-
-        return RowUpdateBuilder.deleteRow(metadata,
-                                          clock.rts(lts),
-                                          serializePartitionKey(store, partitionKey),
-                                          clusteringKey);
-    }
-
-    public Mutation deleteSlice(long lts, long pd, long opId)
-    {
-        return delete(lts, pd, rangeSelector.inflate(lts, opId, Query.QueryKind.CLUSTERING_SLICE));
-    }
-
-    public Mutation deleteRange(long lts, long pd, long opId)
-    {
-        return delete(lts, pd, rangeSelector.inflate(lts, opId, Query.QueryKind.CLUSTERING_RANGE));
-    }
-
-    Mutation delete(long lts, long pd, Query query)
-    {
-        Object[] partitionKey = schema.inflatePartitionKey(pd);
-        WhereClause.Builder builder = new WhereClause.Builder();
-        List<ColumnIdentifier> variableNames = new ArrayList<>();
-
-        List<ByteBuffer> values = new ArrayList<>();
-        for (int i = 0; i < partitionKey.length; i++)
-        {
-            String name = schema.partitionKeys.get(i).name;
-            ColumnMetadata columnDef = metadata.getColumn(ByteBufferUtil.bytes(name));
-            variableNames.add(columnDef.name);
-            values.add(ByteBufferUtil.objectToBytes(partitionKey[i]));
-            builder.add(new SingleColumnRelation(ColumnIdentifier.getInterned(name, true),
-                                                 toOperator(Relation.RelationKind.EQ),
-                                                 new AbstractMarker.Raw(values.size() - 1)));
-        }
-
-        for (Relation relation : query.relations)
-        {
-            String name = relation.column();
-            ColumnMetadata columnDef = metadata.getColumn(ByteBufferUtil.bytes(relation.column()));
-            variableNames.add(columnDef.name);
-            values.add(ByteBufferUtil.objectToBytes(relation.value()));
-            builder.add(new SingleColumnRelation(ColumnIdentifier.getInterned(name, false),
-                                                 toOperator(relation.kind),
-                                                 new AbstractMarker.Raw(values.size() - 1)));
-        }
-
-        StatementRestrictions restrictions = new StatementRestrictions(null,
-                                                                       StatementType.DELETE,
-                                                                       metadata,
-                                                                       builder.build(),
-                                                                       new VariableSpecifications(variableNames),
-                                                                       Collections.emptyList(),
-                                                                       false,
-                                                                       false,
-                                                                       false,
-                                                                       false);
-
-        QueryOptions options = QueryOptions.forInternalCalls(ConsistencyLevel.QUORUM, values);
-        SortedSet<ClusteringBound<?>> startBounds = restrictions.getClusteringColumnsBounds(Bound.START, options);
-        SortedSet<ClusteringBound<?>> endBounds = restrictions.getClusteringColumnsBounds(Bound.END, options);
-
-        Slices slices = DeleteStatement.toSlices(metadata, startBounds, endBounds);
-        assert slices.size() == 1;
-        long deletionTime = FBUtilities.nowInSeconds();
-        long rts = clock.rts(lts);
-
-        return new RowUpdateBuilder(metadata,
-                                    deletionTime,
-                                    rts,
-                                    metadata.params.defaultTimeToLive,
-                                    serializePartitionKey(store, partitionKey))
-               .noRowMarker()
-               .addRangeTombstone(new RangeTombstone(slices.get(0), DeletionTime.build(rts, deletionTime)))
-               .build();
-    }
-
-    public static Operator toOperator(Relation.RelationKind kind)
-    {
-        switch (kind)
-        {
-            case LT: return Operator.LT;
-            case GT: return Operator.GT;
-            case LTE: return Operator.LTE;
-            case GTE: return Operator.GTE;
-            case EQ: return Operator.EQ;
-            default: throw new IllegalArgumentException("Unsupported " + kind);
-        }
-    }
-
-    public static DecoratedKey serializePartitionKey(ColumnFamilyStore store, Object... pk)
-    {
-        if (pk.length == 1)
-            return store.getPartitioner().decorateKey(ByteBufferUtil.objectToBytes(pk[0]));
-
-        ByteBuffer[] values = new ByteBuffer[pk.length];
-        for (int i = 0; i < pk.length; i++)
-            values[i] = ByteBufferUtil.objectToBytes(pk[i]);
-        return store.getPartitioner().decorateKey(CompositeType.build(ByteBufferAccessor.instance, values));
-    }
-}
diff --git a/test/distributed/org/apache/cassandra/distributed/fuzz/SSTableLoadingVisitor.java b/test/distributed/org/apache/cassandra/distributed/fuzz/SSTableLoadingVisitor.java
deleted file mode 100644
index ce6ef57aff..0000000000
--- a/test/distributed/org/apache/cassandra/distributed/fuzz/SSTableLoadingVisitor.java
+++ /dev/null
@@ -1,113 +0,0 @@
-/*
- * 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 org.apache.cassandra.distributed.fuzz;
-
-import harry.core.Run;
-import harry.model.OpSelectors;
-import harry.visitors.VisitExecutor;
-import org.apache.cassandra.db.Keyspace;
-
-public class SSTableLoadingVisitor extends VisitExecutor
-{
-    private final SSTableGenerator gen;
-    private final int forceFlushAfter;
-
-    public SSTableLoadingVisitor(Run run, int forceFlushAfter)
-    {
-        this.forceFlushAfter = forceFlushAfter;
-        gen = new SSTableGenerator(run, Keyspace.open(run.schemaSpec.keyspace).getColumnFamilyStore(run.schemaSpec.table));
-        gen.mark();
-    }
-
-    @Override
-    protected void beforeLts(long l, long l1)
-    {
-
-    }
-
-    protected void afterLts(long lts, long pd) {
-        if (lts > 0 && lts % forceFlushAfter == 0)
-            forceFlush();
-    }
-
-    @Override
-    protected void beforeBatch(long l, long l1, long l2)
-    {
-
-    }
-
-    @Override
-    protected void afterBatch(long l, long l1, long l2)
-    {
-
-    }
-
-    @Override
-    protected void operation(long lts, long pd, long cd, long m, long opId, OpSelectors.OperationKind operationKind)
-    {
-        switch (operationKind)
-        {
-            case INSERT:
-                gen.write(lts, pd, cd, opId, true).applyUnsafe();
-                break;
-            case INSERT_WITH_STATICS:
-                gen.write(lts, pd, cd, opId, true).applyUnsafe();
-                gen.writeStatic(lts, pd, cd, opId, true).applyUnsafe();
-                break;
-            case UPDATE:
-                gen.write(lts, pd, cd, opId, false).applyUnsafe();
-                break;
-            case UPDATE_WITH_STATICS:
-                gen.write(lts, pd, cd, opId, false).applyUnsafe();
-                gen.writeStatic(lts, pd, cd, opId, false).applyUnsafe();
-                break;
-            case DELETE_PARTITION:
-                gen.deletePartition(lts, pd).applyUnsafe();
-                break;
-            case DELETE_ROW:
-                gen.deleteRow(lts, pd, cd).applyUnsafe();
-                break;
-            case DELETE_COLUMN:
-                gen.deleteColumn(lts, pd, cd, opId).applyUnsafe();
-                break;
-            case DELETE_COLUMN_WITH_STATICS:
-                gen.deleteColumn(lts, pd, cd, opId).applyUnsafe();
-                gen.deleteStatic(lts, pd, opId).applyUnsafe();
-                break;
-            case DELETE_RANGE:
-                gen.deleteRange(lts, pd, opId).applyUnsafe();
-                break;
-            case DELETE_SLICE:
-                gen.deleteSlice(lts, pd, opId).applyUnsafe();
-                break;
-        }
-    }
-
-    public void forceFlush()
-    {
-        gen.flush();
-    }
-
-
-    @Override
-    public void shutdown() throws InterruptedException
-    {
-
-    }
-}
diff --git a/test/distributed/org/apache/cassandra/distributed/fuzz/test/SSTableGeneratorTest.java b/test/distributed/org/apache/cassandra/distributed/fuzz/test/SSTableGeneratorTest.java
deleted file mode 100644
index 5050bc68ff..0000000000
--- a/test/distributed/org/apache/cassandra/distributed/fuzz/test/SSTableGeneratorTest.java
+++ /dev/null
@@ -1,131 +0,0 @@
-/*
- * 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 org.apache.cassandra.distributed.fuzz.test;
-
-import java.util.Arrays;
-import java.util.HashSet;
-import java.util.Set;
-import java.util.concurrent.CompletableFuture;
-
-import com.google.common.collect.Iterators;
-import org.junit.Test;
-
-import harry.core.Configuration;
-import harry.core.Run;
-import harry.ddl.ColumnSpec;
-import harry.ddl.SchemaSpec;
-import harry.model.Model;
-import harry.model.QuiescentChecker;
-import harry.model.clock.OffsetClock;
-import harry.model.sut.SystemUnderTest;
-import harry.operations.Query;
-import harry.visitors.GeneratingVisitor;
-import harry.visitors.LtsVisitor;
-import org.apache.cassandra.cql3.CQLTester;
-import org.apache.cassandra.distributed.fuzz.FixedSchemaProviderConfiguration;
-import org.apache.cassandra.distributed.fuzz.HarryHelper;
-import org.apache.cassandra.distributed.fuzz.SSTableLoadingVisitor;
-import org.apache.cassandra.distributed.impl.RowUtil;
-
-public class SSTableGeneratorTest extends CQLTester
-{
-    private static final Configuration configuration;
-
-    static
-    {
-        try
-        {
-            HarryHelper.init();
-            configuration = HarryHelper.defaultConfiguration()
-                                       .setClock(() -> new OffsetClock(10000L))
-                                       .build();
-        }
-        catch (Exception e)
-        {
-            throw new RuntimeException(e);
-        }
-    }
-
-    private static SchemaSpec schemaSpec = new SchemaSpec(KEYSPACE, "tbl1",
-                                                          Arrays.asList(ColumnSpec.pk("pk1", ColumnSpec.asciiType),
-                                                                        ColumnSpec.pk("pk2", ColumnSpec.int64Type)),
-                                                          Arrays.asList(ColumnSpec.ck("ck1", ColumnSpec.asciiType, false),
-                                                                        ColumnSpec.ck("ck2", ColumnSpec.int64Type, false)),
-                                                          Arrays.asList(ColumnSpec.regularColumn("v1", ColumnSpec.int32Type),
-                                                                        ColumnSpec.regularColumn("v2", ColumnSpec.int64Type),
-                                                                        ColumnSpec.regularColumn("v3", ColumnSpec.int32Type),
-                                                                        ColumnSpec.regularColumn("v4", ColumnSpec.asciiType)),
-                                                          Arrays.asList(ColumnSpec.staticColumn("s1", ColumnSpec.asciiType),
-                                                                        ColumnSpec.staticColumn("s2", ColumnSpec.int64Type)));
-
-    @Test
-    public void testSSTableGenerator()
-    {
-        createTable(schemaSpec.compile().cql());
-        Run run = configuration.unbuild()
-                               .setSchemaProvider(new FixedSchemaProviderConfiguration(schemaSpec))
-                               .setSUT(CqlTesterSUT::new)
-                               .build()
-                               .createRun();
-
-
-        SSTableLoadingVisitor sstableVisitor = new SSTableLoadingVisitor(run, 1000);
-        LtsVisitor visitor = new GeneratingVisitor(run, sstableVisitor);
-        Set<Long> pds = new HashSet<>();
-        run.tracker.onLtsStarted((lts) -> pds.add(run.pdSelector.pd(lts, run.schemaSpec)));
-        for (int i = 0; i < 1000; i++)
-            visitor.visit();
-
-        sstableVisitor.forceFlush();
-
-        Model checker = new QuiescentChecker(run);
-        for (Long pd : pds)
-            checker.validate(Query.selectPartition(run.schemaSpec, pd, false));
-    }
-
-    public class CqlTesterSUT implements SystemUnderTest
-    {
-        public boolean isShutdown()
-        {
-            return false;
-        }
-
-        public void shutdown()
-        {
-        }
-
-        public CompletableFuture<Object[][]> executeAsync(String s, ConsistencyLevel consistencyLevel, Object... objects)
-        {
-            throw new RuntimeException("Not implemented");
-        }
-
-        public Object[][] execute(String s, ConsistencyLevel consistencyLevel, Object... objects)
-        {
-            try
-            {
-                return Iterators.toArray(RowUtil.toIter(SSTableGeneratorTest.this.execute(s, objects)),
-                                         Object[].class);
-            }
-            catch (Throwable throwable)
-            {
-                throw new RuntimeException();
-            }
-        }
-    }
-}
diff --git a/test/distributed/org/apache/cassandra/distributed/shared/ClusterUtils.java b/test/distributed/org/apache/cassandra/distributed/shared/ClusterUtils.java
index ac8aea6591..afb782f5a5 100644
--- a/test/distributed/org/apache/cassandra/distributed/shared/ClusterUtils.java
+++ b/test/distributed/org/apache/cassandra/distributed/shared/ClusterUtils.java
@@ -42,14 +42,13 @@ import java.util.regex.Matcher;
 import java.util.regex.Pattern;
 import java.util.stream.Collectors;
 
+import com.google.common.annotations.VisibleForTesting;
 import com.google.common.collect.ImmutableList;
 import com.google.common.util.concurrent.Futures;
-
 import org.junit.Assert;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 
-import harry.core.VisibleForTesting;
 import org.apache.cassandra.dht.Token;
 import org.apache.cassandra.distributed.api.ConsistencyLevel;
 import org.apache.cassandra.distributed.api.Feature;
@@ -61,10 +60,10 @@ import org.apache.cassandra.distributed.api.IMessageFilters;
 import org.apache.cassandra.distributed.api.NodeToolResult;
 import org.apache.cassandra.distributed.impl.AbstractCluster;
 import org.apache.cassandra.distributed.impl.InstanceConfig;
-import org.apache.cassandra.gms.ApplicationState;
-import org.apache.cassandra.gms.VersionedValue;
 import org.apache.cassandra.distributed.impl.TestChangeListener;
 import org.apache.cassandra.distributed.test.log.TestProcessor;
+import org.apache.cassandra.gms.ApplicationState;
+import org.apache.cassandra.gms.VersionedValue;
 import org.apache.cassandra.io.util.File;
 import org.apache.cassandra.locator.InetAddressAndPort;
 import org.apache.cassandra.net.Message;
diff --git a/test/distributed/org/apache/cassandra/distributed/test/log/BounceGossipTest.java b/test/distributed/org/apache/cassandra/distributed/test/log/BounceGossipTest.java
index f75269c540..8c188a2fec 100644
--- a/test/distributed/org/apache/cassandra/distributed/test/log/BounceGossipTest.java
+++ b/test/distributed/org/apache/cassandra/distributed/test/log/BounceGossipTest.java
@@ -27,34 +27,35 @@ import java.util.concurrent.atomic.AtomicInteger;
 import com.google.common.util.concurrent.Uninterruptibles;
 import org.junit.Test;
 
-import harry.core.Configuration;
-import harry.ddl.SchemaSpec;
-import harry.visitors.MutatingVisitor;
-import harry.visitors.QueryLogger;
-import harry.visitors.RandomPartitionValidator;
 import org.apache.cassandra.distributed.Cluster;
 import org.apache.cassandra.distributed.Constants;
-import org.apache.cassandra.distributed.harry.ClusterState;
-import org.apache.cassandra.distributed.harry.ExistingClusterSUT;
-import org.apache.cassandra.distributed.harry.FlaggedRunner;
+import org.apache.cassandra.harry.HarryHelper;
+import org.apache.cassandra.harry.runner.FlaggedRunner;
 import org.apache.cassandra.distributed.test.TestBaseImpl;
 import org.apache.cassandra.gms.ApplicationState;
 import org.apache.cassandra.gms.EndpointState;
 import org.apache.cassandra.gms.Gossiper;
+import org.apache.cassandra.harry.core.Configuration;
+import org.apache.cassandra.harry.ddl.SchemaSpec;
+import org.apache.cassandra.harry.sut.injvm.InJvmSut;
+import org.apache.cassandra.harry.sut.injvm.InJvmSutBase;
+import org.apache.cassandra.harry.visitors.MutatingVisitor;
+import org.apache.cassandra.harry.visitors.QueryLogger;
+import org.apache.cassandra.harry.visitors.RandomPartitionValidator;
 import org.apache.cassandra.locator.InetAddressAndPort;
 import org.apache.cassandra.utils.concurrent.CountDownLatch;
 
-import static harry.core.Configuration.VisitorPoolConfiguration.pool;
-import static harry.ddl.ColumnSpec.asciiType;
-import static harry.ddl.ColumnSpec.ck;
-import static harry.ddl.ColumnSpec.int64Type;
-import static harry.ddl.ColumnSpec.pk;
-import static harry.ddl.ColumnSpec.regularColumn;
-import static harry.ddl.ColumnSpec.staticColumn;
 import static java.util.Arrays.asList;
 import static org.apache.cassandra.concurrent.ExecutorFactory.Global.executorFactory;
 import static org.apache.cassandra.distributed.api.Feature.GOSSIP;
 import static org.apache.cassandra.distributed.api.Feature.NETWORK;
+import static org.apache.cassandra.harry.core.Configuration.VisitorPoolConfiguration.pool;
+import static org.apache.cassandra.harry.ddl.ColumnSpec.asciiType;
+import static org.apache.cassandra.harry.ddl.ColumnSpec.ck;
+import static org.apache.cassandra.harry.ddl.ColumnSpec.int64Type;
+import static org.apache.cassandra.harry.ddl.ColumnSpec.pk;
+import static org.apache.cassandra.harry.ddl.ColumnSpec.regularColumn;
+import static org.apache.cassandra.harry.ddl.ColumnSpec.staticColumn;
 import static org.apache.cassandra.utils.Clock.Global.currentTimeMillis;
 import static org.junit.Assert.fail;
 import static org.psjava.util.AssertStatus.assertTrue;
@@ -76,12 +77,10 @@ public class BounceGossipTest extends TestBaseImpl
                                                asList(regularColumn("regular1", asciiType), regularColumn("regular1", int64Type)),
                                                asList(staticColumn("static1", asciiType), staticColumn("static1", int64Type)));
             AtomicInteger down = new AtomicInteger(0);
-            ClusterState clusterState = (i) -> down.get() == i;
-            Configuration config = Configuration.fromYamlString(createHarryConf())
-                                                .unbuild()
-                                                .setKeyspaceDdl(String.format("CREATE KEYSPACE IF NOT EXISTS %s WITH replication = {'class': 'SimpleStrategy', 'replication_factor': %d};", schema.keyspace, 3))
-                                                .setSUT(new ExistingClusterSUT(cluster, clusterState))
-                                                .build();
+            Configuration config = HarryHelper.defaultConfiguration()
+                                              .setKeyspaceDdl(String.format("CREATE KEYSPACE IF NOT EXISTS %s WITH replication = {'class': 'SimpleStrategy', 'replication_factor': %d};", schema.keyspace, 3))
+                                              .setSUT(() -> new InJvmSut(cluster, () -> 1, InJvmSutBase.retryOnTimeout()))
+                                              .build();
 
             CountDownLatch stopLatch = CountDownLatch.newCountDownLatch(1);
             Future<?> f = es.submit(() -> {
diff --git a/test/distributed/org/apache/cassandra/distributed/test/log/CMSTestBase.java b/test/distributed/org/apache/cassandra/distributed/test/log/CMSTestBase.java
index b262b66b00..eb78bc7e38 100644
--- a/test/distributed/org/apache/cassandra/distributed/test/log/CMSTestBase.java
+++ b/test/distributed/org/apache/cassandra/distributed/test/log/CMSTestBase.java
@@ -24,6 +24,7 @@ import org.apache.cassandra.config.DatabaseDescriptor;
 import org.apache.cassandra.db.guardrails.Guardrails;
 import org.apache.cassandra.dht.Murmur3Partitioner;
 import org.apache.cassandra.distributed.api.IIsolatedExecutor;
+import org.apache.cassandra.harry.sut.TokenPlacementModel;
 import org.apache.cassandra.locator.IEndpointSnitch;
 import org.apache.cassandra.locator.InetAddressAndPort;
 import org.apache.cassandra.locator.Replica;
@@ -87,9 +88,9 @@ public class CMSTestBase
         public final LocalLog log;
         public final ClusterMetadataService service;
         public final SchemaProvider schemaProvider;
-        public final PlacementSimulator.ReplicationFactor rf;
+        public final TokenPlacementModel.ReplicationFactor rf;
 
-        public CMSSut(IIsolatedExecutor.SerializableFunction<LocalLog, Processor> processorFactory, boolean addListeners, PlacementSimulator.ReplicationFactor rf)
+        public CMSSut(IIsolatedExecutor.SerializableFunction<LocalLog, Processor> processorFactory, boolean addListeners, TokenPlacementModel.ReplicationFactor rf)
         {
             partitioner = Murmur3Partitioner.instance;
             this.rf = rf;
diff --git a/test/distributed/org/apache/cassandra/distributed/test/log/CoordinatorPathTest.java b/test/distributed/org/apache/cassandra/distributed/test/log/CoordinatorPathTest.java
index 398fd4ffa7..ce5c5800d4 100644
--- a/test/distributed/org/apache/cassandra/distributed/test/log/CoordinatorPathTest.java
+++ b/test/distributed/org/apache/cassandra/distributed/test/log/CoordinatorPathTest.java
@@ -30,40 +30,36 @@ import java.util.stream.Collectors;
 import org.junit.Assert;
 import org.junit.Test;
 
-import harry.core.Configuration;
-import harry.core.Run;
-import harry.model.OpSelectors;
-import harry.operations.CompiledStatement;
-import harry.operations.WriteHelper;
-import harry.util.ByteUtils;
-import harry.util.TokenUtil;
+import org.apache.cassandra.harry.core.Configuration;
+import org.apache.cassandra.harry.core.Run;
+import org.apache.cassandra.harry.model.OpSelectors;
+import org.apache.cassandra.harry.operations.CompiledStatement;
+import org.apache.cassandra.harry.operations.WriteHelper;
+import org.apache.cassandra.harry.sut.injvm.InJvmSut;
+import org.apache.cassandra.harry.util.ByteUtils;
+import org.apache.cassandra.harry.util.TokenUtil;
 import org.apache.cassandra.db.Mutation;
 import org.apache.cassandra.db.ReadCommand;
 import org.apache.cassandra.db.ReadResponse;
 import org.apache.cassandra.distributed.api.ConsistencyLevel;
-import org.apache.cassandra.distributed.fuzz.HarryHelper;
-import org.apache.cassandra.distributed.fuzz.InJvmSut;
+import org.apache.cassandra.harry.HarryHelper;
+import org.apache.cassandra.harry.sut.TokenPlacementModel;
+import org.apache.cassandra.harry.sut.TokenPlacementModel.Node;
 import org.apache.cassandra.net.Message;
 import org.apache.cassandra.net.NoPayload;
 import org.apache.cassandra.net.Verb;
 import org.apache.cassandra.utils.concurrent.Future;
 
-import static org.apache.cassandra.distributed.test.log.PlacementSimulator.Node;
-
-
 public class CoordinatorPathTest extends CoordinatorPathTestBase
 {
-    private static final PlacementSimulator.SimpleReplicationFactor RF = new PlacementSimulator.SimpleReplicationFactor(3);
+    private static final TokenPlacementModel.SimpleReplicationFactor RF = new TokenPlacementModel.SimpleReplicationFactor(3);
 
     @Test
     public void writeConsistencyTest() throws Throwable
     {
-        Configuration.ConfigurationBuilder configBuilder = HarryHelper.defaultConfiguration()
-                                                                      .setPartitionDescriptorSelector(new Configuration.DefaultPDSelectorConfiguration(1, 1))
-                                                                      .setClusteringDescriptorSelector(HarryHelper.singleRowPerModification().build());
-
         coordinatorPathTest(RF, (cluster, simulatedCluster) -> {
-            configBuilder.setSUT(() -> new InJvmSut(cluster));
+            Configuration.ConfigurationBuilder configBuilder = HarryHelper.defaultConfiguration()
+                                                                          .setSUT(() -> new InJvmSut(cluster));
             Run run = configBuilder.build().createRun();
 
             for (int ignored : new int[]{ 2, 3, 4, 5 })
diff --git a/test/distributed/org/apache/cassandra/distributed/test/log/CoordinatorPathTestBase.java b/test/distributed/org/apache/cassandra/distributed/test/log/CoordinatorPathTestBase.java
index 7f4fc82b32..8c41d17ddc 100644
--- a/test/distributed/org/apache/cassandra/distributed/test/log/CoordinatorPathTestBase.java
+++ b/test/distributed/org/apache/cassandra/distributed/test/log/CoordinatorPathTestBase.java
@@ -42,8 +42,6 @@ import org.junit.Assert;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 
-import harry.util.ByteUtils;
-import harry.util.TestRunner;
 import org.apache.cassandra.ServerTestUtils;
 import org.apache.cassandra.concurrent.ExecutorFactory;
 import org.apache.cassandra.concurrent.ExecutorPlus;
@@ -66,6 +64,11 @@ import org.apache.cassandra.distributed.shared.NetworkTopology;
 import org.apache.cassandra.distributed.test.log.PlacementSimulator.Transformations;
 import org.apache.cassandra.gms.GossipDigestAck;
 import org.apache.cassandra.gms.GossipDigestSyn;
+import org.apache.cassandra.harry.sut.TokenPlacementModel;
+import org.apache.cassandra.harry.sut.TokenPlacementModel.Node;
+import org.apache.cassandra.harry.sut.TokenPlacementModel.NodeFactory;
+import org.apache.cassandra.harry.util.ByteUtils;
+import org.apache.cassandra.harry.util.TestRunner;
 import org.apache.cassandra.locator.InetAddressAndPort;
 import org.apache.cassandra.locator.SimpleSeedProvider;
 import org.apache.cassandra.tcm.*;
@@ -91,7 +94,6 @@ import org.apache.cassandra.utils.concurrent.AsyncPromise;
 import org.apache.cassandra.utils.concurrent.CountDownLatch;
 import org.apache.cassandra.utils.concurrent.Future;
 
-import static org.apache.cassandra.distributed.test.log.PlacementSimulator.Node;
 import static org.apache.cassandra.distributed.test.log.PlacementSimulator.RefSimulatedPlacementHolder;
 import static org.apache.cassandra.distributed.test.log.PlacementSimulator.SimulatedPlacementHolder;
 import static org.apache.cassandra.distributed.test.log.PlacementSimulator.SimulatedPlacements;
@@ -102,7 +104,7 @@ public abstract class CoordinatorPathTestBase extends FuzzTestBase
 {
     private static final Logger logger = LoggerFactory.getLogger(CoordinatorPathTestBase.class);
 
-    public void coordinatorPathTest(PlacementSimulator.ReplicationFactor rf, TestRunner.ThrowingBiConsumer<Cluster, SimulatedCluster> test) throws Throwable
+    public void coordinatorPathTest(TokenPlacementModel.ReplicationFactor rf, TestRunner.ThrowingBiConsumer<Cluster, SimulatedCluster> test) throws Throwable
     {
         coordinatorPathTest(rf, test, true);
     }
@@ -112,7 +114,7 @@ public abstract class CoordinatorPathTestBase extends FuzzTestBase
      *
      * CMS node is 127.0.0.10 and is simulated.
      */
-    public void coordinatorPathTest(PlacementSimulator.ReplicationFactor rf, TestRunner.ThrowingBiConsumer<Cluster, SimulatedCluster> test, boolean startCluster) throws Throwable
+    public void coordinatorPathTest(TokenPlacementModel.ReplicationFactor rf, TestRunner.ThrowingBiConsumer<Cluster, SimulatedCluster> test, boolean startCluster) throws Throwable
     {
         ServerTestUtils.daemonInitialization();
         DatabaseDescriptor.setPartitionerUnsafe(Murmur3Partitioner.instance);
@@ -120,8 +122,8 @@ public abstract class CoordinatorPathTestBase extends FuzzTestBase
         String nodeUnderTest = "127.0.0.1";
         InetAddressAndPort nodeUnderTestAddr = InetAddressAndPort.getByName(nodeUnderTest + ":7012");
 
-        PlacementSimulator.NodeFactory factory = PlacementSimulator.nodeFactory();
-        Node fakeCmsNode = factory.make(10,1,1);
+        NodeFactory factory = TokenPlacementModel.nodeFactory();
+        Node fakeCmsNode = factory.make(10, 1, 1);
         FBUtilities.setBroadcastInetAddressAndPort(fakeCmsNode.addr());
 
         try (Cluster cluster = builder().withNodes(1)
@@ -153,7 +155,7 @@ public abstract class CoordinatorPathTestBase extends FuzzTestBase
                                         .withTokenSupplier(tokenSupplier)
                                         .withNodeIdTopology(NetworkTopology.singleDcNetworkTopology(10, "dc0", "rack0"))
                                         .createWithoutStarting();
-             SimulatedCluster simulatedCluster = new SimulatedCluster(new PlacementSimulator.SimpleReplicationFactor(3),
+             SimulatedCluster simulatedCluster = new SimulatedCluster(new TokenPlacementModel.SimpleReplicationFactor(3),
                                                                       cluster, tokenSupplier))
         {
             // Note that in case of a CMS node teset we first start a cluster, and then initialize a simulated cluster.
@@ -607,9 +609,9 @@ public abstract class CoordinatorPathTestBase extends FuzzTestBase
         protected final TokenSupplier tokenSupplier;
         protected final IPartitioner partitioner = Murmur3Partitioner.instance;
         protected ExecutorPlus executor;
-        public final PlacementSimulator.NodeFactory nodeFactory = PlacementSimulator.nodeFactory();
+        public final NodeFactory nodeFactory = TokenPlacementModel.nodeFactory();
 
-        public SimulatedCluster(PlacementSimulator.ReplicationFactor rf, ICluster<IInvokableInstance> realCluster, TokenSupplier tokenSupplier)
+        public SimulatedCluster(TokenPlacementModel.ReplicationFactor rf, ICluster<IInvokableInstance> realCluster, TokenSupplier tokenSupplier)
         {
             this.tokenSupplier = tokenSupplier;
 
@@ -845,9 +847,9 @@ public abstract class CoordinatorPathTestBase extends FuzzTestBase
     {
         protected final SimulatedPlacementHolder state;
         protected final List<VirtualSimulatedNode> nodes;
-        protected final PlacementSimulator.NodeFactory factory;
+        protected final NodeFactory factory;
 
-        public VirtualSimulatedCluster(SimulatedPlacementHolder state, PlacementSimulator.NodeFactory factory)
+        public VirtualSimulatedCluster(SimulatedPlacementHolder state, NodeFactory factory)
         {
             this.state = state;
             this.factory = factory;
diff --git a/test/distributed/org/apache/cassandra/distributed/test/log/FailedLeaveTest.java b/test/distributed/org/apache/cassandra/distributed/test/log/FailedLeaveTest.java
index 52aec24c0b..5dadb0ed01 100644
--- a/test/distributed/org/apache/cassandra/distributed/test/log/FailedLeaveTest.java
+++ b/test/distributed/org/apache/cassandra/distributed/test/log/FailedLeaveTest.java
@@ -28,12 +28,16 @@ import java.util.function.BiFunction;
 import org.junit.Assert;
 import org.junit.Test;
 
-import harry.core.Configuration;
-import harry.core.Run;
-import harry.model.sut.SystemUnderTest;
-import harry.visitors.GeneratingVisitor;
-import harry.visitors.MutatingRowVisitor;
-import harry.visitors.Visitor;
+import org.apache.cassandra.harry.core.Configuration;
+import org.apache.cassandra.harry.core.Run;
+import org.apache.cassandra.harry.sut.SystemUnderTest;
+import org.apache.cassandra.harry.sut.TokenPlacementModel;
+import org.apache.cassandra.harry.sut.injvm.InJVMTokenAwareVisitExecutor;
+import org.apache.cassandra.harry.sut.injvm.InJvmSut;
+import org.apache.cassandra.harry.sut.injvm.QuiescentLocalStateChecker;
+import org.apache.cassandra.harry.visitors.GeneratingVisitor;
+import org.apache.cassandra.harry.visitors.MutatingRowVisitor;
+import org.apache.cassandra.harry.visitors.Visitor;
 import net.bytebuddy.ByteBuddy;
 import net.bytebuddy.dynamic.loading.ClassLoadingStrategy;
 import net.bytebuddy.implementation.MethodDelegation;
@@ -42,12 +46,9 @@ import org.apache.cassandra.distributed.Cluster;
 import org.apache.cassandra.distributed.api.ConsistencyLevel;
 import org.apache.cassandra.distributed.api.Feature;
 import org.apache.cassandra.distributed.api.IInvokableInstance;
-import org.apache.cassandra.distributed.fuzz.HarryHelper;
-import org.apache.cassandra.distributed.fuzz.InJVMTokenAwareVisitorExecutor;
-import org.apache.cassandra.distributed.fuzz.InJvmSut;
+import org.apache.cassandra.harry.HarryHelper;
 import org.apache.cassandra.distributed.shared.ClusterUtils;
 import org.apache.cassandra.distributed.shared.ClusterUtils.SerializableBiPredicate;
-import org.apache.cassandra.locator.ReplicationFactor;
 import org.apache.cassandra.tcm.Commit;
 import org.apache.cassandra.tcm.Epoch;
 import org.apache.cassandra.tcm.Transformation;
@@ -89,11 +90,6 @@ public class FailedLeaveTest extends FuzzTestBase
     throws Exception
     {
         ExecutorService es = Executors.newSingleThreadExecutor();
-        Configuration.ConfigurationBuilder configBuilder = HarryHelper.defaultConfiguration()
-                                                                      .setPartitionDescriptorSelector(new Configuration.DefaultPDSelectorConfiguration(1, 1))
-                                                                      .setClusteringDescriptorSelector(HarryHelper.defaultClusteringDescriptorSelectorConfiguration().setMaxPartitionSize(100).build());
-
-
         try (Cluster cluster = builder().withNodes(3)
                                         .withInstanceInitializer(BB::install)
                                         .appendConfig(c -> c.with(Feature.NETWORK))
@@ -101,7 +97,9 @@ public class FailedLeaveTest extends FuzzTestBase
         {
             IInvokableInstance cmsInstance = cluster.get(1);
             IInvokableInstance leavingInstance = cluster.get(2);
-            configBuilder.setSUT(() -> new InJvmSut(cluster));
+
+            Configuration.ConfigurationBuilder configBuilder = HarryHelper.defaultConfiguration()
+                                                                          .setSUT(() -> new InJvmSut(cluster));
             Run run = configBuilder.build().createRun();
 
             cluster.coordinator(1).execute("CREATE KEYSPACE " + run.schemaSpec.keyspace +
@@ -110,8 +108,12 @@ public class FailedLeaveTest extends FuzzTestBase
             cluster.coordinator(1).execute(run.schemaSpec.compile().cql(), ConsistencyLevel.ALL);
             ClusterUtils.waitForCMSToQuiesce(cluster, cmsInstance);
 
-            QuiescentLocalStateChecker model = new QuiescentLocalStateChecker(run, ReplicationFactor.fullOnly(2));
-            Visitor visitor = new GeneratingVisitor(run, new InJVMTokenAwareVisitorExecutor(run, MutatingRowVisitor::new, SystemUnderTest.ConsistencyLevel.ALL));
+            TokenPlacementModel.ReplicationFactor rf = new TokenPlacementModel.SimpleReplicationFactor(2);
+            QuiescentLocalStateChecker model = new QuiescentLocalStateChecker(run, rf);
+            Visitor visitor = new GeneratingVisitor(run, new InJVMTokenAwareVisitExecutor(run,
+                                                                                          MutatingRowVisitor::new,
+                                                                                          SystemUnderTest.ConsistencyLevel.ALL,
+                                                                                          rf));
             for (int i = 0; i < WRITES; i++)
                 visitor.visit();
 
diff --git a/test/distributed/org/apache/cassandra/distributed/test/log/FuzzTestBase.java b/test/distributed/org/apache/cassandra/distributed/test/log/FuzzTestBase.java
index 94df950c91..86e5f681e1 100644
--- a/test/distributed/org/apache/cassandra/distributed/test/log/FuzzTestBase.java
+++ b/test/distributed/org/apache/cassandra/distributed/test/log/FuzzTestBase.java
@@ -18,30 +18,13 @@
 
 package org.apache.cassandra.distributed.test.log;
 
-
-import java.util.ArrayList;
-import java.util.Arrays;
-import java.util.List;
 import java.util.concurrent.TimeUnit;
 
 import org.junit.BeforeClass;
 
-import harry.core.Run;
-import harry.data.ResultSetRow;
-import harry.model.OpSelectors;
-import harry.model.QuiescentChecker;
-import harry.model.sut.SystemUnderTest;
-import harry.operations.CompiledStatement;
-import harry.operations.Query;
 import org.apache.cassandra.distributed.Cluster;
-import org.apache.cassandra.distributed.api.IIsolatedExecutor;
-import org.apache.cassandra.distributed.fuzz.HarryHelper;
-import org.apache.cassandra.distributed.fuzz.InJvmSut;
-import org.apache.cassandra.distributed.test.ExecUtil;
 import org.apache.cassandra.distributed.test.TestBaseImpl;
-import org.apache.cassandra.locator.ReplicationFactor;
-
-import static harry.model.SelectHelper.resultSetToRow;
+import org.apache.cassandra.harry.HarryHelper;
 
 public class FuzzTestBase extends TestBaseImpl
 {
@@ -61,81 +44,4 @@ public class FuzzTestBase extends TestBaseImpl
                                           // unpaused before event expiration.
                                           .set("cms_await_timeout", String.format("%dms", TimeUnit.MINUTES.toMillis(10))));
     }
-
-    public static IIsolatedExecutor.SerializableRunnable toRunnable(ExecUtil.ThrowingSerializableRunnable runnable)
-    {
-        return () -> {
-            try
-            {
-                runnable.run();
-            }
-            catch (Throwable t)
-            {
-                System.out.println(t.getMessage());
-                t.printStackTrace();
-            }
-        };
-    }
-
-    public static class QuiescentLocalStateChecker extends QuiescentChecker
-    {
-        public final InJvmSut inJvmSut;
-        public final ReplicationFactor rf;
-        private final OpSelectors.PdSelector pdSelector;
-
-        public QuiescentLocalStateChecker(Run run)
-        {
-            this(run, null);
-        }
-
-        public QuiescentLocalStateChecker(Run run, ReplicationFactor rf)
-        {
-            super(run);
-            assert run.sut instanceof InJvmSut;
-
-            this.inJvmSut = (InJvmSut) run.sut;
-            this.rf = rf;
-            this.pdSelector = run.pdSelector;
-        }
-
-        public void validateAll()
-        {
-            for (int lts = 0; lts < clock.peek(); lts++)
-                validate(Query.selectPartition(schema, pdSelector.pd(lts, schema), false));
-        }
-
-        @Override
-        public void validate(Query query)
-        {
-            CompiledStatement compiled = query.toSelectStatement();
-            int[] replicas = inJvmSut.getReadReplicasFor(schema.inflatePartitionKey(query.pd), schema.keyspace, schema.table);
-            if (rf != null && replicas.length != rf.allReplicas)
-                throw new IllegalStateException(String.format("Total number of replicas %d does not match expectation %d",
-                                                              replicas.length, rf.allReplicas));
-
-            for (int node : replicas)
-            {
-                try
-                {
-                    validate(() -> {
-                        Object[][] objects = inJvmSut.execute(compiled.cql(),
-                                                              SystemUnderTest.ConsistencyLevel.NODE_LOCAL,
-                                                              node,
-                                                              compiled.bindings());
-                        List<ResultSetRow> result = new ArrayList<>();
-                        for (Object[] obj : objects)
-                            result.add(resultSetToRow(query.schemaSpec, clock, obj));
-
-                        return result;
-                    }, query);
-                }
-                catch (ValidationException e)
-                {
-                    throw new AssertionError(String.format("Caught error while validating replica %d of replica set %s",
-                                                           node, Arrays.toString(replicas)),
-                                             e);
-                }
-            }
-        }
-    }
 }
diff --git a/test/distributed/org/apache/cassandra/distributed/test/log/MetadataChangeSimulationTest.java b/test/distributed/org/apache/cassandra/distributed/test/log/MetadataChangeSimulationTest.java
index 857db9d8ba..036e5fff71 100644
--- a/test/distributed/org/apache/cassandra/distributed/test/log/MetadataChangeSimulationTest.java
+++ b/test/distributed/org/apache/cassandra/distributed/test/log/MetadataChangeSimulationTest.java
@@ -44,7 +44,7 @@ import org.apache.cassandra.dht.Murmur3Partitioner;
 import org.apache.cassandra.dht.Murmur3Partitioner.LongToken;
 import org.apache.cassandra.dht.Range;
 import org.apache.cassandra.dht.Token;
-import org.apache.cassandra.distributed.test.log.PlacementSimulator.SimulatedPlacements;
+import org.apache.cassandra.harry.sut.TokenPlacementModel;
 import org.apache.cassandra.locator.EndpointsForRange;
 import org.apache.cassandra.locator.InetAddressAndPort;
 import org.apache.cassandra.locator.CMSPlacementStrategy;
@@ -65,8 +65,8 @@ import org.apache.cassandra.tcm.ownership.VersionedEndpoints;
 import org.apache.cassandra.tcm.transformations.Register;
 import org.apache.cassandra.tcm.transformations.SealPeriod;
 
-import static org.apache.cassandra.distributed.test.log.PlacementSimulator.Node;
-import static org.apache.cassandra.distributed.test.log.PlacementSimulator.nodeFactoryHumanReadable;
+import static org.apache.cassandra.distributed.test.log.PlacementSimulator.*;
+import static org.apache.cassandra.harry.sut.TokenPlacementModel.*;
 
 public class MetadataChangeSimulationTest extends CMSTestBase
 {
@@ -88,7 +88,7 @@ public class MetadataChangeSimulationTest extends CMSTestBase
         {
             for (int rf : new int[]{ 2, 3, 5 })
             {
-                simulate(50, new PlacementSimulator.NtsReplicationFactor(3, rf), concurrency);
+                simulate(50, new NtsReplicationFactor(3, rf), concurrency);
             }
         }
     }
@@ -100,7 +100,7 @@ public class MetadataChangeSimulationTest extends CMSTestBase
         {
             for (int rf : new int[]{ 2, 3, 5 })
             {
-                simulate(50, new PlacementSimulator.SimpleReplicationFactor(rf), concurrency);
+                simulate(50, new SimpleReplicationFactor(rf), concurrency);
             }
         }
     }
@@ -110,49 +110,49 @@ public class MetadataChangeSimulationTest extends CMSTestBase
     {
         for (int i = 0; i < 4; i++)
         {
-            testMoveReal(new PlacementSimulator.NtsReplicationFactor(1, 3), i, 150, 4);
-            testMoveReal(new PlacementSimulator.NtsReplicationFactor(1, 3), i, 350, 4);
-            testMoveReal(new PlacementSimulator.NtsReplicationFactor(1, 3), i, 550, 4);
+            testMoveReal(new NtsReplicationFactor(1, 3), i, 150, 4);
+            testMoveReal(new NtsReplicationFactor(1, 3), i, 350, 4);
+            testMoveReal(new NtsReplicationFactor(1, 3), i, 550, 4);
         }
 
         for (int i = 0; i < 5; i++)
         {
-            testMoveReal(new PlacementSimulator.NtsReplicationFactor(1, 3), i, 150, 5);
-            testMoveReal(new PlacementSimulator.NtsReplicationFactor(1, 3), i, 350, 5);
-            testMoveReal(new PlacementSimulator.NtsReplicationFactor(1, 3), i, 650, 5);
+            testMoveReal(new NtsReplicationFactor(1, 3), i, 150, 5);
+            testMoveReal(new NtsReplicationFactor(1, 3), i, 350, 5);
+            testMoveReal(new NtsReplicationFactor(1, 3), i, 650, 5);
         }
 
         for (int i = 0; i < 10; i++)
         {
-            testMoveReal(new PlacementSimulator.NtsReplicationFactor(1, 3), i, 150, 10);
-            testMoveReal(new PlacementSimulator.NtsReplicationFactor(1, 3), i, 550, 10);
-            testMoveReal(new PlacementSimulator.NtsReplicationFactor(1, 3), i, 1050, 10);
+            testMoveReal(new NtsReplicationFactor(1, 3), i, 150, 10);
+            testMoveReal(new NtsReplicationFactor(1, 3), i, 550, 10);
+            testMoveReal(new NtsReplicationFactor(1, 3), i, 1050, 10);
         }
 
         for (int i = 0; i < 9; i++)
         {
-            testMoveReal(new PlacementSimulator.NtsReplicationFactor(3, 3), i, 350, 9);
-            testMoveReal(new PlacementSimulator.NtsReplicationFactor(3, 3), i, 550, 9);
-            testMoveReal(new PlacementSimulator.NtsReplicationFactor(3, 3), i, 1050, 9);
+            testMoveReal(new NtsReplicationFactor(3, 3), i, 350, 9);
+            testMoveReal(new NtsReplicationFactor(3, 3), i, 550, 9);
+            testMoveReal(new NtsReplicationFactor(3, 3), i, 1050, 9);
         }
 
         for (int i = 0; i < 10; i++)
         {
-            testMoveReal(new PlacementSimulator.NtsReplicationFactor(3, 3), i, 350, 10);
-            testMoveReal(new PlacementSimulator.NtsReplicationFactor(3, 3), i, 550, 10);
-            testMoveReal(new PlacementSimulator.NtsReplicationFactor(3, 3), i, 1050, 10);
+            testMoveReal(new NtsReplicationFactor(3, 3), i, 350, 10);
+            testMoveReal(new NtsReplicationFactor(3, 3), i, 550, 10);
+            testMoveReal(new NtsReplicationFactor(3, 3), i, 1050, 10);
         }
 
         for (int i = 0; i < 18; i++)
         {
-            testMoveReal(new PlacementSimulator.NtsReplicationFactor(3, 3), i, 350, 18);
-            testMoveReal(new PlacementSimulator.NtsReplicationFactor(3, 3), i, 1050, 18);
-            testMoveReal(new PlacementSimulator.NtsReplicationFactor(3, 3), i, 2050, 18);
+            testMoveReal(new NtsReplicationFactor(3, 3), i, 350, 18);
+            testMoveReal(new NtsReplicationFactor(3, 3), i, 1050, 18);
+            testMoveReal(new NtsReplicationFactor(3, 3), i, 2050, 18);
         }
 
     }
 
-    public void testMoveReal(PlacementSimulator.ReplicationFactor rf, int moveNodeId, long moveToken, int numberOfNodes) throws Throwable
+    public void testMoveReal(ReplicationFactor rf, int moveNodeId, long moveToken, int numberOfNodes) throws Throwable
     {
         try (CMSTestBase.CMSSut sut = new CMSTestBase.CMSSut(AtomicLongBackedProcessor::new, false, rf))
         {
@@ -181,14 +181,14 @@ public class MetadataChangeSimulationTest extends CMSTestBase
     @Test
     public void testLeaveReal() throws Throwable
     {
-        testLeaveReal(new PlacementSimulator.NtsReplicationFactor(1, 3), 1);
-        testLeaveReal(new PlacementSimulator.NtsReplicationFactor(1, 3), 5);
-        testLeaveReal(new PlacementSimulator.NtsReplicationFactor(3, 3), 1);
-        testLeaveReal(new PlacementSimulator.NtsReplicationFactor(3, 3), 5);
+        testLeaveReal(new NtsReplicationFactor(1, 3), 1);
+        testLeaveReal(new NtsReplicationFactor(1, 3), 5);
+        testLeaveReal(new NtsReplicationFactor(3, 3), 1);
+        testLeaveReal(new NtsReplicationFactor(3, 3), 5);
 
     }
 
-    public void testLeaveReal(PlacementSimulator.ReplicationFactor rf, int decomNodeId) throws Throwable
+    public void testLeaveReal(ReplicationFactor rf, int decomNodeId) throws Throwable
     {
         try (CMSTestBase.CMSSut sut = new CMSTestBase.CMSSut(AtomicLongBackedProcessor::new, false, rf))
         {
@@ -217,11 +217,11 @@ public class MetadataChangeSimulationTest extends CMSTestBase
     @Test
     public void testJoinReal() throws Throwable
     {
-        testJoinReal(new PlacementSimulator.NtsReplicationFactor(3, 3), 1);
-        testJoinReal(new PlacementSimulator.NtsReplicationFactor(3, 3), 5);
+        testJoinReal(new NtsReplicationFactor(3, 3), 1);
+        testJoinReal(new NtsReplicationFactor(3, 3), 5);
     }
 
-    public void testJoinReal(PlacementSimulator.ReplicationFactor rf, int joinNodeId) throws Throwable
+    public void testJoinReal(ReplicationFactor rf, int joinNodeId) throws Throwable
     {
         try (CMSTestBase.CMSSut sut = new CMSTestBase.CMSSut(AtomicLongBackedProcessor::new, false, rf))
         {
@@ -250,14 +250,14 @@ public class MetadataChangeSimulationTest extends CMSTestBase
         }
     }
 
-    public void simulate(int toBootstrap, PlacementSimulator.ReplicationFactor rf, int concurrency) throws Throwable
+    public void simulate(int toBootstrap, ReplicationFactor rf, int concurrency) throws Throwable
     {
         System.out.printf("RUNNING SIMULATION. TO BOOTSTRAP: %s, RF: %s, CONCURRENCY: %s%n",
                           toBootstrap, rf, concurrency);
         long startTime = System.currentTimeMillis();
         ModelChecker<ModelState, CMSSut> modelChecker = new ModelChecker<>();
         ClusterMetadataService.unsetInstance();
-        modelChecker.init(ModelState.empty(PlacementSimulator.nodeFactory(), toBootstrap, concurrency),
+        modelChecker.init(ModelState.empty(nodeFactory(), toBootstrap, concurrency),
                           new CMSSut(AtomicLongBackedProcessor::new, false, rf))
                     // Sequentially bootstrap rf nodes first
                     .step((state, sut) -> state.currentNodes.isEmpty(),
@@ -380,7 +380,7 @@ public class MetadataChangeSimulationTest extends CMSTestBase
         Random random = new Random(1L);
         for (int i = 0; i < 10; i++)
         {
-            PlacementSimulator.ReplicationFactor ntsRf = new PlacementSimulator.NtsReplicationFactor(3, 3);
+            ReplicationFactor ntsRf = new NtsReplicationFactor(3, 3);
             Map<String, Integer> cmsRf = new HashMap<>();
             for (String s : ntsRf.asMap().keySet())
                 cmsRf.put(s, 3);
@@ -389,12 +389,12 @@ public class MetadataChangeSimulationTest extends CMSTestBase
         }
     }
 
-    public void simulateBounces(PlacementSimulator.ReplicationFactor rf, CMSPlacementStrategy CMSConfigurationStrategy, Random random) throws Throwable
+    public void simulateBounces(ReplicationFactor rf, CMSPlacementStrategy CMSConfigurationStrategy, Random random) throws Throwable
     {
 
         try(CMSSut sut = new CMSSut(AtomicLongBackedProcessor::new, false, rf))
         {
-            ModelState state = ModelState.empty(PlacementSimulator.nodeFactory(), 300, 1);
+            ModelState state = ModelState.empty(nodeFactory(), 300, 1);
 
             for (Map.Entry<String, Integer> e : rf.asMap().entrySet())
             {
@@ -553,16 +553,16 @@ public class MetadataChangeSimulationTest extends CMSTestBase
         return sb.toString();
     }
 
-    public static void match(PlacementForRange actual, Map<PlacementSimulator.Range, List<Node>> predicted) throws Throwable
+    public static void match(PlacementForRange actual, Map<TokenPlacementModel.Range, List<Node>> predicted) throws Throwable
     {
         Map<Range<Token>, VersionedEndpoints.ForRange> actualGroups = actual.replicaGroups();
         assert predicted.size() == actualGroups.size() :
         String.format("\nPredicted:\n%s(%d)" +
                       "\nActual:\n%s(%d)", toString(predicted), predicted.size(), toString(actual.replicaGroups()), actualGroups.size());
 
-        for (Map.Entry<PlacementSimulator.Range, List<Node>> entry : predicted.entrySet())
+        for (Map.Entry<TokenPlacementModel.Range, List<Node>> entry : predicted.entrySet())
         {
-            PlacementSimulator.Range range = entry.getKey();
+            TokenPlacementModel.Range range = entry.getKey();
             List<Node> predictedNodes = entry.getValue();
             Range<Token> predictedRange = new Range<Token>(new Murmur3Partitioner.LongToken(range.start),
                                                            new Murmur3Partitioner.LongToken(range.end));
@@ -653,7 +653,7 @@ public class MetadataChangeSimulationTest extends CMSTestBase
     }
 
     public static void validatePlacements(IPartitioner partitioner,
-                                          PlacementSimulator.ReplicationFactor rf,
+                                          ReplicationFactor rf,
                                           ModelState modelState,
                                           DataPlacements placements)
     {
@@ -678,7 +678,7 @@ public class MetadataChangeSimulationTest extends CMSTestBase
         Assert.assertEquals(new TreeSet<>(l), new TreeSet<>(r));
     }
 
-    public static void validatePlacementsInternal(PlacementSimulator.ReplicationFactor rf, List<SimulatedOperation> opStates, List<Range<Token>> expectedRanges, PlacementForRange placements, boolean allowPending)
+    public static void validatePlacementsInternal(ReplicationFactor rf, List<SimulatedOperation> opStates, List<Range<Token>> expectedRanges, PlacementForRange placements, boolean allowPending)
     {
         int overreplicated = 0;
         for (Range<Token> range : expectedRanges)
@@ -719,7 +719,7 @@ public class MetadataChangeSimulationTest extends CMSTestBase
             }
         }
 
-        if (allowPending && rf instanceof PlacementSimulator.SimpleReplicationFactor)
+        if (allowPending && rf instanceof SimpleReplicationFactor)
         {
             int bootstrappingNodes = 0;
             int movingNodes = 0;
diff --git a/test/distributed/org/apache/cassandra/distributed/test/log/ModelState.java b/test/distributed/org/apache/cassandra/distributed/test/log/ModelState.java
index b1e0a6d7d5..7a0a61604d 100644
--- a/test/distributed/org/apache/cassandra/distributed/test/log/ModelState.java
+++ b/test/distributed/org/apache/cassandra/distributed/test/log/ModelState.java
@@ -28,6 +28,8 @@ import java.util.Random;
 import java.util.Set;
 import java.util.TreeMap;
 
+import org.apache.cassandra.harry.sut.TokenPlacementModel;
+
 public class ModelState
 {
     public final int maxClusterSize;
@@ -37,17 +39,17 @@ public class ModelState
     public final int[] cancelled;
     public final int[] finished;
     public final int bootstrappingCount;
-    public final List<PlacementSimulator.Node> currentNodes;
-    public final Map<String, List<PlacementSimulator.Node>> nodesByDc;
-    public final List<PlacementSimulator.Node> registeredNodes;
-    public final List<PlacementSimulator.Node> leavingNodes;
-    public final List<PlacementSimulator.Node> movingNodes;
+    public final List<TokenPlacementModel.Node> currentNodes;
+    public final Map<String, List<TokenPlacementModel.Node>> nodesByDc;
+    public final List<TokenPlacementModel.Node> registeredNodes;
+    public final List<TokenPlacementModel.Node> leavingNodes;
+    public final List<TokenPlacementModel.Node> movingNodes;
     public final List<SimulatedOperation> inFlightOperations;
     public final PlacementSimulator.SimulatedPlacements simulatedPlacements;
-    public final PlacementSimulator.NodeFactory nodeFactory;
+    public final TokenPlacementModel.NodeFactory nodeFactory;
 
 
-    public static ModelState empty(PlacementSimulator.NodeFactory nodeFactory, int maxClusterSize, int maxConcurrency)
+    public static ModelState empty(TokenPlacementModel.NodeFactory nodeFactory, int maxClusterSize, int maxConcurrency)
     {
         return new ModelState(maxClusterSize, maxConcurrency,
                               0, 0,
@@ -58,11 +60,11 @@ public class ModelState
                               nodeFactory);
     }
 
-    public static Map<String, List<PlacementSimulator.Node>> groupByDc(List<PlacementSimulator.Node> nodes)
+    public static Map<String, List<TokenPlacementModel.Node>> groupByDc(List<TokenPlacementModel.Node> nodes)
     {
         // using treemap here since it is much easier to read/debug when it comes to that
-        Map<String, List<PlacementSimulator.Node>> grouped = new TreeMap<>();
-        for (PlacementSimulator.Node node : nodes)
+        Map<String, List<TokenPlacementModel.Node>> grouped = new TreeMap<>();
+        for (TokenPlacementModel.Node node : nodes)
         {
             grouped.computeIfAbsent(node.dc(), (k) -> new ArrayList<>())
                    .add(node);
@@ -76,13 +78,13 @@ public class ModelState
                        int rejected,
                        int[] cancelled,
                        int[] finished,
-                       List<PlacementSimulator.Node> currentNodes,
-                       List<PlacementSimulator.Node> registeredNodes,
-                       List<PlacementSimulator.Node> leavingNodes,
-                       List<PlacementSimulator.Node> movingNodes,
+                       List<TokenPlacementModel.Node> currentNodes,
+                       List<TokenPlacementModel.Node> registeredNodes,
+                       List<TokenPlacementModel.Node> leavingNodes,
+                       List<TokenPlacementModel.Node> movingNodes,
                        List<SimulatedOperation> operationStates,
                        PlacementSimulator.SimulatedPlacements simulatedPlacements,
-                       PlacementSimulator.NodeFactory nodeFactory)
+                       TokenPlacementModel.NodeFactory nodeFactory)
     {
         this.maxClusterSize = maxClusterSize;
         this.maxConcurrency = maxConcurrency;
@@ -118,30 +120,30 @@ public class ModelState
         return withinConcurrencyLimit() && bootstrappingCount + currentNodes.size() < maxClusterSize;
     }
 
-    public boolean shouldLeave(PlacementSimulator.ReplicationFactor rf, Random rng)
+    public boolean shouldLeave(TokenPlacementModel.ReplicationFactor rf, Random rng)
     {
         return canRemove(rf) && rng.nextDouble() > 0.7;
     }
 
-    public boolean shouldMove(PlacementSimulator.ReplicationFactor rf, Random rng)
+    public boolean shouldMove(TokenPlacementModel.ReplicationFactor rf, Random rng)
     {
         return canRemove(rf) && rng.nextDouble() > 0.7;
     }
 
-    public boolean shouldReplace(PlacementSimulator.ReplicationFactor rf, Random rng)
+    public boolean shouldReplace(TokenPlacementModel.ReplicationFactor rf, Random rng)
     {
         return canRemove(rf) && rng.nextDouble() > 0.8;
     }
 
-    private boolean canRemove(PlacementSimulator.ReplicationFactor rfs)
+    private boolean canRemove(TokenPlacementModel.ReplicationFactor rfs)
     {
         if (!withinConcurrencyLimit()) return false;
         for (Map.Entry<String, Integer> e : rfs.asMap().entrySet())
         {
             String dc = e.getKey();
             int rf = e.getValue();
-            List<PlacementSimulator.Node> nodes = nodesByDc.get(dc);
-            Set<PlacementSimulator.Node> nodesInDc = nodes == null ? new HashSet<>() : new HashSet<>(nodes);
+            List<TokenPlacementModel.Node> nodes = nodesByDc.get(dc);
+            Set<TokenPlacementModel.Node> nodesInDc = nodes == null ? new HashSet<>() : new HashSet<>(nodes);
             for (SimulatedOperation op : inFlightOperations)
                 nodesInDc.removeAll(Arrays.asList(op.nodes));
             if (nodesInDc.size() > rf)
@@ -179,13 +181,13 @@ public class ModelState
         // join/replace/leave/move
         private int[] cancelled;
         private int[] finished;
-        private List<PlacementSimulator.Node> currentNodes;
-        private List<PlacementSimulator.Node> registeredNodes;
-        private List<PlacementSimulator.Node> leavingNodes;
-        private List<PlacementSimulator.Node> movingNodes;
+        private List<TokenPlacementModel.Node> currentNodes;
+        private List<TokenPlacementModel.Node> registeredNodes;
+        private List<TokenPlacementModel.Node> leavingNodes;
+        private List<TokenPlacementModel.Node> movingNodes;
         private List<SimulatedOperation> operationStates;
         private PlacementSimulator.SimulatedPlacements simulatedPlacements;
-        private PlacementSimulator.NodeFactory nodeFactory;
+        private TokenPlacementModel.NodeFactory nodeFactory;
 
         private Transformer(ModelState source)
         {
@@ -254,26 +256,26 @@ public class ModelState
             return this;
         }
 
-        public Transformer withJoined(PlacementSimulator.Node node)
+        public Transformer withJoined(TokenPlacementModel.Node node)
         {
             addToCluster(node);
             finished[0]++;
             return this;
         }
 
-        public Transformer recycleRejected(PlacementSimulator.Node node)
+        public Transformer recycleRejected(TokenPlacementModel.Node node)
         {
             registeredNodes = new ArrayList<>(registeredNodes);
             registeredNodes.add(node);
             return this;
         }
 
-        public Transformer withMoved(PlacementSimulator.Node movingNode, PlacementSimulator.Node movedTo)
+        public Transformer withMoved(TokenPlacementModel.Node movingNode, TokenPlacementModel.Node movedTo)
         {
             assert currentNodes.contains(movingNode) : movingNode;
-            List<PlacementSimulator.Node> tmp = currentNodes;
+            List<TokenPlacementModel.Node> tmp = currentNodes;
             currentNodes = new ArrayList<>();
-            for (PlacementSimulator.Node n : tmp)
+            for (TokenPlacementModel.Node n : tmp)
             {
                 if (n.idx() == movingNode.idx())
                     currentNodes.add(movedTo);
@@ -288,14 +290,14 @@ public class ModelState
             return this;
         }
 
-        private void addToCluster(PlacementSimulator.Node node)
+        private void addToCluster(TokenPlacementModel.Node node)
         {
             // called during both join and replacement
             currentNodes = new ArrayList<>(currentNodes);
             currentNodes.add(node);
         }
 
-        public Transformer markMoving(PlacementSimulator.Node moving)
+        public Transformer markMoving(TokenPlacementModel.Node moving)
         {
             assert currentNodes.contains(moving);
             movingNodes = new ArrayList<>(movingNodes);
@@ -303,7 +305,7 @@ public class ModelState
             return this;
         }
 
-        public Transformer markLeaving(PlacementSimulator.Node leaving)
+        public Transformer markLeaving(TokenPlacementModel.Node leaving)
         {
             assert currentNodes.contains(leaving);
             leavingNodes = new ArrayList<>(leavingNodes);
@@ -311,7 +313,7 @@ public class ModelState
             return this;
         }
 
-        public Transformer withLeft(PlacementSimulator.Node node)
+        public Transformer withLeft(TokenPlacementModel.Node node)
         {
             assert currentNodes.contains(node);
             // for now... assassinate may change this assertion
@@ -321,7 +323,7 @@ public class ModelState
             return this;
         }
 
-        private void removeFromCluster(PlacementSimulator.Node node)
+        private void removeFromCluster(TokenPlacementModel.Node node)
         {
             // called during both decommission and replacement
             currentNodes = new ArrayList<>(currentNodes);
@@ -330,7 +332,7 @@ public class ModelState
             leavingNodes.remove(node);
         }
 
-        public Transformer withReplaced(PlacementSimulator.Node oldNode, PlacementSimulator.Node newNode)
+        public Transformer withReplaced(TokenPlacementModel.Node oldNode, TokenPlacementModel.Node newNode)
         {
             addToCluster(newNode);
             removeFromCluster(oldNode);
diff --git a/test/distributed/org/apache/cassandra/distributed/test/log/OperationalEquivalenceTest.java b/test/distributed/org/apache/cassandra/distributed/test/log/OperationalEquivalenceTest.java
index 23cb98a283..58732e3641 100644
--- a/test/distributed/org/apache/cassandra/distributed/test/log/OperationalEquivalenceTest.java
+++ b/test/distributed/org/apache/cassandra/distributed/test/log/OperationalEquivalenceTest.java
@@ -31,6 +31,7 @@ import org.junit.Assert;
 import org.junit.Test;
 
 import org.apache.cassandra.config.DatabaseDescriptor;
+import org.apache.cassandra.harry.sut.TokenPlacementModel;
 import org.apache.cassandra.locator.EndpointsForRange;
 import org.apache.cassandra.locator.Replica;
 import org.apache.cassandra.schema.KeyspaceParams;
@@ -60,9 +61,9 @@ public class OperationalEquivalenceTest extends CMSTestBase
     }
 
     // Private to this class for now as this is only a limited implementation
-    private static class TransientReplicationFactor extends PlacementSimulator.ReplicationFactor
+    private static class TransientReplicationFactor extends TokenPlacementModel.ReplicationFactor
     {
-        private final PlacementSimulator.Lookup lookup = new PlacementSimulator.DefaultLookup();
+        private final TokenPlacementModel.Lookup lookup = new TokenPlacementModel.DefaultLookup();
         private final int dcs;
         private final int full;
         private final int trans;
@@ -96,7 +97,7 @@ public class OperationalEquivalenceTest extends CMSTestBase
             throw new IllegalStateException("Does not work with transient replication");
         }
 
-        public PlacementSimulator.ReplicatedRanges replicate(PlacementSimulator.Range[] ranges, List<PlacementSimulator.Node> nodes)
+        public TokenPlacementModel.ReplicatedRanges replicate(TokenPlacementModel.Range[] ranges, List<TokenPlacementModel.Node> nodes)
         {
             throw new IllegalStateException("Does not work with transient replication (yet)");
         }
@@ -105,28 +106,28 @@ public class OperationalEquivalenceTest extends CMSTestBase
     @Test
     public void testMove() throws Exception
     {
-        testMove(new PlacementSimulator.SimpleReplicationFactor(2));
-        testMove(new PlacementSimulator.SimpleReplicationFactor(3));
-        testMove(new PlacementSimulator.SimpleReplicationFactor(5));
+        testMove(new TokenPlacementModel.SimpleReplicationFactor(2));
+        testMove(new TokenPlacementModel.SimpleReplicationFactor(3));
+        testMove(new TokenPlacementModel.SimpleReplicationFactor(5));
 
-        testMove(new PlacementSimulator.NtsReplicationFactor(1, 2));
-        testMove(new PlacementSimulator.NtsReplicationFactor(1, 3));
-        testMove(new PlacementSimulator.NtsReplicationFactor(1, 5));
+        testMove(new TokenPlacementModel.NtsReplicationFactor(1, 2));
+        testMove(new TokenPlacementModel.NtsReplicationFactor(1, 3));
+        testMove(new TokenPlacementModel.NtsReplicationFactor(1, 5));
 
-        testMove(new PlacementSimulator.NtsReplicationFactor(3, 2));
-        testMove(new PlacementSimulator.NtsReplicationFactor(3, 3));
-        testMove(new PlacementSimulator.NtsReplicationFactor(3, 5));
+        testMove(new TokenPlacementModel.NtsReplicationFactor(3, 2));
+        testMove(new TokenPlacementModel.NtsReplicationFactor(3, 3));
+        testMove(new TokenPlacementModel.NtsReplicationFactor(3, 5));
 
         testMove(new TransientReplicationFactor(3, 3, 1));
         testMove(new TransientReplicationFactor(3, 3, 2));
     }
 
-    public void testMove(PlacementSimulator.ReplicationFactor rf) throws Exception
+    public void testMove(TokenPlacementModel.ReplicationFactor rf) throws Exception
     {
-        PlacementSimulator.NodeFactory nodeFactory = PlacementSimulator.nodeFactory();
+        TokenPlacementModel.NodeFactory nodeFactory = TokenPlacementModel.nodeFactory();
 
         ClusterMetadata withMove = null;
-        List<PlacementSimulator.Node> equivalentNodes = new ArrayList<>();
+        List<TokenPlacementModel.Node> equivalentNodes = new ArrayList<>();
         int nodes = 30;
         try (CMSSut sut = new CMSSut(AtomicLongBackedProcessor::new, false, rf))
         {
@@ -134,14 +135,14 @@ public class OperationalEquivalenceTest extends CMSTestBase
             for (int i = 0; i < nodes; i++)
             {
                 int dc = toDc(i, rf);
-                PlacementSimulator.Node node = nodeFactory.make(counter.incrementAndGet(), dc, 1);
+                TokenPlacementModel.Node node = nodeFactory.make(counter.incrementAndGet(), dc, 1);
                 sut.service.commit(new Register(new NodeAddresses(node.addr()), new Location(node.dc(), node.rack()), NodeVersion.CURRENT));
                 sut.service.commit(new UnsafeJoin(node.nodeId(), Collections.singleton(node.longToken()), sut.service.placementProvider()));
                 equivalentNodes.add(node);
             }
 
-            PlacementSimulator.Node toMove = equivalentNodes.get(rng.nextInt(equivalentNodes.size()));
-            PlacementSimulator.Node moved = toMove.withNewToken();
+            TokenPlacementModel.Node toMove = equivalentNodes.get(rng.nextInt(equivalentNodes.size()));
+            TokenPlacementModel.Node moved = toMove.withNewToken();
             equivalentNodes.set(equivalentNodes.indexOf(toMove), moved);
 
             Move plan = SimulatedOperation.prepareMove(sut, toMove, moved.longToken()).get();
@@ -156,12 +157,12 @@ public class OperationalEquivalenceTest extends CMSTestBase
                          withMove.placements);
     }
 
-    private static ClusterMetadata simulateAndCompare(PlacementSimulator.ReplicationFactor rf, List<PlacementSimulator.Node> nodes) throws Exception
+    private static ClusterMetadata simulateAndCompare(TokenPlacementModel.ReplicationFactor rf, List<TokenPlacementModel.Node> nodes) throws Exception
     {
         Collections.shuffle(nodes, rng);
         try (CMSSut sut = new CMSSut(AtomicLongBackedProcessor::new, false, rf))
         {
-            for (PlacementSimulator.Node node : nodes)
+            for (TokenPlacementModel.Node node : nodes)
             {
                 sut.service.commit(new Register(new NodeAddresses(node.addr()), new Location(node.dc(), node.rack()), NodeVersion.CURRENT));
                 sut.service.commit(new UnsafeJoin(node.nodeId(), Collections.singleton(node.longToken()), sut.service.placementProvider()));
@@ -196,7 +197,7 @@ public class OperationalEquivalenceTest extends CMSTestBase
     {
         return ep.stream().sorted(Replica::compareTo).collect(Collectors.toList());
     }
-    private static int toDc(int i, PlacementSimulator.ReplicationFactor rf)
+    private static int toDc(int i, TokenPlacementModel.ReplicationFactor rf)
     {
         return (i % rf.dcs()) + 1;
     }
diff --git a/test/distributed/org/apache/cassandra/distributed/test/log/PlacementSimulator.java b/test/distributed/org/apache/cassandra/distributed/test/log/PlacementSimulator.java
index 4d44c3b775..bd9e5c577f 100644
--- a/test/distributed/org/apache/cassandra/distributed/test/log/PlacementSimulator.java
+++ b/test/distributed/org/apache/cassandra/distributed/test/log/PlacementSimulator.java
@@ -24,23 +24,25 @@ import java.io.FileNotFoundException;
 import java.io.FileOutputStream;
 import java.io.IOException;
 import java.io.OutputStreamWriter;
-import java.net.UnknownHostException;
-import java.util.*;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.HashSet;
+import java.util.Iterator;
+import java.util.List;
+import java.util.Map;
+import java.util.NavigableMap;
+import java.util.Random;
+import java.util.Set;
+import java.util.TreeMap;
 import java.util.function.Function;
 import java.util.function.Predicate;
 import java.util.stream.Collectors;
 
 import org.junit.Assert;
 
-import harry.generators.PCGFastPure;
-import org.apache.cassandra.dht.Murmur3Partitioner;
-import org.apache.cassandra.distributed.api.TokenSupplier;
-import org.apache.cassandra.locator.InetAddressAndPort;
-;
-import org.apache.cassandra.schema.KeyspaceParams;
-import org.apache.cassandra.tcm.ClusterMetadata;
-import org.apache.cassandra.tcm.membership.Location;
-import org.apache.cassandra.tcm.membership.NodeId;
+import static org.apache.cassandra.harry.sut.TokenPlacementModel.Node;
+import static org.apache.cassandra.harry.sut.TokenPlacementModel.Range;
+import static org.apache.cassandra.harry.sut.TokenPlacementModel.ReplicationFactor;
 
 /**
  * A small class that helps to avoid doing mental arithmetics on ranges.
@@ -398,7 +400,7 @@ public class PlacementSimulator
         List<Node> finalNodes = new ArrayList<>();
         for (int i = 0; i < origNodes.size(); i++)
         {
-            if (origNodes.get(i).nodeIdx == movingNode.nodeIdx)
+            if (origNodes.get(i).idx() == movingNode.idx())
                 continue;
             finalNodes.add(origNodes.get(i));
         }
@@ -674,8 +676,8 @@ public class PlacementSimulator
             // bootstrap_diffBased implementation and the real code doesn't do this, only the endpoint matters for
             // correctness, so we limit this comparison to endpoints only.
             Assert.assertEquals(String.format("For key: %s\n", k),
-                                expected.get(k).stream().map(n -> n.nodeIdx).sorted().collect(Collectors.toList()),
-                                actual.get(k).stream().map(n -> n.nodeIdx).sorted().collect(Collectors.toList()));
+                                expected.get(k).stream().map(n -> n.idx()).sorted().collect(Collectors.toList()),
+                                actual.get(k).stream().map(n -> n.idx()).sorted().collect(Collectors.toList()));
         });
     }
 
@@ -935,199 +937,6 @@ public class PlacementSimulator
         return Collections.unmodifiableList(newNodes);
     }
 
-    /**
-     * Replicate ranges to rf nodes.
-     */
-    private static ReplicatedRanges combine(NavigableMap<Range, Map<String, List<Node>>> orig)
-    {
-
-        Range[] ranges = new Range[orig.size()];
-        int idx = 0;
-        NavigableMap<Range, List<Node>> flattened = new TreeMap<>();
-        for (Map.Entry<Range, Map<String, List<Node>>> e : orig.entrySet())
-        {
-            List<Node> placementsForRange = new ArrayList<>();
-            for (List<Node> v : e.getValue().values())
-                placementsForRange.addAll(v);
-            ranges[idx++] = e.getKey();
-            flattened.put(e.getKey(), placementsForRange);
-        }
-        return new ReplicatedRanges(ranges, flattened);
-    }
-
-    public static class ReplicatedRanges
-    {
-        private final Range[] ranges;
-        private final NavigableMap<Range, List<Node>> placementsForRange;
-
-        public ReplicatedRanges(Range[] ranges, NavigableMap<Range, List<Node>> placementsForRange)
-        {
-            this.ranges = ranges;
-            this.placementsForRange = placementsForRange;
-        }
-
-        public List<Node> replicasFor(long token)
-        {
-            int idx = indexedBinarySearch(ranges, range -> {
-                // exclusive start, so token at the start belongs to a lower range
-                if (token <= range.start)
-                    return 1;
-                // ie token > start && token <= end
-                if (token <= range.end ||range.end == Long.MIN_VALUE)
-                    return 0;
-
-                return -1;
-            });
-            assert idx >= 0 : String.format("Somehow ranges %s do not contain token %d", Arrays.toString(ranges), token);
-            return placementsForRange.get(ranges[idx]);
-        }
-
-        public NavigableMap<Range, List<Node>> asMap()
-        {
-            return placementsForRange;
-        }
-
-        private static <T> int indexedBinarySearch(T[] arr, CompareTo<T> comparator)
-        {
-            int low = 0;
-            int high = arr.length - 1;
-
-            while (low <= high)
-            {
-                int mid = (low + high) >>> 1;
-                T midEl = arr[mid];
-                int cmp = comparator.compareTo(midEl);
-
-                if (cmp < 0)
-                    low = mid + 1;
-                else if (cmp > 0)
-                    high = mid - 1;
-                else
-                    return mid;
-            }
-            return -(low + 1); // key not found
-        }
-    }
-
-    public interface CompareTo<V>
-    {
-        int compareTo(V v);
-    }
-
-    private static <K extends Comparable<K>, T1, T2> Map<K, T2> mapValues(Map<K, T1> allDCs, Function<T1, T2> map)
-    {
-        NavigableMap<K, T2> res = new TreeMap<>();
-        for (Map.Entry<K, T1> e : allDCs.entrySet())
-        {
-            res.put(e.getKey(), map.apply(e.getValue()));
-        }
-        return res;
-    }
-
-    public static Map<String, List<Node>> nodesByDC(List<Node> nodes)
-    {
-        Map<String, List<Node>> nodesByDC = new HashMap<>();
-        for (Node node : nodes)
-            nodesByDC.computeIfAbsent(node.dc(), (k) -> new ArrayList<>()).add(node);
-
-        return nodesByDC;
-    }
-
-    public static Map<String, Set<String>> racksByDC(List<Node> nodes)
-    {
-        Map<String, Set<String>> racksByDC = new HashMap<>();
-        for (Node node : nodes)
-            racksByDC.computeIfAbsent(node.dc(), (k) -> new HashSet<>()).add(node.rack());
-
-        return racksByDC;
-    }
-
-    private static final class DatacenterNodes
-    {
-        private final List<Node> nodes = new ArrayList<>();
-        private final Set<Location> racks = new HashSet<>();
-
-        /** Number of replicas left to fill from this DC. */
-        int rfLeft;
-        int acceptableRackRepeats;
-
-        public DatacenterNodes copy()
-        {
-            return new DatacenterNodes(rfLeft, acceptableRackRepeats);
-        }
-
-        DatacenterNodes(int rf,
-                        int rackCount,
-                        int nodeCount)
-        {
-            this.rfLeft = Math.min(rf, nodeCount);
-            acceptableRackRepeats = rf - rackCount;
-        }
-
-        // for copying
-        DatacenterNodes(int rfLeft, int acceptableRackRepeats)
-        {
-            this.rfLeft = rfLeft;
-            this.acceptableRackRepeats = acceptableRackRepeats;
-        }
-
-        boolean addAndCheckIfDone(Node node, Location location)
-        {
-            if (done())
-                return false;
-
-            if (nodes.contains(node))
-                // Cannot repeat a node.
-                return false;
-
-            if (racks.add(location))
-            {
-                // New rack.
-                --rfLeft;
-                nodes.add(node);
-                return done();
-            }
-            if (acceptableRackRepeats <= 0)
-                // There must be rfLeft distinct racks left, do not add any more rack repeats.
-                return false;
-
-            nodes.add(node);
-
-            // Added a node that is from an already met rack to match RF when there aren't enough racks.
-            --acceptableRackRepeats;
-            --rfLeft;
-            return done();
-        }
-
-        boolean done()
-        {
-            assert rfLeft >= 0;
-            return rfLeft == 0;
-        }
-    }
-
-
-    public static void addIfUnique(List<Node> nodes, Set<Integer> names, Node node)
-    {
-        if (names.contains(node.idx()))
-            return;
-        nodes.add(node);
-        names.add(node.idx());
-    }
-
-    /**
-     * Finds a primary replica
-     */
-    public static int primaryReplica(List<Node> nodes, Range range)
-    {
-        for (int i = 0; i < nodes.size(); i++)
-        {
-            if (range.end != Long.MIN_VALUE && nodes.get(i).token() >= range.end)
-                return i;
-        }
-        return -1;
-    }
-
     /**
      * Generates token ranges from the list of nodes
      */
@@ -1190,297 +999,6 @@ public class PlacementSimulator
         }
     }
 
-
-    /**
-     * A Range is responsible for the tokens between (start, end].
-     */
-    public static class Range implements Comparable<Range>
-    {
-        public final long start;
-        public final long end;
-
-        public Range(long start, long end)
-        {
-            assert end > start || end == Long.MIN_VALUE : String.format("Start (%d) should be smaller than end (%d)", start, end);
-            this.start = start;
-            this.end = end;
-        }
-
-        public boolean contains(long min, long max)
-        {
-            assert max > min;
-            return min > start && (max <= end || end == Long.MIN_VALUE);
-        }
-
-        public boolean contains(long token)
-        {
-            return token > start && (token <= end || end == Long.MIN_VALUE);
-        }
-
-        public int compareTo(Range o)
-        {
-            int res = Long.compare(start, o.start);
-            if (res == 0)
-                return Long.compare(end, o.end);
-            return res;
-        }
-
-        public boolean equals(Object o)
-        {
-            if (this == o) return true;
-            if (o == null || getClass() != o.getClass()) return false;
-            Range range = (Range) o;
-            return start == range.start && end == range.end;
-        }
-
-        public int hashCode()
-        {
-            return Objects.hash(start, end);
-        }
-
-        public String toString()
-        {
-            return "(" +
-                   "" + (start == Long.MIN_VALUE ? "MIN" : start) +
-                   ", " + (end == Long.MIN_VALUE ? "MIN" : end) +
-                   ']';
-        }
-    }
-
-    public interface Lookup
-    {
-        String id(int nodeIdx);
-        String dc(int dcIdx);
-        String rack(int rackIdx);
-        NodeId nodeId(int nodeIdx);
-        long token(int tokenIdx);
-        Lookup forceToken(int tokenIdx, long token);
-        InetAddressAndPort addr(int idx);
-        void reset();
-    }
-
-    public static class DefaultLookup implements Lookup
-    {
-        protected final Map<Integer, Long> overrides = new HashMap<>(2);
-
-        public String id(int nodeIdx)
-        {
-            return String.format("127.0.%d.%d", nodeIdx / 256, nodeIdx % 256);
-        }
-
-        public NodeId nodeId(int nodeIdx)
-        {
-            return ClusterMetadata.current().directory.peerId(addr(nodeIdx));
-        }
-
-        public long token(int tokenIdx)
-        {
-            Long override = overrides.get(tokenIdx);
-            if (override != null)
-                return override;
-            return PCGFastPure.next(tokenIdx, 1L);
-        }
-
-        public Lookup forceToken(int tokenIdx, long token)
-        {
-            DefaultLookup newLookup = new DefaultLookup();
-            newLookup.overrides.putAll(overrides);
-            newLookup.overrides.put(tokenIdx, token);
-            return newLookup;
-        }
-
-        public InetAddressAndPort addr(int idx)
-        {
-            try
-            {
-                return InetAddressAndPort.getByName(id(idx));
-            }
-            catch (UnknownHostException e)
-            {
-                throw new RuntimeException(e);
-            }
-        }
-
-        public void reset()
-        {
-            overrides.clear();
-        }
-
-        public String dc(int dcIdx)
-        {
-            return String.format("datacenter%d", dcIdx);
-        }
-
-        public String rack(int rackIdx)
-        {
-            return String.format("rack%d", rackIdx);
-        }
-    }
-
-    public static class HumanReadableTokensLookup extends DefaultLookup {
-        @Override
-        public long token(int tokenIdx)
-        {
-            Long override = overrides.get(tokenIdx);
-            if (override != null)
-                return override;
-            return tokenIdx * 100L;
-        }
-
-        public Lookup forceToken(int tokenIdx, long token)
-        {
-            DefaultLookup lookup = new HumanReadableTokensLookup();
-            lookup.overrides.putAll(overrides);
-            lookup.overrides.put(tokenIdx, token);
-            return lookup;
-        }
-    }
-    public static NodeFactory nodeFactory()
-    {
-        return new NodeFactory(new DefaultLookup());
-    }
-
-    public static NodeFactory nodeFactoryHumanReadable()
-    {
-        return new NodeFactory(new HumanReadableTokensLookup());
-    }
-
-    public static class NodeFactory implements TokenSupplier
-    {
-        private final Lookup lookup;
-
-        public NodeFactory(Lookup lookup)
-        {
-            this.lookup = lookup;
-        }
-
-        public Node make(int idx, int dc, int rack)
-        {
-            return new Node(idx, idx, dc, rack, lookup);
-        }
-
-        public Lookup lookup()
-        {
-            return lookup;
-        }
-
-        public Collection<String> tokens(int i)
-        {
-            return Collections.singletonList(Long.toString(lookup.token(i)));
-        }
-    }
-
-    public static class Node implements Comparable<Node>
-    {
-        private final int tokenIdx;
-        private final int nodeIdx;
-        private final int dcIdx;
-        private final int rackIdx;
-        private final Lookup lookup;
-
-        private Node(int tokenIdx, int idx, int dcIdx, int rackIdx, Lookup lookup)
-        {
-            this.tokenIdx = tokenIdx;
-            this.nodeIdx = idx;
-            this.dcIdx = dcIdx;
-            this.rackIdx = rackIdx;
-            this.lookup = lookup;
-        }
-
-        public InetAddressAndPort addr()
-        {
-            return lookup.addr(nodeIdx);
-        }
-
-        public NodeId nodeId()
-        {
-            return lookup.nodeId(nodeIdx);
-        }
-
-        public String id()
-        {
-            return lookup.id(nodeIdx);
-        }
-
-        public int idx()
-        {
-            return nodeIdx;
-        }
-
-        public int dcIdx()
-        {
-            return dcIdx;
-        }
-
-        public int rackIdx()
-        {
-            return rackIdx;
-        }
-
-        public String dc()
-        {
-            return lookup.dc(dcIdx);
-        }
-
-        public String rack()
-        {
-            return lookup.rack(rackIdx);
-        }
-
-        public long token()
-        {
-            return lookup.token(tokenIdx);
-        }
-
-        public int tokenIdx()
-        {
-            return tokenIdx;
-        }
-
-        public Murmur3Partitioner.LongToken longToken()
-        {
-            return new Murmur3Partitioner.LongToken(token());
-        }
-
-        public Node withNewToken()
-        {
-            return new Node(tokenIdx + 100_000, nodeIdx, dcIdx, rackIdx, lookup);
-        }
-
-        public Node withToken(int tokenIdx)
-        {
-            return new Node(tokenIdx, nodeIdx, dcIdx, rackIdx, lookup);
-        }
-
-        public Node overrideToken(long override)
-        {
-            return new Node(tokenIdx, nodeIdx, dcIdx, rackIdx, lookup.forceToken(tokenIdx, override));
-        }
-
-        public boolean equals(Object o)
-        {
-            if (this == o) return true;
-            if (o == null || !Node.class.isAssignableFrom(o.getClass())) return false;
-            Node node = (Node) o;
-            return Objects.equals(nodeIdx, node.nodeIdx);
-        }
-
-        public int hashCode()
-        {
-            return Objects.hash(nodeIdx);
-        }
-
-        public int compareTo(Node o)
-        {
-            return Long.compare(token(), o.token());
-        }
-
-        public String toString()
-        {
-            return String.format("%s-%s@%d", dc(), id(), token());
-        }
-    }
-
     public static String diffsToString(Map<Range, Diff<Node>> placements)
     {
         StringBuilder builder = new StringBuilder();
@@ -1535,255 +1053,4 @@ public class PlacementSimulator
             }
         }
     }
-
-    public abstract static class ReplicationFactor
-    {
-        private final int nodesTotal;
-
-        public ReplicationFactor(int total)
-        {
-            this.nodesTotal = total;
-        }
-
-        public int total()
-        {
-            return nodesTotal;
-        }
-
-        public abstract int dcs();
-
-        public abstract KeyspaceParams asKeyspaceParams();
-
-        public abstract Map<String, Integer> asMap();
-
-        public ReplicatedRanges replicate(List<Node> nodes)
-        {
-            return replicate(toRanges(nodes), nodes);
-        }
-        public abstract ReplicatedRanges replicate(Range[] ranges, List<Node> nodes);
-    }
-
-    public static class NtsReplicationFactor extends ReplicationFactor
-    {
-        private final Lookup lookup = new DefaultLookup();
-        public final int[] nodesPerDc;
-        private KeyspaceParams keyspaceParams;
-        private Map<String, Integer> map;
-
-        public NtsReplicationFactor(int... nodesPerDc)
-        {
-            super(total(nodesPerDc));
-            this.nodesPerDc = nodesPerDc;
-        }
-
-        public NtsReplicationFactor(int dcs, int nodesPerDc)
-        {
-            super(dcs * nodesPerDc);
-            this.nodesPerDc = new int[dcs];
-            Arrays.fill(this.nodesPerDc, nodesPerDc);
-        }
-
-        private static int total(int... num)
-        {
-            int tmp = 0;
-            for (int i : num)
-                tmp += i;
-            return tmp;
-        }
-
-        public int dcs()
-        {
-            return nodesPerDc.length;
-        }
-
-        public KeyspaceParams asKeyspaceParams()
-        {
-            if (this.keyspaceParams == null)
-                this.keyspaceParams = toKeyspaceParams(lookup);
-            return this.keyspaceParams;
-        }
-
-
-        public Map<String, Integer> asMap()
-        {
-            if (this.map == null)
-                this.map = toMap(lookup);
-            return this.map;
-        }
-
-        public ReplicatedRanges replicate(Range[] ranges, List<Node> nodes)
-        {
-            return replicate(ranges, nodes, asMap());
-        }
-
-        private static <T extends Comparable<T>> void assertStrictlySorted(Collection<T> coll)
-        {
-            if (coll.size() <= 1) return;
-
-            Iterator<T> iter = coll.iterator();
-            T prev = iter.next();
-            while (iter.hasNext())
-            {
-                T next = iter.next();
-                assert next.compareTo(prev) > 0 : String.format("Collection does not seem to be sorted. %s and %s are in wrong order", prev, next);
-                prev = next;
-            }
-        }
-        public static ReplicatedRanges replicate(Range[] ranges, List<Node> nodes, Map<String, Integer> rfs)
-        {
-            assertStrictlySorted(nodes);
-            Map<String, DatacenterNodes> template = new HashMap<>();
-
-            Map<String, List<Node>> nodesByDC = nodesByDC(nodes);
-            Map<String, Set<String>> racksByDC = racksByDC(nodes);
-
-            for (Map.Entry<String, Integer> entry : rfs.entrySet())
-            {
-                String dc = entry.getKey();
-                int rf = entry.getValue();
-                List<Node> nodesInThisDC = nodesByDC.get(dc);
-                Set<String> racksInThisDC = racksByDC.get(dc);
-                int nodeCount = nodesInThisDC == null ? 0 : nodesInThisDC.size();
-                int rackCount = racksInThisDC == null ? 0 : racksInThisDC.size();
-                if (rf <= 0 || nodeCount == 0)
-                    continue;
-
-                template.put(dc, new DatacenterNodes(rf, rackCount, nodeCount));
-            }
-
-            NavigableMap<Range, Map<String, List<Node>>> replication = new TreeMap<>();
-
-            for (Range range : ranges)
-            {
-                final int idx = primaryReplica(nodes, range);
-                int cnt = 0;
-                if (idx >= 0)
-                {
-                    int dcsToFill = template.size();
-
-                    Map<String, DatacenterNodes> nodesInDCs = new HashMap<>();
-                    for (Map.Entry<String, DatacenterNodes> e : template.entrySet())
-                        nodesInDCs.put(e.getKey(), e.getValue().copy());
-
-                    while (dcsToFill > 0 && cnt < nodes.size())
-                    {
-                        Node node = nodes.get((idx + cnt) % nodes.size());
-                        DatacenterNodes dcNodes = nodesInDCs.get(node.dc());
-                        if (dcNodes != null && dcNodes.addAndCheckIfDone(node, new Location(node.dc(), node.rack())))
-                            dcsToFill--;
-
-                        cnt++;
-                    }
-
-                    replication.put(range, mapValues(nodesInDCs, v -> v.nodes));
-                }
-                else
-                {
-                    // if the range end is larger than the highest assigned token, then treat it
-                    // as part of the wraparound and replicate it to the same nodes as the first
-                    // range. This is most likely caused by a decommission removing the node with
-                    // the largest token.
-                    replication.put(range, replication.get(ranges[0]));
-                }
-            }
-
-            return combine(replication);
-        }
-
-        private KeyspaceParams toKeyspaceParams(Lookup lookup)
-        {
-            Object[] args = new Object[nodesPerDc.length * 2];
-            for (int i = 0; i < nodesPerDc.length; i++)
-            {
-                args[i * 2] = lookup.dc(i + 1);
-                args[i * 2 + 1] = nodesPerDc[i];
-            }
-            return KeyspaceParams.nts(args);
-        }
-
-        private Map<String, Integer> toMap(Lookup lookup)
-        {
-            Map<String, Integer> map = new TreeMap<>();
-            for (int i = 0; i < nodesPerDc.length; i++)
-            {
-                map.put(lookup.dc(i + 1), nodesPerDc[i]);
-            }
-            return map;
-        }
-
-        public String toString()
-        {
-            return "NtsReplicationFactor{" +
-                   "map=" + asMap() +
-                   '}';
-        }
-    }
-
-    public static class SimpleReplicationFactor extends ReplicationFactor
-    {
-        private final Lookup lookup = new DefaultLookup();
-        public SimpleReplicationFactor(int total)
-        {
-            super(total);
-        }
-
-        public int dcs()
-        {
-            return 1;
-        }
-
-        public KeyspaceParams asKeyspaceParams()
-        {
-            return KeyspaceParams.simple(total());
-        }
-
-        public Map<String, Integer> asMap()
-        {
-            return Collections.singletonMap(lookup.dc(1), total());
-        }
-
-        public ReplicatedRanges replicate(Range[] ranges, List<Node> nodes)
-        {
-            return replicate(ranges, nodes, total());
-        }
-
-        public static ReplicatedRanges replicate(Range[] ranges, List<Node> nodes, int rf)
-        {
-            NavigableMap<Range, List<Node>> replication = new TreeMap<>();
-            for (Range range : ranges)
-            {
-                Set<Integer> names = new HashSet<>();
-                List<Node> replicas = new ArrayList<>();
-                int idx = primaryReplica(nodes, range);
-                if (idx >= 0)
-                {
-                    for (int i = idx; i < nodes.size() && replicas.size() < rf; i++)
-                        addIfUnique(replicas, names, nodes.get(i));
-
-                    for (int i = 0; replicas.size() < rf && i < idx; i++)
-                        addIfUnique(replicas, names, nodes.get(i));
-                    if (range.start == Long.MIN_VALUE)
-                        replication.put(ranges[ranges.length - 1], replicas);
-                    replication.put(range, replicas);
-                }
-                else
-                {
-                    // if the range end is larger than the highest assigned token, then treat it
-                    // as part of the wraparound and replicate it to the same nodes as the first
-                    // range. This is most likely caused by a decommission removing the node with
-                    // the largest token.
-                    replication.put(range, replication.get(ranges[0]));
-                }
-            }
-
-            return new ReplicatedRanges(ranges, Collections.unmodifiableNavigableMap(replication));
-        }
-
-        public String toString()
-        {
-            return "SimpleReplicationFactor{" +
-                   "rf=" + total() +
-                   '}';
-        }
-    }
 }
diff --git a/test/distributed/org/apache/cassandra/distributed/test/log/PlacementSimulatorTest.java b/test/distributed/org/apache/cassandra/distributed/test/log/PlacementSimulatorTest.java
index babe38a9c1..6b4b496532 100644
--- a/test/distributed/org/apache/cassandra/distributed/test/log/PlacementSimulatorTest.java
+++ b/test/distributed/org/apache/cassandra/distributed/test/log/PlacementSimulatorTest.java
@@ -30,7 +30,24 @@ import java.util.function.Supplier;
 
 import org.junit.Test;
 
-import static org.apache.cassandra.distributed.test.log.PlacementSimulator.*;
+import org.apache.cassandra.harry.sut.TokenPlacementModel;
+
+import static org.apache.cassandra.distributed.test.log.PlacementSimulator.SimulatedPlacements;
+import static org.apache.cassandra.distributed.test.log.PlacementSimulator.Transformations;
+import static org.apache.cassandra.distributed.test.log.PlacementSimulator.assertPlacements;
+import static org.apache.cassandra.distributed.test.log.PlacementSimulator.assertRanges;
+import static org.apache.cassandra.distributed.test.log.PlacementSimulator.filter;
+import static org.apache.cassandra.distributed.test.log.PlacementSimulator.join;
+import static org.apache.cassandra.distributed.test.log.PlacementSimulator.leave;
+import static org.apache.cassandra.distributed.test.log.PlacementSimulator.move;
+import static org.apache.cassandra.distributed.test.log.PlacementSimulator.replace;
+import static org.apache.cassandra.distributed.test.log.PlacementSimulator.split;
+import static org.apache.cassandra.distributed.test.log.PlacementSimulator.superset;
+import static org.apache.cassandra.harry.sut.TokenPlacementModel.Node;
+import static org.apache.cassandra.harry.sut.TokenPlacementModel.NodeFactory;
+import static org.apache.cassandra.harry.sut.TokenPlacementModel.Range;
+import static org.apache.cassandra.harry.sut.TokenPlacementModel.ReplicationFactor;
+import static org.apache.cassandra.harry.sut.TokenPlacementModel.SimpleReplicationFactor;
 import static org.junit.Assert.assertTrue;
 
 public class PlacementSimulatorTest
@@ -50,7 +67,7 @@ public class PlacementSimulatorTest
 
     public void testMove(long t1, long t2, long t3, long t4, long newToken, ReplicationFactor rf)
     {
-        NodeFactory factory = PlacementSimulator.nodeFactory();
+        NodeFactory factory = TokenPlacementModel.nodeFactory();
         Node movingNode = factory.make(1, 1, 1).overrideToken(t1);
         List<Node> orig = Arrays.asList(movingNode,
                                         factory.make(2, 1, 1).overrideToken(t2),
@@ -103,7 +120,7 @@ public class PlacementSimulatorTest
 
     public void testBootstrap(long t1, long t2, long t3, long t4, long newToken, ReplicationFactor rf)
     {
-        NodeFactory factory = PlacementSimulator.nodeFactory();
+        NodeFactory factory = TokenPlacementModel.nodeFactory();
         List<Node> orig = Arrays.asList(factory.make(1, 1, 1).overrideToken(t1),
                                         factory.make(2, 1, 1).overrideToken(t2),
                                         factory.make(3, 1, 1).overrideToken(t3),
@@ -157,7 +174,7 @@ public class PlacementSimulatorTest
 
     public void testDecommission(long t1, long t2, long t3, long t4, long t5, ReplicationFactor rf)
     {
-        NodeFactory factory = PlacementSimulator.nodeFactory();
+        NodeFactory factory = TokenPlacementModel.nodeFactory();
         Node leavingNode = factory.make(1, 1, 1).overrideToken(t1);
         List<Node> orig = Arrays.asList(leavingNode,
                                         factory.make(2, 1, 1).overrideToken(t2),
@@ -225,7 +242,7 @@ public class PlacementSimulatorTest
 
     public void simulate(ReplicationFactor rf) throws Throwable
     {
-        NodeFactory factory = PlacementSimulator.nodeFactory();
+        NodeFactory factory = TokenPlacementModel.nodeFactory();
         List<Node> orig = Collections.singletonList(factory.make(1, 1, 1));
 
         ModelChecker<SimulatedPlacements, SUTState> modelChecker = new ModelChecker<>();
@@ -287,7 +304,7 @@ public class PlacementSimulatorTest
         for (int n : new int[]{ 2, 3, 5 })
         {
             ReplicationFactor rf = new SimpleReplicationFactor(n);
-            NodeFactory factory = PlacementSimulator.nodeFactoryHumanReadable();
+            NodeFactory factory = TokenPlacementModel.nodeFactoryHumanReadable();
             List<Node> nodes = new ArrayList<>(10);
             for (int i = 1; i <= 10; i++)
                 nodes.add(factory.make(i, 1, 1));
@@ -304,7 +321,7 @@ public class PlacementSimulatorTest
         for (int n : new int[]{ 2, 3, 5 })
         {
             ReplicationFactor rf = new SimpleReplicationFactor(n);
-            NodeFactory factory = PlacementSimulator.nodeFactoryHumanReadable();
+            NodeFactory factory = TokenPlacementModel.nodeFactoryHumanReadable();
             List<Node> nodes = new ArrayList<>(10);
             for (int i = 1; i <= 10; i++)
                 nodes.add(factory.make(i, 1, 1));
@@ -321,7 +338,7 @@ public class PlacementSimulatorTest
         for (int n : new int[]{ 2, 3, 5 })
         {
             ReplicationFactor rf = new SimpleReplicationFactor(n);
-            NodeFactory factory = PlacementSimulator.nodeFactoryHumanReadable();
+            NodeFactory factory = TokenPlacementModel.nodeFactoryHumanReadable();
             List<Node> nodes = new ArrayList<>(10);
             for (int i = 1; i <= 10; i++)
                 nodes.add(factory.make(i, 1, 1));
diff --git a/test/distributed/org/apache/cassandra/distributed/test/log/ResumableStartupTest.java b/test/distributed/org/apache/cassandra/distributed/test/log/ResumableStartupTest.java
index d73be10136..c1440a1342 100644
--- a/test/distributed/org/apache/cassandra/distributed/test/log/ResumableStartupTest.java
+++ b/test/distributed/org/apache/cassandra/distributed/test/log/ResumableStartupTest.java
@@ -24,19 +24,21 @@ import java.util.concurrent.Callable;
 import org.junit.Assert;
 import org.junit.Test;
 
-import harry.core.Configuration;
-import harry.core.Run;
-import harry.model.sut.SystemUnderTest;
-import harry.visitors.GeneratingVisitor;
-import harry.visitors.MutatingRowVisitor;
-import harry.visitors.Visitor;
+import org.apache.cassandra.harry.core.Configuration;
+import org.apache.cassandra.harry.core.Run;
+import org.apache.cassandra.harry.sut.SystemUnderTest;
+import org.apache.cassandra.harry.sut.TokenPlacementModel;
+import org.apache.cassandra.harry.sut.injvm.InJVMTokenAwareVisitExecutor;
+import org.apache.cassandra.harry.sut.injvm.InJvmSut;
+import org.apache.cassandra.harry.sut.injvm.QuiescentLocalStateChecker;
+import org.apache.cassandra.harry.visitors.GeneratingVisitor;
+import org.apache.cassandra.harry.visitors.MutatingRowVisitor;
+import org.apache.cassandra.harry.visitors.Visitor;
 import org.apache.cassandra.config.CassandraRelevantProperties;
 import org.apache.cassandra.distributed.Cluster;
 import org.apache.cassandra.distributed.Constants;
 import org.apache.cassandra.distributed.api.*;
-import org.apache.cassandra.distributed.fuzz.HarryHelper;
-import org.apache.cassandra.distributed.fuzz.InJVMTokenAwareVisitorExecutor;
-import org.apache.cassandra.distributed.fuzz.InJvmSut;
+import org.apache.cassandra.harry.HarryHelper;
 import org.apache.cassandra.distributed.shared.ClusterUtils;
 import org.apache.cassandra.distributed.shared.NetworkTopology;
 import org.apache.cassandra.locator.InetAddressAndPort;
@@ -57,11 +59,6 @@ public class ResumableStartupTest extends FuzzTestBase
     @Test
     public void bootstrapWithDeferredJoinTest() throws Throwable
     {
-        Configuration.ConfigurationBuilder configBuilder = HarryHelper.defaultConfiguration()
-                                                                      .setPartitionDescriptorSelector(new Configuration.DefaultPDSelectorConfiguration(1, 1))
-                                                                      .setClusteringDescriptorSelector(HarryHelper.defaultClusteringDescriptorSelectorConfiguration().setMaxPartitionSize(100).build());
-
-
         try (Cluster cluster = builder().withNodes(1)
                                         .withTokenSupplier(TokenSupplier.evenlyDistributedTokens(4))
                                         .withNodeIdTopology(NetworkTopology.singleDcNetworkTopology(4, "dc0", "rack0"))
@@ -69,7 +66,8 @@ public class ResumableStartupTest extends FuzzTestBase
                                         .createWithoutStarting())
         {
             IInvokableInstance cmsInstance = cluster.get(1);
-            configBuilder.setSUT(() -> new InJvmSut(cluster));
+            Configuration.ConfigurationBuilder configBuilder = HarryHelper.defaultConfiguration()
+                                                                          .setSUT(() -> new InJvmSut(cluster));
             Run run = configBuilder.build().createRun();
 
             cmsInstance.config().set("auto_bootstrap", true);
@@ -81,7 +79,10 @@ public class ResumableStartupTest extends FuzzTestBase
             cluster.coordinator(1).execute(run.schemaSpec.compile().cql(), ConsistencyLevel.ALL);
             ClusterUtils.waitForCMSToQuiesce(cluster, cluster.get(1));
 
-            Visitor visitor = new GeneratingVisitor(run, new InJVMTokenAwareVisitorExecutor(run, MutatingRowVisitor::new, SystemUnderTest.ConsistencyLevel.NODE_LOCAL));
+            TokenPlacementModel.ReplicationFactor rf = new TokenPlacementModel.SimpleReplicationFactor(2);
+            Visitor visitor = new GeneratingVisitor(run, new InJVMTokenAwareVisitExecutor(run, MutatingRowVisitor::new,
+                                                                                          SystemUnderTest.ConsistencyLevel.NODE_LOCAL,
+                                                                                          rf));
             for (int i = 0; i < WRITES; i++)
                 visitor.visit();
 
@@ -92,16 +93,18 @@ public class ResumableStartupTest extends FuzzTestBase
 
             withProperty(CassandraRelevantProperties.TEST_WRITE_SURVEY, true, newInstance::startup);
 
-            // Write with ONE, replicate via pending range mechanism
-            visitor = new GeneratingVisitor(run, new InJVMTokenAwareVisitorExecutor(run, MutatingRowVisitor::new, SystemUnderTest.ConsistencyLevel.ONE));
+            // Write with ALL, replicate via pending range mechanism
+            visitor = new GeneratingVisitor(run, new InJVMTokenAwareVisitExecutor(run,
+                                                                                  MutatingRowVisitor::new,
+                                                                                  SystemUnderTest.ConsistencyLevel.ONE,
+                                                                                  rf));
+
             for (int i = 0; i < WRITES; i++)
                 visitor.visit();
 
             Epoch currentEpoch = getClusterMetadataVersion(cmsInstance);
             // Quick check that schema changes are possible with nodes in write survey mode (i.e. with ranges locked)
-            cluster.coordinator(1).execute("ALTER TABLE " + run.schemaSpec.keyspace + "." + run.schemaSpec.table +
-                                           " WITH comment = 'Schema alterations which do not affect placements should" +
-                                           " not be restricted by in flight operations';",
+            cluster.coordinator(1).execute(String.format("ALTER TABLE %s.%s WITH comment = 'Schema alterations which do not affect placements should not be restricted by in flight operations';", run.schemaSpec.keyspace, run.schemaSpec.table),
                                            ConsistencyLevel.ALL);
 
             final String newAddress = ClusterUtils.getBroadcastAddressHostWithPortString(newInstance);
@@ -144,7 +147,7 @@ public class ResumableStartupTest extends FuzzTestBase
             for (int i = 0; i < WRITES; i++)
                 visitor.visit();
 
-            QuiescentLocalStateChecker model = new QuiescentLocalStateChecker(run);
+            QuiescentLocalStateChecker model = new QuiescentLocalStateChecker(run, new TokenPlacementModel.SimpleReplicationFactor(3));
             model.validateAll();
         }
     }
diff --git a/test/distributed/org/apache/cassandra/distributed/test/log/SimulatedOperation.java b/test/distributed/org/apache/cassandra/distributed/test/log/SimulatedOperation.java
index 57ca794882..932b7c3bba 100644
--- a/test/distributed/org/apache/cassandra/distributed/test/log/SimulatedOperation.java
+++ b/test/distributed/org/apache/cassandra/distributed/test/log/SimulatedOperation.java
@@ -33,6 +33,8 @@ import org.junit.Assert;
 
 import org.apache.cassandra.dht.Range;
 import org.apache.cassandra.dht.Token;
+import org.apache.cassandra.distributed.test.log.PlacementSimulator.SimulatedPlacements;
+import org.apache.cassandra.distributed.test.log.PlacementSimulator.Transformations;
 import org.apache.cassandra.tcm.ClusterMetadata;
 import org.apache.cassandra.tcm.ClusterMetadataService;
 import org.apache.cassandra.tcm.MultiStepOperation;
@@ -51,9 +53,7 @@ import org.apache.cassandra.tcm.transformations.UnsafeJoin;
 
 import static org.apache.cassandra.dht.Murmur3Partitioner.LongToken;
 import static org.apache.cassandra.distributed.test.log.CMSTestBase.CMSSut;
-import static org.apache.cassandra.distributed.test.log.PlacementSimulator.Node;
-import static org.apache.cassandra.distributed.test.log.PlacementSimulator.SimulatedPlacements;
-import static org.apache.cassandra.distributed.test.log.PlacementSimulator.Transformations;
+import static org.apache.cassandra.harry.sut.TokenPlacementModel.*;
 
 public abstract class SimulatedOperation
 {
diff --git a/test/distributed/org/apache/cassandra/distributed/test/log/SystemKeyspaceStorageTest.java b/test/distributed/org/apache/cassandra/distributed/test/log/SystemKeyspaceStorageTest.java
index 7cfa62ec34..9682091bf0 100644
--- a/test/distributed/org/apache/cassandra/distributed/test/log/SystemKeyspaceStorageTest.java
+++ b/test/distributed/org/apache/cassandra/distributed/test/log/SystemKeyspaceStorageTest.java
@@ -30,6 +30,7 @@ import org.apache.cassandra.config.DatabaseDescriptor;
 import org.apache.cassandra.cql3.QueryProcessor;
 import org.apache.cassandra.cql3.UntypedResultSet;
 import org.apache.cassandra.distributed.test.ExecUtil;
+import org.apache.cassandra.harry.sut.TokenPlacementModel;
 import org.apache.cassandra.tcm.ClusterMetadataService;
 import org.apache.cassandra.tcm.Epoch;
 import org.apache.cassandra.tcm.Sealed;
@@ -155,7 +156,7 @@ public class SystemKeyspaceStorageTest extends CoordinatorPathTestBase
     @Test
     public void bounceNodeBootrappedFromSnapshot() throws Throwable
     {
-        coordinatorPathTest(new PlacementSimulator.SimpleReplicationFactor(3), (cluster, simulatedCluster) -> {
+        coordinatorPathTest(new TokenPlacementModel.SimpleReplicationFactor(3), (cluster, simulatedCluster) -> {
             ClusterMetadataService.instance().sealPeriod();
             ClusterMetadataService.instance().log().waitForHighestConsecutive();
             ClusterMetadataService.instance().snapshotManager().storeSnapshot(ClusterMetadata.current());
diff --git a/test/distributed/org/apache/cassandra/fuzz/harry/examples/RangeTombstoneBurnTest.java b/test/distributed/org/apache/cassandra/fuzz/harry/examples/RangeTombstoneBurnTest.java
new file mode 100644
index 0000000000..b67813b0a5
--- /dev/null
+++ b/test/distributed/org/apache/cassandra/fuzz/harry/examples/RangeTombstoneBurnTest.java
@@ -0,0 +1,144 @@
+/*
+ * 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 org.apache.cassandra.fuzz.harry.examples;
+
+import java.util.concurrent.atomic.AtomicInteger;
+import java.util.function.Supplier;
+
+import org.junit.Test;
+
+import org.apache.cassandra.harry.checker.ModelChecker;
+import org.apache.cassandra.harry.ddl.SchemaGenerators;
+import org.apache.cassandra.harry.ddl.SchemaSpec;
+import org.apache.cassandra.harry.dsl.ReplayingHistoryBuilder;
+import org.apache.cassandra.harry.gen.EntropySource;
+import org.apache.cassandra.harry.gen.rng.JdkRandomEntropySource;
+import org.apache.cassandra.harry.model.AgainstSutChecker;
+import org.apache.cassandra.fuzz.harry.integration.model.IntegrationTestBase;
+import org.apache.cassandra.harry.sut.QueryModifyingSut;
+import org.apache.cassandra.harry.sut.SystemUnderTest;
+import org.apache.cassandra.harry.sut.TokenPlacementModel;
+import org.apache.cassandra.harry.tracker.DataTracker;
+import org.apache.cassandra.harry.tracker.DefaultDataTracker;
+
+public class RangeTombstoneBurnTest extends IntegrationTestBase
+{
+    private final long seed = 1;
+    private final int ITERATIONS = 5;
+    private final int STEPS_PER_ITERATION = 100;
+
+    @Test
+    public void rangeTombstoneBurnTest() throws Throwable
+    {
+        Supplier<SchemaSpec> supplier = SchemaGenerators.progression(SchemaGenerators.DEFAULT_SWITCH_AFTER);
+
+        for (int i = 0; i < SchemaGenerators.DEFAULT_RUNS; i++)
+        {
+            SchemaSpec schema = supplier.get();
+            beforeEach();
+            SchemaSpec doubleWriteSchema = schema.cloneWithName(schema.keyspace, schema.keyspace + "_debug");
+
+            sut.schemaChange(schema.compile().cql());
+            sut.schemaChange(doubleWriteSchema.compile().cql());
+
+            QueryModifyingSut sut = new QueryModifyingSut(this.sut,
+                                                          schema.table,
+                                                          doubleWriteSchema.table);
+
+            cluster.get(1).nodetool("disableautocompaction");
+
+            for (int iteration = 0; iteration < ITERATIONS; iteration++)
+            {
+                ModelChecker<ReplayingHistoryBuilder> modelChecker = new ModelChecker<>();
+                EntropySource entropySource = new JdkRandomEntropySource(iteration);
+
+                int maxPartitionSize = entropySource.nextInt(1, 1 << entropySource.nextInt(5, 11));
+
+                int[] partitions = new int[10];
+                for (int j = 0; j < partitions.length; j++)
+                    partitions[j] = iteration * partitions.length + j;
+
+                float deleteRowChance = entropySource.nextFloat(0.99f, 1.0f);
+                float deletePartitionChance = entropySource.nextFloat(0.999f, 1.0f);
+                float deleteRangeChance = entropySource.nextFloat(0.95f, 1.0f);
+                float flushChance = entropySource.nextFloat(0.999f, 1.0f);
+                AtomicInteger flushes = new AtomicInteger();
+
+                TokenPlacementModel.ReplicationFactor rf = new TokenPlacementModel.SimpleReplicationFactor(1);
+
+                DataTracker tracker = new DefaultDataTracker();
+                modelChecker.init(new ReplayingHistoryBuilder(seed, maxPartitionSize, STEPS_PER_ITERATION, new DefaultDataTracker(), sut, schema, rf, SystemUnderTest.ConsistencyLevel.ALL))
+                            .step((history) -> {
+                                      int rowIdx = entropySource.nextInt(maxPartitionSize);
+                                      int partitionIdx = partitions[entropySource.nextInt(partitions.length)];
+                                      history.visitPartition(partitionIdx).insert(rowIdx);
+                                      return history;
+                                  })
+                            .step((history) -> entropySource.nextFloat() > deleteRowChance,
+                                  (history) -> {
+                                      int partitionIdx = partitions[entropySource.nextInt(partitions.length)];
+                                      history.visitPartition(partitionIdx).deleteRow();
+                                      return history;
+                                  })
+                            .step((history) -> entropySource.nextFloat() > deleteRowChance,
+                                  (history) -> {
+                                      int partitionIdx = partitions[entropySource.nextInt(partitions.length)];
+                                      history.visitPartition(partitionIdx).deleteColumns();
+                                      return history;
+                                  })
+                            .step((history) -> entropySource.nextFloat() > deletePartitionChance,
+                                  (history) -> {
+                                      int partitionIdx = partitions[entropySource.nextInt(partitions.length)];
+                                      history.visitPartition(partitionIdx).deletePartition();
+                                      return history;
+                                  })
+                            .step((history) -> entropySource.nextFloat() > flushChance,
+                                  (history) -> {
+                                      cluster.get(1).nodetool("flush", schema.keyspace, schema.table);
+                                      flushes.incrementAndGet();
+                                      return history;
+                                  })
+                            .step((history) -> entropySource.nextFloat() > deleteRangeChance,
+                                  (history) -> {
+                                      int partitionIdx = partitions[entropySource.nextInt(partitions.length)];
+                                      history.visitPartition(partitionIdx).deleteRowSlice();
+                                      return history;
+                                  })
+                            .step((history) -> entropySource.nextFloat() > deleteRangeChance,
+                                  (history) -> {
+                                      int row1 = entropySource.nextInt(maxPartitionSize);
+                                      int row2 = entropySource.nextInt(maxPartitionSize);
+                                      int partitionIdx = partitions[entropySource.nextInt(partitions.length)];
+                                      history.visitPartition(partitionIdx).deleteRowRange(Math.min(row1, row2),
+                                                                                          Math.max(row1, row2),
+                                                                                          entropySource.nextBoolean(),
+                                                                                          entropySource.nextBoolean());
+                                      return history;
+                                  })
+                            .afterAll((history) -> {
+                                // Sanity check
+                                history.validate(new AgainstSutChecker(tracker, history.clock(), sut, schema, doubleWriteSchema),
+                                                 partitions);
+                                history.validate(partitions);
+                            })
+                            .run(STEPS_PER_ITERATION, seed);
+            }
+        }
+    }
+}
\ No newline at end of file
diff --git a/test/distributed/org/apache/cassandra/fuzz/harry/gen/DataGeneratorsTest.java b/test/distributed/org/apache/cassandra/fuzz/harry/gen/DataGeneratorsTest.java
new file mode 100644
index 0000000000..37aa5a5274
--- /dev/null
+++ b/test/distributed/org/apache/cassandra/fuzz/harry/gen/DataGeneratorsTest.java
@@ -0,0 +1,504 @@
+/*
+ * 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 org.apache.cassandra.fuzz.harry.gen;
+
+import java.lang.reflect.Array;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.Comparator;
+import java.util.Iterator;
+import java.util.List;
+import java.util.function.BiConsumer;
+import java.util.function.Consumer;
+
+import org.junit.Assert;
+import org.junit.Ignore;
+import org.junit.Test;
+
+import org.apache.cassandra.harry.ddl.ColumnSpec;
+import org.apache.cassandra.harry.gen.Bijections;
+import org.apache.cassandra.harry.gen.Bytes;
+import org.apache.cassandra.harry.gen.DataGenerators;
+import org.apache.cassandra.harry.gen.EntropySource;
+import org.apache.cassandra.harry.gen.Generator;
+import org.apache.cassandra.harry.gen.StringBijection;
+
+public class DataGeneratorsTest
+{
+    private static final int RUNS = 100;
+    private static final EntropySource rand = EntropySource.forTests(1);
+
+    @Test
+    public void testSingleTypeRoundTrip()
+    {
+        for (ColumnSpec.DataType dt : new ColumnSpec.DataType[]{ColumnSpec.int8Type,
+                                                                ColumnSpec.int16Type,
+                                                                ColumnSpec.int32Type,
+                                                                ColumnSpec.int64Type,
+                                                                ColumnSpec.asciiType,
+                                                                ColumnSpec.floatType,
+                                                                ColumnSpec.doubleType})
+        {
+            for (int i = 0; i < RUNS; i++)
+            {
+                DataGenerators.SinglePartKeyGenerator gen = new DataGenerators.SinglePartKeyGenerator(Collections.singletonList(ColumnSpec.ck("ck0", dt, false)));
+                long descriptor = rand.next();
+                descriptor = gen.adjustEntropyDomain(descriptor);
+                Assert.assertEquals(descriptor,
+                                    gen.deflate(gen.inflate(descriptor)));
+            }
+        }
+    }
+
+    @Test
+    public void testRequiredBytes()
+    {
+        testRequiredBytes(sizes(4, 1),
+                          ColumnSpec.int32Type, ColumnSpec.int8Type);
+        testRequiredBytes(sizes(7, 1),
+                          ColumnSpec.int64Type, ColumnSpec.int8Type);
+        testRequiredBytes(sizes(4, 4),
+                          ColumnSpec.int32Type, ColumnSpec.int64Type);
+        testRequiredBytes(sizes(4, 4),
+                          ColumnSpec.int32Type, ColumnSpec.int32Type);
+        testRequiredBytes(sizes(4, 3),
+                          ColumnSpec.int32Type, ColumnSpec.floatType);
+        testRequiredBytes(sizes(4, 4),
+                          ColumnSpec.int64Type, ColumnSpec.int64Type);
+        testRequiredBytes(sizes(4, 2, 2),
+                          ColumnSpec.int64Type, ColumnSpec.int16Type, ColumnSpec.int32Type);
+        testRequiredBytes(sizes(6, 1, 1),
+                          ColumnSpec.int64Type, ColumnSpec.int8Type, ColumnSpec.int8Type);
+        testRequiredBytes(sizes(4, 2, 2),
+                          ColumnSpec.int32Type, ColumnSpec.int32Type, ColumnSpec.int64Type);
+        testRequiredBytes(sizes(4, 2, 2),
+                          ColumnSpec.int32Type, ColumnSpec.int32Type, ColumnSpec.int64Type);
+        testRequiredBytes(sizes(1, 5, 2),
+                          ColumnSpec.int8Type, ColumnSpec.asciiType, ColumnSpec.int64Type);
+        testRequiredBytes(sizes(1, 1, 6),
+                          ColumnSpec.int8Type, ColumnSpec.int8Type, ColumnSpec.int64Type);
+        testRequiredBytes(sizes(2, 2, 2, 2),
+                          ColumnSpec.int64Type, ColumnSpec.int32Type, ColumnSpec.int32Type, ColumnSpec.int32Type);
+        testRequiredBytes(sizes(1, 3, 2, 2),
+                          ColumnSpec.int8Type, ColumnSpec.int32Type, ColumnSpec.int32Type, ColumnSpec.int32Type);
+        testRequiredBytes(sizes(1, 3, 2, 2),
+                          ColumnSpec.int8Type, ColumnSpec.int32Type, ColumnSpec.int32Type, ColumnSpec.int32Type);
+        testRequiredBytes(sizes(1, 3, 2, 2),
+                          ColumnSpec.int8Type, ColumnSpec.int32Type, ColumnSpec.int32Type, ColumnSpec.int32Type, ColumnSpec.int64Type);
+    }
+
+    private static void testRequiredBytes(int[] sizes,
+                                         ColumnSpec.DataType<?>... types)
+    {
+        int sum = 0;
+        for (int size : sizes)
+            sum += size;
+        Assert.assertTrue(sum > 0);
+        Assert.assertTrue(sum <= 8);
+
+        List<ColumnSpec<?>> columns = new ArrayList<>(types.length);
+        for (int i = 0; i < types.length; i++)
+            columns.add(ColumnSpec.ck("r" + i, types[i], false));
+        Assert.assertArrayEquals(columns.toString(),
+                                 sizes,
+                                 DataGenerators.requiredBytes(columns));
+    }
+
+    @Test
+    public void testSliceStitch()
+    {
+        for (int i = 2; i < 5; i++)
+        {
+            Iterator<ColumnSpec.DataType[]> iter = permutations(i,
+                                                                ColumnSpec.DataType.class,
+                                                                ColumnSpec.int8Type,
+                                                                ColumnSpec.asciiType,
+                                                                ColumnSpec.int16Type,
+                                                                ColumnSpec.int32Type,
+                                                                ColumnSpec.int32Type,
+                                                                ColumnSpec.floatType,
+                                                                ColumnSpec.doubleType);
+            while (iter.hasNext())
+            {
+                testSliceStitch(iter.next());
+            }
+        }
+    }
+
+    private static void testSliceStitch(ColumnSpec.DataType... types)
+    {
+        List<ColumnSpec<?>> spec = new ArrayList<>(types.length);
+        for (int i = 0; i < types.length; i++)
+            spec.add(ColumnSpec.ck("r" + i, types[i], false));
+        DataGenerators.MultiPartKeyGenerator gen = new DataGenerators.MultiPartKeyGenerator(spec);
+
+        for (int i = 0; i < RUNS; i++)
+        {
+            long orig = gen.adjustEntropyDomain(rand.next());
+            long[] sliced = gen.slice(orig);
+            long stitched = gen.stitch(sliced);
+            Assert.assertEquals(String.format("Orig: %s. Stitched: %s",
+                                              Long.toHexString(orig),
+                                              Long.toHexString(stitched)),
+                                orig, stitched);
+        }
+    }
+
+    @Test
+    public void testKeyGenerators()
+    {
+        for (int i = 1; i < 5; i++)
+        {
+            for (boolean asReversed : new boolean[]{ false, true })
+            {
+                Iterator<ColumnSpec.DataType[]> iter = permutations(i,
+                                                                    ColumnSpec.DataType.class,
+                                                                    ColumnSpec.int8Type,
+                                                                    ColumnSpec.asciiType,
+                                                                    ColumnSpec.int16Type,
+                                                                    ColumnSpec.int32Type,
+                                                                    ColumnSpec.int64Type,
+                                                                    ColumnSpec.floatType,
+                                                                    ColumnSpec.doubleType
+                );
+
+                while (iter.hasNext())
+                {
+                    ColumnSpec.DataType[] types = iter.next();
+                    try
+                    {
+                        testKeyGenerators(asReversed, types);
+                    }
+                    catch (Throwable t)
+                    {
+                        throw new AssertionError("Caught error for the type combination " + Arrays.toString(types), t);
+                    }
+                }
+            }
+        }
+    }
+
+    @Ignore
+    @Test // this one is mostly useful when the above test fails and you need a quicker turnaround
+    public void testSomeKeyGenerators()
+    {
+        Iterator<ColumnSpec.DataType[]> iter = Collections.singletonList(new ColumnSpec.DataType[]{ ColumnSpec.int32Type, ColumnSpec.int32Type }).iterator();
+
+        while (iter.hasNext())
+        {
+            ColumnSpec.DataType[] types = iter.next();
+            try
+            {
+                testKeyGenerators(true, types);
+                testKeyGenerators(false, types);
+            }
+            catch (Throwable t)
+            {
+                throw new AssertionError("Caught error for the type combination " + Arrays.toString(types), t);
+            }
+        }
+    }
+
+    static void testKeyGenerators(boolean reversed, ColumnSpec.DataType<?>... types)
+    {
+        List<ColumnSpec<?>> spec = new ArrayList<>(types.length);
+        for (int i = 0; i < types.length; i++)
+            spec.add(ColumnSpec.ck("r" + i, types[i], reversed));
+
+        DataGenerators.KeyGenerator keyGenerator = DataGenerators.createKeyGenerator(spec);
+
+        for (int i = 0; i < RUNS; i++)
+        {
+            testKeyGenerators(rand.next(), rand.next(), keyGenerator);
+            // test some edge cases
+            testKeyGenerators(0, 0, keyGenerator);
+            testKeyGenerators(0xffffffffffffffffL, 0xffffffffffffffffL, keyGenerator);
+            testKeyGenerators(keyGenerator.minValue(), keyGenerator.maxValue(), keyGenerator);
+            testKeyGenerators(0, keyGenerator.minValue(), keyGenerator);
+            testKeyGenerators(0, keyGenerator.maxValue(), keyGenerator);
+            long descriptor = rand.next();
+            testKeyGenerators(descriptor, 0, keyGenerator);
+            testKeyGenerators(descriptor, keyGenerator.minValue(), keyGenerator);
+            testKeyGenerators(descriptor, keyGenerator.maxValue(), keyGenerator);
+            testKeyGenerators(descriptor, 0xffffffffffffffffL, keyGenerator);
+            testKeyGenerators(descriptor, descriptor + 1, keyGenerator);
+            testKeyGenerators(descriptor, descriptor - 1, keyGenerator);
+            testKeyGenerators(descriptor, descriptor, keyGenerator);
+        }
+
+        // Fixed prefix, tests sign inversion of subsequent values
+        if (types.length > 1)
+        {
+
+            long pattern = Bytes.bytePatternFor(Long.BYTES - DataGenerators.requiredBytes(spec)[0]);
+            for (int i = 0; i < RUNS; i++)
+            {
+                long descriptor = rand.next();
+                long descriptor2 = (descriptor & ~pattern) | (rand.next() & pattern);
+                testKeyGenerators(descriptor, descriptor2, keyGenerator);
+            }
+
+        }
+    }
+
+    static void testKeyGenerators(long descriptor1, long descriptor2, DataGenerators.KeyGenerator keyGenerator)
+    {
+        descriptor1 = keyGenerator.adjustEntropyDomain(descriptor1);
+        descriptor2 = keyGenerator.adjustEntropyDomain(descriptor2);
+        Object[] value1 = keyGenerator.inflate(descriptor1);
+        Object[] value2 = keyGenerator.inflate(descriptor2);
+
+        assertDescriptorsEqual(descriptor1,
+                               keyGenerator.deflate(value1));
+        assertDescriptorsEqual(descriptor2,
+                               keyGenerator.deflate(value2));
+
+        Assert.assertEquals(String.format("%s %s %s and %s %s %s have different order. ",
+                                          Arrays.toString(value1),
+                                          toSignString(compare(value1, value2, keyGenerator.columns)),
+                                          Arrays.toString(value2),
+                                          Long.toHexString(descriptor1),
+                                          toSignString(Long.compare(descriptor1, descriptor2)),
+                                          Long.toHexString(descriptor2)),
+                            normalize(Long.compare(descriptor1, descriptor2)),
+                            compare(value1, value2, keyGenerator.columns));
+    }
+
+    private static void assertDescriptorsEqual(long l, long r)
+    {
+        Assert.assertEquals(String.format("Expected %d (0x%s), but got %d (0x%s)",
+                                          l, Long.toHexString(l), r, Long.toHexString(r)),
+                            l, r);
+    }
+
+    @Test
+    public void int8GeneratorTest()
+    {
+        testInverse(Bijections.INT8_GENERATOR);
+    }
+
+    @Test
+    public void int16GeneratorTest()
+    {
+        testInverse(Bijections.INT16_GENERATOR);
+    }
+
+    @Test
+    public void int32GeneratorTest()
+    {
+        testInverse(Bijections.INT32_GENERATOR);
+    }
+
+    @Test
+    public void int64GeneratorTest()
+    {
+        testInverse(Bijections.INT64_GENERATOR);
+        testOrderPreserving(Bijections.INT64_GENERATOR);
+        testInverse(new Bijections.ReverseBijection(Bijections.INT64_GENERATOR));
+        testOrderPreserving(new Bijections.ReverseBijection(Bijections.INT64_GENERATOR), true);
+    }
+
+    @Test
+    public void booleanGenTest()
+    {
+        testInverse(Bijections.BOOLEAN_GENERATOR);
+        testOrderPreserving(Bijections.BOOLEAN_GENERATOR);
+    }
+
+    @Test
+    public void floatGeneratorTest()
+    {
+        testInverse(Bijections.FLOAT_GENERATOR);
+        testOrderPreserving(Bijections.FLOAT_GENERATOR, Float::compareTo);
+    }
+
+    @Test
+    public void doubleGeneratorTest()
+    {
+        testInverse(Bijections.DOUBLE_GENERATOR);
+        testOrderPreserving(Bijections.DOUBLE_GENERATOR);
+    }
+
+
+    @Test
+    public void stringGenTest()
+    {
+        testInverse(new StringBijection());
+        testOrderPreserving(new StringBijection());
+    }
+
+    public static <T> void testInverse(Bijections.Bijection<T> gen)
+    {
+        test(gen,
+             (v) -> Assert.assertEquals(gen.adjustEntropyDomain(v.descriptor), gen.deflate(v.value)));
+    }
+
+    public static <T extends Comparable> void testOrderPreserving(Bijections.Bijection<T> gen)
+    {
+        testOrderPreserving(gen, false);
+    }
+
+    public static <T extends Comparable> void testOrderPreserving(Bijections.Bijection<T> gen, boolean reverse)
+    {
+        test(gen, gen,
+             (v1, v2) -> {
+                 long v1Descriptor = gen.adjustEntropyDomain(v1.descriptor);
+                 long v2Descriptor = gen.adjustEntropyDomain(v2.descriptor);
+                 Assert.assertEquals(String.format("%s (%s) and %s (%s) sort wrong",
+                                                   v1.value,
+                                                   Long.toHexString(v1Descriptor),
+                                                   v2.value,
+                                                   Long.toHexString(v2Descriptor)),
+                                     normalize(Long.compare(v1Descriptor, v2Descriptor)) * (reverse ? -1 : 1),
+                                     normalize(v1.value.compareTo(v2.value)));
+             });
+    }
+
+    public static <T extends Comparable> void testOrderPreserving(Bijections.Bijection<T> gen, Comparator<T> comparator)
+    {
+        test(gen, gen,
+             (v1, v2) -> Assert.assertEquals(normalize(Long.compare(gen.adjustEntropyDomain(v1.descriptor),
+                                                                    gen.adjustEntropyDomain(v2.descriptor))),
+                                             normalize(comparator.compare(v1.value, v2.value))));
+    }
+
+    public static <T1> void test(Bijections.Bijection<T1> gen1,
+                                 Consumer<Generator.Value<T1>> validate)
+    {
+
+        for (int i = 0; i < RUNS; i++)
+        {
+            long descriptor1 = rand.next();
+            validate.accept(new Generator.Value<T1>(descriptor1, gen1.inflate(gen1.adjustEntropyDomain(descriptor1))));
+        }
+    }
+
+    public static <T1, T2> void test(Bijections.Bijection<T1> gen1,
+                                     Bijections.Bijection<T2> gen2,
+                                     BiConsumer<Generator.Value<T1>, Generator.Value<T2>> validate)
+    {
+
+        for (int i = 0; i < RUNS; i++)
+        {
+            long descriptor1 = rand.next();
+            long descriptor2 = rand.next();
+            validate.accept(new Generator.Value<T1>(descriptor1, gen1.inflate(gen1.adjustEntropyDomain(descriptor1))),
+                            new Generator.Value<T2>(descriptor2, gen2.inflate(gen1.adjustEntropyDomain(descriptor2))));
+        }
+    }
+
+    public static int[] sizes(int... ints)
+    {
+        return ints;
+    }
+
+
+    public static String toSignString(int l)
+    {
+        if (l == 0)
+            return "=";
+        else if (l > 0)
+            return ">";
+        return "<";
+    }
+    public static int normalize(int l)
+    {
+        if (l == 0)
+            return 0;
+        if (l > 0)
+            return 1;
+        else
+            return -1;
+    }
+
+    static int compare(Object[] a, Object[] b, List<ColumnSpec<?>> spec)
+    {
+        assert a.length == b.length;
+        for (int i = 0; i < a.length; i++)
+        {
+            Comparable comparableA = (Comparable) a[i];
+            Comparable comparableB = (Comparable) b[i];
+
+            int cmp = comparableA.compareTo(comparableB);
+            if (cmp != 0)
+            {
+                if (spec.get(i).isReversed())
+                    cmp *= -1;
+
+                return cmp < 0 ? -1 : 1;
+            }
+        }
+        return 0;
+    }
+
+    public static <T> Iterator<T[]> permutations(int size, Class<T> klass, T... values)
+    {
+        int[] cursors = new int[size];
+        return new Iterator<T[]>()
+        {
+            int left = 0;
+            T[] next = fromCursors();
+
+            public boolean hasNext()
+            {
+                for (int i = cursors.length - 1; i >= 0; i--)
+                {
+                    if (cursors[i] < values.length - 1)
+                        return true;
+                }
+                return false;
+            }
+
+            public T[] computeNext()
+            {
+                cursors[left]++;
+
+                for (int i = 0; i < cursors.length; i++)
+                {
+                    if (cursors[i] == values.length)
+                    {
+                        cursors[i] = 0;
+                        cursors[i + 1]++;
+                    }
+                }
+
+                return fromCursors();
+            }
+
+            public T[] next()
+            {
+                if (next == null)
+                    next = computeNext();
+
+                T[] ret = next;
+                next = null;
+                return ret;
+            }
+
+            public T[] fromCursors()
+            {
+                T[] res = (T[]) Array.newInstance(klass, cursors.length);
+                for (int i = 0; i < cursors.length; i++)
+                    res[i] = values[cursors[i]];
+                return res;
+            }
+        };
+    }
+}
\ No newline at end of file
diff --git a/test/distributed/org/apache/cassandra/fuzz/harry/gen/EntropySourceTest.java b/test/distributed/org/apache/cassandra/fuzz/harry/gen/EntropySourceTest.java
new file mode 100644
index 0000000000..a2b53306d0
--- /dev/null
+++ b/test/distributed/org/apache/cassandra/fuzz/harry/gen/EntropySourceTest.java
@@ -0,0 +1,158 @@
+/*
+ * 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 org.apache.cassandra.fuzz.harry.gen;
+
+import java.util.Random;
+
+import org.junit.Assert;
+import org.junit.Test;
+
+import org.apache.cassandra.harry.gen.EntropySource;
+import org.apache.cassandra.harry.gen.distribution.Distribution;
+import org.apache.cassandra.harry.gen.rng.PCGFastPure;
+import org.apache.cassandra.harry.gen.rng.PcgRSUFast;
+import org.apache.cassandra.harry.model.OpSelectors;
+
+public class EntropySourceTest
+{
+    private static int RUNS = 100000;
+
+    @Test
+    public void testScale()
+    {
+        Random rand = new Random();
+        for (int cycle = 0; cycle < RUNS; cycle++)
+        {
+            int a = rand.nextInt(100);
+            int b = rand.nextInt(100);
+            while (a == b)
+                b = rand.nextInt(100);
+
+            int min = Math.min(a, b);
+            int max = Math.max(a, b);
+            long[] cardinality = new long[max - min];
+            for (int i = 0; i < 100000; i++)
+            {
+                long rnd = rand.nextLong();
+                long scaled = Distribution.ScaledDistribution.scale(rnd, min, max);
+                cardinality[(int) scaled - min]++;
+            }
+
+            for (long c : cardinality)
+                Assert.assertTrue(c > 0);
+        }
+    }
+
+    @Test
+    public void testShuffleUnshuffle()
+    {
+        Random rnd = new Random();
+
+        for (int i = 1; i < RUNS; i++)
+        {
+            long l = rnd.nextLong();
+            Assert.assertEquals(l, PCGFastPure.unshuffle(PCGFastPure.shuffle(l)));
+        }
+    }
+
+    @Test
+    public void testImmutableRng()
+    {
+        int size = 5;
+        OpSelectors.PureRng rng = new OpSelectors.PCGFast(1);
+        for (int stream = 1; stream < RUNS; stream++)
+        {
+            long[] generated = new long[size];
+            for (int i = 0; i < size; i++)
+                generated[i] = rng.randomNumber(i, stream);
+
+            Assert.assertEquals(0, rng.sequenceNumber(generated[0], stream));
+            Assert.assertEquals(generated[1], rng.next(generated[0], stream));
+
+            for (int i = 1; i < size; i++)
+            {
+                Assert.assertEquals(generated[i], rng.next(generated[i - 1], stream));
+                Assert.assertEquals(generated[i - 1], rng.prev(generated[i], stream));
+                Assert.assertEquals(i, rng.sequenceNumber(generated[i], stream));
+            }
+        }
+    }
+
+    @Test
+    public void testSequenceNumber()
+    {
+        int size = 5;
+        OpSelectors.PureRng rng = new OpSelectors.PCGFast(1);
+        for (int stream = 1; stream < RUNS; stream++)
+        {
+            for (int i = 0; i < size; i++)
+                Assert.assertEquals(i, rng.sequenceNumber(rng.randomNumber(i, stream), stream));
+        }
+    }
+
+    @Test
+    public void seekTest()
+    {
+        PcgRSUFast rand = new PcgRSUFast(1, 1);
+        long first = rand.next();
+        long last = 0;
+        for (int i = 0; i < 10; i++)
+            last = rand.next();
+
+        rand.advance(-11);
+        Assert.assertEquals(first, rand.next());
+
+        rand.advance(9);
+        Assert.assertEquals(last, rand.next());
+        Assert.assertEquals(first, rand.nextAt(0));
+        Assert.assertEquals(last, rand.nextAt(10));
+        Assert.assertEquals(-10, rand.distance(first));
+    }
+
+    @Test
+    public void shuffleUnshuffleTest()
+    {
+        Random rnd = new Random();
+        for (int i = 0; i < RUNS; i++)
+        {
+            long a = rnd.nextLong();
+            Assert.assertEquals(a, PCGFastPure.unshuffle(PCGFastPure.shuffle(a)));
+        }
+    }
+
+    @Test
+    public void testIntBetween()
+    {
+        EntropySource rng = new PcgRSUFast(System.currentTimeMillis(), 0);
+
+        int a = 0;
+        int b = 50;
+        int[] cardinality = new int[b - a];
+        for (int i = 0; i < RUNS; i++)
+        {
+            int min = Math.min(a, b);
+            int max = Math.max(a, b);
+            cardinality[rng.nextInt(min, max - 1) - min]++;
+        }
+
+        // Extremely improbable yet possible that some of the values won't be generated
+        for (int i = 0; i < cardinality.length; i++)
+            Assert.assertTrue(cardinality[i] > 0);
+    }
+}
diff --git a/test/distributed/org/apache/cassandra/distributed/fuzz/FixedSchemaProviderConfiguration.java b/test/distributed/org/apache/cassandra/fuzz/harry/gen/ExtensionsTest.java
similarity index 55%
rename from test/distributed/org/apache/cassandra/distributed/fuzz/FixedSchemaProviderConfiguration.java
rename to test/distributed/org/apache/cassandra/fuzz/harry/gen/ExtensionsTest.java
index 2e03903542..d6d331eed5 100644
--- a/test/distributed/org/apache/cassandra/distributed/fuzz/FixedSchemaProviderConfiguration.java
+++ b/test/distributed/org/apache/cassandra/fuzz/harry/gen/ExtensionsTest.java
@@ -16,28 +16,29 @@
  * limitations under the License.
  */
 
-package org.apache.cassandra.distributed.fuzz;
+package org.apache.cassandra.fuzz.harry.gen;
 
-import com.fasterxml.jackson.annotation.JsonCreator;
-import com.fasterxml.jackson.annotation.JsonTypeName;
-import harry.core.Configuration;
-import harry.ddl.SchemaSpec;
-import harry.model.sut.SystemUnderTest;
+import java.util.function.Supplier;
 
-@JsonTypeName("fixed")
-public class FixedSchemaProviderConfiguration implements Configuration.SchemaProviderConfiguration
+import org.junit.Assert;
+import org.junit.Test;
+
+import org.apache.cassandra.harry.gen.EntropySource;
+import org.apache.cassandra.harry.gen.Generators;
+
+public class ExtensionsTest
 {
-    private final SchemaSpec schemaSpec;
 
-    @JsonCreator
-    public FixedSchemaProviderConfiguration(SchemaSpec schemaSpec)
+    @Test
+    public void testPick()
     {
-        this.schemaSpec = schemaSpec;
-    }
+        Supplier<Integer> gen = Generators.pick(101, 102, 103, 104, 105).bind(EntropySource.forTests());
 
-    @Override
-    public SchemaSpec make(long l, SystemUnderTest systemUnderTest)
-    {
-        return this.schemaSpec;
+        int[] counts = new int[5];
+        for (int i = 0; i < 1000; i++)
+            counts[gen.get() - 101] += 1;
+
+        // It is possible, however very improbable we won't hit each one at least once
+        for (int count: counts) Assert.assertTrue(count > 0);
     }
 }
diff --git a/test/distributed/org/apache/cassandra/fuzz/harry/gen/SurjectionsTest.java b/test/distributed/org/apache/cassandra/fuzz/harry/gen/SurjectionsTest.java
new file mode 100644
index 0000000000..b749c4140a
--- /dev/null
+++ b/test/distributed/org/apache/cassandra/fuzz/harry/gen/SurjectionsTest.java
@@ -0,0 +1,63 @@
+/*
+ * 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 org.apache.cassandra.fuzz.harry.gen;
+
+import java.util.HashMap;
+import java.util.Map;
+
+import org.junit.Assert;
+import org.junit.Test;
+
+import org.apache.cassandra.harry.gen.EntropySource;
+import org.apache.cassandra.harry.gen.Surjections;
+import org.apache.cassandra.harry.gen.rng.PcgRSUFast;
+
+public class SurjectionsTest
+{
+    private static int RUNS = 1000000;
+
+    @Test
+    public void weightedTest()
+    {
+        int[] weights = new int[] {50, 40, 10};
+
+        Surjections.Surjection<String> gen = Surjections.weighted(Surjections.weights(weights),
+                                                                  "a", "b", "c");
+
+        Map<String, Integer> frequencies = new HashMap<>();
+        EntropySource rng = new PcgRSUFast(System.currentTimeMillis(), 0);
+
+        for (int i = 0; i < RUNS; i++)
+        {
+            String s = gen.inflate(rng.next());
+            frequencies.compute(s, (s1, i1) -> {
+                if (i1 == null)
+                    return 1;
+                else
+                    return i1 + 1;
+            });
+        }
+
+        Assert.assertEquals(frequencies.get("a") / 10000, weights[0], 1);
+        Assert.assertEquals(frequencies.get("b") / 10000, weights[1], 1);
+        Assert.assertEquals(frequencies.get("c") / 10000, weights[2], 1);
+    }
+
+
+}
diff --git a/test/distributed/org/apache/cassandra/fuzz/harry/integration/QuickTheoriesAdapter.java b/test/distributed/org/apache/cassandra/fuzz/harry/integration/QuickTheoriesAdapter.java
new file mode 100644
index 0000000000..ecb3513278
--- /dev/null
+++ b/test/distributed/org/apache/cassandra/fuzz/harry/integration/QuickTheoriesAdapter.java
@@ -0,0 +1,100 @@
+/*
+ * 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 org.apache.cassandra.fuzz.harry.integration;
+
+import org.apache.cassandra.harry.gen.Generator;
+import org.apache.cassandra.harry.gen.EntropySource;
+import org.apache.cassandra.harry.gen.rng.RngUtils;
+import org.quicktheories.core.Gen;
+import org.quicktheories.core.RandomnessSource;
+import org.quicktheories.impl.Constraint;
+
+public class QuickTheoriesAdapter
+{
+    public static <T> Gen<T> convert(Generator<T> generator)
+    {
+        return new Gen<T>()
+        {
+            private final RandomnessSourceAdapter<T> adapter = new RandomnessSourceAdapter<>();
+
+            public T generate(RandomnessSource randomnessSource)
+            {
+                return adapter.generate(randomnessSource, generator);
+            }
+        };
+    }
+
+    public static class RandomnessSourceAdapter<T> implements EntropySource
+    {
+        private RandomnessSource rnd;
+
+        public long next()
+        {
+            return rnd.next(Constraint.none());
+        }
+
+        public void seed(long seed)
+        {
+            throw new RuntimeException("Seed is not settable");
+        }
+
+        public EntropySource derive()
+        {
+            return new RandomnessSourceAdapter<>();
+        }
+
+        public int nextInt()
+        {
+            return RngUtils.asInt(next());
+        }
+
+        public int nextInt(int max)
+        {
+            return RngUtils.asInt(next(), max);
+        }
+
+        public int nextInt(int min, int max)
+        {
+            return RngUtils.asInt(next(), min, max);
+        }
+
+        public long nextLong(long min, long max)
+        {
+            return RngUtils.trim(next(), min, max);
+        }
+
+        public float nextFloat()
+        {
+            return RngUtils.asFloat(next());
+        }
+
+        public boolean nextBoolean()
+        {
+            return RngUtils.asBoolean(next());
+        }
+
+        public T generate(RandomnessSource rnd, Generator<T> generate)
+        {
+            this.rnd = rnd;
+            T value = generate.generate(this);
+            this.rnd = null;
+            return value;
+        }
+    }
+}
\ No newline at end of file
diff --git a/test/distributed/org/apache/cassandra/fuzz/harry/integration/ddl/SchemaGenTest.java b/test/distributed/org/apache/cassandra/fuzz/harry/integration/ddl/SchemaGenTest.java
new file mode 100644
index 0000000000..8334ae4911
--- /dev/null
+++ b/test/distributed/org/apache/cassandra/fuzz/harry/integration/ddl/SchemaGenTest.java
@@ -0,0 +1,216 @@
+/*
+ * 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 org.apache.cassandra.fuzz.harry.integration.ddl;
+
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collection;
+import java.util.Comparator;
+import java.util.Iterator;
+import java.util.List;
+
+import org.junit.Assert;
+import org.junit.Test;
+
+import org.apache.cassandra.fuzz.harry.integration.QuickTheoriesAdapter;
+import org.apache.cassandra.harry.ddl.ColumnSpec;
+import org.apache.cassandra.harry.ddl.SchemaGenerators;
+import org.apache.cassandra.harry.ddl.SchemaSpec;
+import org.apache.cassandra.harry.gen.Generator;
+import org.apache.cassandra.harry.operations.CompiledStatement;
+
+import org.apache.cassandra.harry.operations.Query;
+import org.apache.cassandra.harry.util.TestRunner;
+import org.apache.cassandra.cql3.CQLTester;
+import org.apache.cassandra.db.Keyspace;
+import org.apache.cassandra.schema.ColumnMetadata;
+import org.apache.cassandra.schema.TableMetadata;
+import org.apache.cassandra.utils.Pair;
+import org.quicktheories.QuickTheory;
+import org.quicktheories.core.Gen;
+
+import static org.quicktheories.generators.SourceDSL.integers;
+
+public class SchemaGenTest extends CQLTester
+{
+    private static final int CYCLES = 10;
+
+    // TODO: compact storage tests
+    @Test
+    public void testSelectForwardAndReverseIteration() throws Throwable
+    {
+        Generator<SchemaSpec> gen = new SchemaGenerators.Builder(KEYSPACE).partitionKeyColumnCount(1, 4)
+                                                                          .clusteringColumnCount(1, 10)
+                                                                          .regularColumnCount(0, 10)
+                                                                          .staticColumnCount(0, 10)
+                                                                          .generator();
+
+
+        TestRunner.test(gen,
+                        schemaDefinition -> {
+                            String tableDef = schemaDefinition.compile().cql();
+                            createTable(tableDef);
+
+                            try
+                            {
+                                CompiledStatement statement = Query.selectPartition(schemaDefinition, 1, false).toSelectStatement();
+                                execute(statement.cql(), statement.bindings());
+                                statement = Query.selectPartition(schemaDefinition, 1, true).toSelectStatement();
+                                execute(statement.cql(), statement.bindings());
+                            }
+                            catch (Throwable t)
+                            {
+                                throw new AssertionError("Exception caught", t);
+                            }
+                        });
+    }
+
+    @Test
+    public void createTableRoundTrip() throws Throwable
+    {
+        Generator<SchemaSpec> gen = new SchemaGenerators.Builder(KEYSPACE).partitionKeyColumnCount(1, 10)
+                                                                          .clusteringColumnCount(1, 10)
+                                                                          .regularColumnCount(0, 10)
+                                                                          .staticColumnCount(0, 10)
+                                                                          .generator();
+
+        TestRunner.test(gen,
+                        schemaDefinition -> {
+                            String tableDef = schemaDefinition.compile().cql();
+                            createTable(KEYSPACE, tableDef);
+                            TableMetadata tableMetadata = Keyspace.open(KEYSPACE).getColumnFamilyStore(schemaDefinition.table).metadata.get();
+                            compareColumns(schemaDefinition.partitionKeys, tableMetadata.partitionKeyColumns());
+                            compareColumns(schemaDefinition.clusteringKeys, tableMetadata.clusteringColumns());
+                            compareColumns(schemaDefinition.regularColumns, tableMetadata.regularColumns());
+                            compareColumns(schemaDefinition.staticColumns, tableMetadata.staticColumns());
+                        });
+    }
+
+    @Test
+    public void testReverseComparator()
+    {
+        SchemaSpec spec = new SchemaSpec(KEYSPACE, "tbl1",
+                                         Arrays.asList(ColumnSpec.pk("pk1", ColumnSpec.asciiType),
+                                                       ColumnSpec.pk("pk2", ColumnSpec.int64Type)),
+                                         Arrays.asList(ColumnSpec.ck("ck1", ColumnSpec.asciiType, true),
+                                                       ColumnSpec.ck("ck2", ColumnSpec.int64Type, false)),
+                                         Arrays.asList(ColumnSpec.regularColumn("v1", ColumnSpec.asciiType),
+                                                       ColumnSpec.regularColumn("v2", ColumnSpec.asciiType),
+                                                       ColumnSpec.regularColumn("v3", ColumnSpec.int64Type),
+                                                       ColumnSpec.regularColumn("v4", ColumnSpec.int64Type)),
+                                         Arrays.asList(ColumnSpec.staticColumn("static1", ColumnSpec.asciiType),
+                                                       ColumnSpec.staticColumn("static2", ColumnSpec.int64Type)));
+
+
+        String tableDef = spec.compile().cql();
+        createTable(KEYSPACE, tableDef);
+        TableMetadata tableMetadata = Keyspace.open(KEYSPACE).getColumnFamilyStore(spec.table).metadata.get();
+        compareColumns(spec.partitionKeys, tableMetadata.partitionKeyColumns());
+        compareColumns(spec.clusteringKeys, tableMetadata.clusteringColumns());
+        compareColumns(spec.regularColumns, tableMetadata.regularColumns());
+        compareColumns(spec.staticColumns, tableMetadata.staticColumns());
+    }
+
+
+    @Test
+    public void testSchemaGeneration()
+    {
+        Gen<Pair<Integer, Integer>> ckCounts = integers().between(0, 4).zip(integers().between(0, 6), Pair::create);
+        Gen<Pair<Integer, Integer>> regCounts = integers().between(0, 4).zip(integers().between(0, 6), Pair::create);
+//        Gen<Pair<Integer, Integer>> staticCounts = integers().between(0, 4).zip(integers().between(0, 6), Pair::create);
+        Gen<Pair<Integer, Integer>> pkCounts = integers().between(1, 4).zip(integers().between(0, 6), Pair::create);
+
+        Gen<SchemaGenerationInputs> inputs = pkCounts.zip(ckCounts, regCounts,
+                                                          (pks, cks, regs) ->
+                                                          new SchemaGenerationInputs(pks.left, pks.left + pks.right,
+                                                                                     cks.left, cks.left + cks.right,
+                                                                                     regs.left, regs.left + regs.right));
+
+        Gen<Pair<SchemaGenerationInputs, SchemaSpec>> schemaAndInputs = inputs.flatMap(input -> {
+            Generator<SchemaSpec> gen = new SchemaGenerators.Builder("test")
+                    .partitionKeyColumnCount(input.minPk, input.maxPk)
+                    .clusteringColumnCount(input.minCks, input.maxCks)
+                    .regularColumnCount(input.minRegs, input.maxRegs)
+                    .generator();
+
+            return QuickTheoriesAdapter.convert(gen).map(schema -> Pair.create(input, schema));
+        });
+
+        qt().forAll(schemaAndInputs)
+            .check(schemaAndInput -> {
+                SchemaGenerationInputs input = schemaAndInput.left;
+                SchemaSpec schema = schemaAndInput.right;
+
+                return schema.partitionKeys.size() <= input.maxPk && schema.partitionKeys.size() >= input.minPk &&
+                       schema.clusteringKeys.size() <= input.maxCks && schema.clusteringKeys.size() >= input.minCks &&
+                       schema.regularColumns.size() <= input.maxRegs && schema.regularColumns.size() >= input.minRegs;
+            });
+    }
+
+    private static class SchemaGenerationInputs {
+        private final int minPk;
+        private final int maxPk;
+        private final int minCks;
+        private final int maxCks;
+        private final int minRegs;
+        private final int maxRegs;
+
+        public SchemaGenerationInputs(int minPk, int maxPk, int minCks, int maxCks, int minRegs, int maxRegs)
+        {
+            this.minPk = minPk;
+            this.maxPk = maxPk;
+            this.minCks = minCks;
+            this.maxCks = maxCks;
+            this.minRegs = minRegs;
+            this.maxRegs = maxRegs;
+        }
+    }
+
+    private static boolean compareColumns(Collection<ColumnSpec<?>> expectedColl, Collection<ColumnMetadata> actualColl)
+    {
+        List<ColumnSpec<?>> expectedSorted = new ArrayList<>(expectedColl);
+        expectedSorted.sort(Comparator.comparing(Object::toString));
+        List<ColumnMetadata> actualSorted = new ArrayList<>(actualColl);
+        actualSorted.sort(Comparator.comparing(Object::toString));
+
+        Iterator<ColumnSpec<?>> expectedIter = expectedSorted.iterator();
+        Iterator<ColumnMetadata> actualIter = actualSorted.iterator();
+
+        while (expectedIter.hasNext() && actualIter.hasNext())
+        {
+            ColumnSpec expected = expectedIter.next();
+            ColumnMetadata actual = actualIter.next();
+
+            Assert.assertEquals(expected.kind.toString(), actual.kind.toString());
+            Assert.assertEquals(expected.name, actual.name.toString());
+            Assert.assertEquals(expected.type.toString(), actual.type.asCQL3Type().toString());
+        }
+
+        Assert.assertEquals(String.format("Collections %s and %s have different sizes", expectedColl, actualColl),
+                            expectedIter.hasNext(), actualIter.hasNext());
+        return true;
+    }
+
+    public static QuickTheory qt()
+    {
+        return QuickTheory.qt()
+                          .withExamples(CYCLES)
+                          .withShrinkCycles(0);
+    }
+}
\ No newline at end of file
diff --git a/test/distributed/org/apache/cassandra/fuzz/harry/integration/dsl/HistoryBuilderIntegrationTest.java b/test/distributed/org/apache/cassandra/fuzz/harry/integration/dsl/HistoryBuilderIntegrationTest.java
new file mode 100644
index 0000000000..bf5cfde121
--- /dev/null
+++ b/test/distributed/org/apache/cassandra/fuzz/harry/integration/dsl/HistoryBuilderIntegrationTest.java
@@ -0,0 +1,148 @@
+/*
+ * 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 org.apache.cassandra.fuzz.harry.integration.dsl;
+
+import java.util.Random;
+import java.util.function.LongSupplier;
+import java.util.function.Supplier;
+
+import org.junit.Test;
+
+import org.apache.cassandra.fuzz.harry.integration.model.ModelTestBase;
+import org.apache.cassandra.harry.checker.ModelChecker;
+import org.apache.cassandra.harry.core.Configuration;
+import org.apache.cassandra.harry.core.Run;
+import org.apache.cassandra.harry.ddl.SchemaGenerators;
+import org.apache.cassandra.harry.ddl.SchemaSpec;
+import org.apache.cassandra.harry.dsl.BatchVisitBuilder;
+import org.apache.cassandra.harry.dsl.HistoryBuilder;
+import org.apache.cassandra.harry.dsl.SingleOperationBuilder;
+import org.apache.cassandra.harry.dsl.ValueDescriptorIndexGenerator;
+import org.apache.cassandra.harry.gen.rng.JdkRandomEntropySource;
+import org.apache.cassandra.harry.model.Model;
+import org.apache.cassandra.harry.operations.Query;
+import org.apache.cassandra.harry.sut.SystemUnderTest;
+import org.apache.cassandra.harry.sut.TokenPlacementModel;
+import org.apache.cassandra.harry.visitors.ReplayingVisitor;
+
+public class HistoryBuilderIntegrationTest extends ModelTestBase
+{
+    private final long seed = 1L;
+    private final int STEPS_PER_ITERATION = 1_000;
+    private final int MAX_PARTITIONS = 100;
+
+    @Override
+    protected Configuration.ModelConfiguration modelConfiguration()
+    {
+        return new Configuration.QuiescentCheckerConfig();
+    }
+
+    public Configuration.ConfigurationBuilder configuration(long seed, SchemaSpec schema)
+    {
+        return super.configuration(seed, schema)
+                    .setPartitionDescriptorSelector(new Configuration.DefaultPDSelectorConfiguration(2, 2))
+                    .setClusteringDescriptorSelector((builder) -> builder.setOperationsPerLtsDistribution(new Configuration.ConstantDistributionConfig(100_000)));
+    }
+
+    @Test
+    public void simpleDSLTest() throws Throwable
+    {
+        Supplier<SchemaSpec> supplier = SchemaGenerators.progression(SchemaGenerators.DEFAULT_SWITCH_AFTER);
+        for (int i = 0; i < SchemaGenerators.DEFAULT_RUNS; i++)
+        {
+            SchemaSpec schema = supplier.get();
+            Configuration config = configuration(i, schema).build();
+
+            Run run = config.createRun();
+            beforeEach();
+            run.sut.schemaChange(schema.compile().cql());
+
+            ModelChecker<SingleOperationBuilder> modelChecker = new ModelChecker<>();
+            JdkRandomEntropySource entropySource = new JdkRandomEntropySource(new Random(seed));
+
+            LongSupplier[] valueGenerators = new LongSupplier[run.schemaSpec.regularColumns.size()];
+            for (int j = 0; j < valueGenerators.length; j++)
+            {
+                valueGenerators[j] = new ValueDescriptorIndexGenerator(run.schemaSpec.regularColumns.get(j),
+                                                                       run.rng)
+                                     .toSupplier(entropySource.derive(), 20, 0.2f);
+            }
+
+            TokenPlacementModel.ReplicationFactor rf = new TokenPlacementModel.SimpleReplicationFactor(1);
+
+            int maxPartitionSize = 100;
+            modelChecker.init(new HistoryBuilder(seed, maxPartitionSize, 10, schema, rf))
+                        .step((history) -> {
+                            return history.insert();
+                        })
+                        .step((history) -> {
+                            return history.insert(entropySource.nextInt(maxPartitionSize));
+                        })
+                        .step((history) -> {
+                            int row = entropySource.nextInt(maxPartitionSize);
+                            long[] vds = new long[valueGenerators.length];
+                            for (int j = 0; j < valueGenerators.length; j++)
+                                vds[j] = valueGenerators[j].getAsLong();
+
+                            return history.insert(row, vds);
+                        })
+                        .step((history) -> {
+                            return history.deleteRow();
+                        })
+                        .step((history) -> {
+                            return history.deleteRow(entropySource.nextInt(maxPartitionSize));
+                        })
+                        .step(SingleOperationBuilder::deletePartition)
+                        .step(SingleOperationBuilder::deleteColumns)
+                        .step(SingleOperationBuilder::deleteRowSlice)
+                        .step((history) -> {
+                            return history.deleteRowRange();
+                        })
+                        .step((history) -> {
+                            return history.deleteRowRange(entropySource.nextInt(maxPartitionSize),
+                                                          entropySource.nextInt(maxPartitionSize),
+                                                          entropySource.nextBoolean(),
+                                                          entropySource.nextBoolean());
+                        })
+                        .step((history) -> history instanceof HistoryBuilder,
+                              (history) -> ((HistoryBuilder) history).beginBatch())
+                        .step((history) -> (history instanceof BatchVisitBuilder) && ((BatchVisitBuilder) history).size() > 1,
+                              (history) -> ((BatchVisitBuilder) history).endBatch())
+                        .exitCondition((history) -> {
+                            if (!(history instanceof HistoryBuilder))
+                                return false;
+
+                            HistoryBuilder historyBuilder = (HistoryBuilder) history;
+                            ReplayingVisitor visitor = historyBuilder.visitor(run.tracker, run.sut, SystemUnderTest.ConsistencyLevel.ALL);
+                            visitor.replayAll();
+
+                            if (historyBuilder.presetSelector.pds().size() < MAX_PARTITIONS)
+                                return false;
+
+                            Model model = historyBuilder.quiescentChecker(run.tracker, sut);
+
+                            for (Long pd : historyBuilder.presetSelector.pds())
+                                model.validate(Query.selectPartition(run.schemaSpec, pd,false));
+
+                            return true;
+                        })
+                        .run(STEPS_PER_ITERATION, seed);
+        }
+    }
+}
\ No newline at end of file
diff --git a/test/distributed/org/apache/cassandra/fuzz/harry/integration/generators/DataGeneratorsIntegrationTest.java b/test/distributed/org/apache/cassandra/fuzz/harry/integration/generators/DataGeneratorsIntegrationTest.java
new file mode 100644
index 0000000000..1ce9994c09
--- /dev/null
+++ b/test/distributed/org/apache/cassandra/fuzz/harry/integration/generators/DataGeneratorsIntegrationTest.java
@@ -0,0 +1,179 @@
+/*
+ * 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 org.apache.cassandra.fuzz.harry.integration.generators;
+
+import java.util.Random;
+import java.util.concurrent.CompletableFuture;
+
+import com.google.common.collect.Iterators;
+import org.junit.Test;
+
+import org.apache.cassandra.harry.HarryHelper;
+import org.apache.cassandra.harry.core.Configuration;
+import org.apache.cassandra.harry.core.Run;
+import org.apache.cassandra.harry.ddl.ColumnSpec;
+import org.apache.cassandra.harry.ddl.SchemaGenerators;
+import org.apache.cassandra.harry.ddl.SchemaSpec;
+import org.apache.cassandra.harry.gen.Generator;
+import org.apache.cassandra.harry.gen.Surjections;
+import org.apache.cassandra.harry.gen.distribution.Distribution;
+import org.apache.cassandra.harry.model.NoOpChecker;
+import org.apache.cassandra.harry.model.OpSelectors;
+import org.apache.cassandra.harry.sut.SystemUnderTest;
+import org.apache.cassandra.harry.visitors.MutatingVisitor;
+import org.apache.cassandra.harry.visitors.MutatingRowVisitor;
+import org.apache.cassandra.harry.visitors.SingleValidator;
+import org.apache.cassandra.harry.util.TestRunner;
+import org.apache.cassandra.harry.visitors.Visitor;
+import org.apache.cassandra.cql3.CQLTester;
+import org.apache.cassandra.cql3.UntypedResultSet;
+import org.apache.cassandra.distributed.impl.RowUtil;
+
+public class DataGeneratorsIntegrationTest extends CQLTester
+{
+    @Test
+    public void testTimestampTieResolution()
+    {
+        Random rng = new Random(1);
+        String ks = "test_timestamp_tie_resolution";
+        createKeyspace(String.format("CREATE KEYSPACE %s WITH replication = {'class': 'SimpleStrategy', 'replication_factor': 1}", ks));
+        int counter = 0;
+        for (ColumnSpec.DataType<?> dataType : new ColumnSpec.DataType[]{ ColumnSpec.int8Type,
+                                                                          ColumnSpec.int16Type,
+                                                                          ColumnSpec.int32Type,
+                                                                          ColumnSpec.int64Type,
+                                                                          ColumnSpec.asciiType,
+                                                                          ColumnSpec.floatType,
+                                                                          ColumnSpec.doubleType })
+        {
+
+
+            String tbl = "table_" + (counter++);
+            createTable(String.format("CREATE TABLE %s.%s (pk int PRIMARY KEY, v %s)",
+                                      ks, tbl,
+                                      dataType));
+            for (int i = 0; i < 10_000; i++)
+            {
+                long d1 = dataType.generator().adjustEntropyDomain(rng.nextLong());
+                long d2 = dataType.generator().adjustEntropyDomain(rng.nextLong());
+                for (long d : new long[]{ d1, d2 })
+                {
+                    execute(String.format("INSERT INTO %s.%s (pk, v) VALUES (?,?) USING TIMESTAMP 1", ks, tbl),
+                            i, dataType.generator().inflate(d));
+                }
+
+                if (dataType.compareLexicographically(d1, d2) > 0)
+                    assertRows(execute(String.format("SELECT v FROM %s.%s WHERE pk=?", ks, tbl), i),
+                               row(dataType.generator().inflate(d1)));
+                else
+                    assertRows(execute(String.format("SELECT v FROM %s.%s WHERE pk=?", ks, tbl), i),
+                               row(dataType.generator().inflate(d2)));
+            }
+        }
+    }
+
+    @Test
+    public void queryParseabilityTest() throws Throwable
+    {
+        Generator<SchemaSpec> gen = new SchemaGenerators.Builder(KEYSPACE).partitionKeyColumnCount(2, 4)
+                                                                          .clusteringColumnCount(1, 4)
+                                                                          .regularColumnCount(1, 4)
+                                                                          .staticColumnCount(1, 4)
+                                                                          .generator();
+
+        TestRunner.test(gen,
+                        (schema) -> {
+                            try
+                            {
+                                schema.validate();
+                            }
+                            catch (AssertionError e)
+                            {
+                                return;
+                            }
+                            createTable(schema.compile().cql());
+
+
+                            Configuration.ConfigurationBuilder builder = HarryHelper.defaultConfiguration()
+                                                                                    .setDataTracker(new Configuration.NoOpDataTrackerConfiguration())
+                                                                                    .setSchemaProvider(new Configuration.FixedSchemaProviderConfiguration(schema))
+                                                                                    .setSUT(CqlTesterSut::new);
+
+                            for (OpSelectors.OperationKind kind : OpSelectors.OperationKind.values())
+                            {
+                                Run run = builder
+                                          .setClusteringDescriptorSelector((rng, schema_) -> {
+                                              return new OpSelectors.DefaultDescriptorSelector(rng,
+                                                                                               OpSelectors.columnSelectorBuilder().forAll(schema_).build(),
+                                                                                               OpSelectors.OperationSelector.weighted(Surjections.weights(100), kind),
+                                                                                               new Distribution.ConstantDistribution(2),
+                                                                                               100);
+                                          })
+                                          .build()
+                                          .createRun();
+
+                                Visitor visitor = new MutatingVisitor(run, MutatingRowVisitor::new);
+                                for (int lts = 0; lts < 100; lts++)
+                                    visitor.visit();
+
+                                SingleValidator validator = new SingleValidator(100, run, NoOpChecker::new);
+                                for (int lts = 0; lts < 10; lts++)
+                                    validator.visit(lts);
+                            }
+                        });
+    }
+
+    public class CqlTesterSut implements SystemUnderTest
+    {
+        public boolean isShutdown()
+        {
+            return false;
+        }
+
+        public void shutdown()
+        {
+        }
+
+        public void schemaChange(String statement)
+        {
+            createTable(statement);
+        }
+
+        public Object[][] execute(String statement, ConsistencyLevel cl, Object... bindings)
+        {
+            try
+            {
+                UntypedResultSet res = DataGeneratorsIntegrationTest.this.execute(statement, bindings);
+                if (res == null)
+                    return new Object[][]{};
+
+                return Iterators.toArray(RowUtil.toIter(res), Object[].class);
+            }
+            catch (Throwable throwable)
+            {
+                throw new RuntimeException(throwable);
+            }
+        }
+
+        public CompletableFuture<Object[][]> executeAsync(String statement, ConsistencyLevel cl, Object... bindings)
+        {
+            return CompletableFuture.completedFuture(execute(statement, cl, bindings));
+        }
+    }
+}
\ No newline at end of file
diff --git a/test/distributed/org/apache/cassandra/fuzz/harry/integration/model/ConcurrentQuiescentCheckerIntegrationTest.java b/test/distributed/org/apache/cassandra/fuzz/harry/integration/model/ConcurrentQuiescentCheckerIntegrationTest.java
new file mode 100644
index 0000000000..192629b026
--- /dev/null
+++ b/test/distributed/org/apache/cassandra/fuzz/harry/integration/model/ConcurrentQuiescentCheckerIntegrationTest.java
@@ -0,0 +1,71 @@
+/*
+ * 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 org.apache.cassandra.fuzz.harry.integration.model;
+
+import java.util.concurrent.TimeUnit;
+import java.util.function.Supplier;
+
+import org.junit.Test;
+
+import org.apache.cassandra.harry.core.Configuration;
+import org.apache.cassandra.harry.ddl.SchemaGenerators;
+import org.apache.cassandra.harry.ddl.SchemaSpec;
+import org.apache.cassandra.harry.tracker.LockingDataTracker;
+import org.apache.cassandra.harry.runner.Runner;
+import org.apache.cassandra.harry.visitors.MutatingVisitor;
+import org.apache.cassandra.harry.visitors.QueryLogger;
+import org.apache.cassandra.harry.visitors.RandomPartitionValidator;
+
+import static org.apache.cassandra.harry.core.Configuration.VisitorPoolConfiguration.pool;
+import static java.util.Arrays.asList;
+
+public class ConcurrentQuiescentCheckerIntegrationTest extends ModelTestBase
+{
+    @Test
+    public void testConcurrentReadWriteWorkload() throws Throwable
+    {
+        Supplier<SchemaSpec> supplier = SchemaGenerators.progression(1);
+        int writeThreads = 2;
+        int readThreads = 2;
+
+        for (int i = 0; i < SchemaGenerators.GENERATORS_COUNT; i++)
+        {
+            SchemaSpec schema = supplier.get();
+
+            Configuration config = configuration(i, schema)
+                                   .setKeyspaceDdl("CREATE KEYSPACE IF NOT EXISTS " + schema.keyspace + " WITH replication = {'class': 'SimpleStrategy', 'replication_factor': 1};")
+                                   .setCreateSchema(true)
+                                   .setDropSchema(true)
+                                   .setDataTracker(LockingDataTracker::new)
+                                   .build();
+
+            Runner.concurrent(config,
+                              asList(pool("Writer", writeThreads, MutatingVisitor::new),
+                                     pool("Reader", readThreads, (run) -> new RandomPartitionValidator(run, modelConfiguration(), QueryLogger.NO_OP))),
+                              2, TimeUnit.MINUTES)
+                  .run();
+        }
+    }
+
+    @Override
+    protected Configuration.ModelConfiguration modelConfiguration()
+    {
+        return new Configuration.QuiescentCheckerConfig();
+    }
+}
\ No newline at end of file
diff --git a/test/distributed/org/apache/cassandra/fuzz/harry/integration/model/InJVMTokenAwareExecutorTest.java b/test/distributed/org/apache/cassandra/fuzz/harry/integration/model/InJVMTokenAwareExecutorTest.java
new file mode 100644
index 0000000000..bd7927a13e
--- /dev/null
+++ b/test/distributed/org/apache/cassandra/fuzz/harry/integration/model/InJVMTokenAwareExecutorTest.java
@@ -0,0 +1,98 @@
+/*
+ * 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 org.apache.cassandra.fuzz.harry.integration.model;
+
+import java.util.concurrent.TimeUnit;
+import java.util.function.Supplier;
+
+import org.junit.Before;
+import org.junit.BeforeClass;
+import org.junit.Test;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import org.apache.cassandra.harry.core.Configuration;
+import org.apache.cassandra.harry.core.Run;
+import org.apache.cassandra.harry.ddl.SchemaGenerators;
+import org.apache.cassandra.harry.ddl.SchemaSpec;
+import org.apache.cassandra.harry.model.QuiescentChecker;
+import org.apache.cassandra.harry.model.RepairingLocalStateValidator;
+import org.apache.cassandra.harry.sut.SystemUnderTest;
+import org.apache.cassandra.harry.sut.TokenPlacementModel;
+import org.apache.cassandra.harry.sut.injvm.InJVMTokenAwareVisitExecutor;
+import org.apache.cassandra.harry.sut.injvm.InJvmSut;
+import org.apache.cassandra.harry.sut.injvm.InJvmSutBase;
+import org.apache.cassandra.harry.runner.Runner;
+import org.apache.cassandra.harry.runner.UpToLtsRunner;
+import org.apache.cassandra.harry.visitors.MutatingVisitor;
+import org.apache.cassandra.distributed.Cluster;
+import org.apache.cassandra.distributed.api.Feature;
+
+public class InJVMTokenAwareExecutorTest extends IntegrationTestBase
+{
+    private static final Logger logger = LoggerFactory.getLogger(InJVMTokenAwareExecutorTest.class);
+
+    @BeforeClass
+    public static void before() throws Throwable
+    {
+        cluster = Cluster.build()
+                         .withNodes(5)
+                         .withConfig((cfg) -> InJvmSutBase.defaultConfig().accept(cfg.with(Feature.GOSSIP, Feature.NETWORK)))
+                         .createWithoutStarting();
+        cluster.setUncaughtExceptionsFilter(t -> {
+            logger.error("Caught exception, reporting during shutdown. Ignoring.", t);
+            return true;
+        });
+        cluster.startup();
+        cluster = init(cluster);
+        sut = new InJvmSut(cluster);
+    }
+
+    @Override
+    @Before
+    public void beforeEach()
+    {
+        cluster.schemaChange("DROP KEYSPACE IF EXISTS harry");
+        cluster.schemaChange("CREATE KEYSPACE harry WITH replication = {'class': 'SimpleStrategy', 'replication_factor': 3};");
+    }
+
+    @Test
+    public void testRepair() throws Throwable
+    {
+        Supplier<SchemaSpec> schemaGen = SchemaGenerators.progression(1);
+        for (int cnt = 0; cnt < SchemaGenerators.DEFAULT_RUNS; cnt++)
+        {
+            SchemaSpec schema = schemaGen.get();
+            Configuration.ConfigurationBuilder builder = sharedConfiguration(cnt, schema);
+
+            Configuration configuration = builder.build();
+            Run run = configuration.createRun();
+            run.sut.schemaChange(run.schemaSpec.compile().cql());
+
+            Runner.chain(configuration,
+                         UpToLtsRunner.factory(MutatingVisitor.factory(InJVMTokenAwareVisitExecutor.factory(new Configuration.MutatingRowVisitorConfiguration(),
+                                                                                                            SystemUnderTest.ConsistencyLevel.NODE_LOCAL,
+                                                                                                            new TokenPlacementModel.SimpleReplicationFactor(3))),
+                                               10_000, 2, TimeUnit.SECONDS),
+                         Runner.single(RepairingLocalStateValidator.factoryForTests(5, QuiescentChecker::new)))
+                  .run();
+        }
+    }
+}
diff --git a/test/distributed/org/apache/cassandra/fuzz/harry/integration/model/IntegrationTestBase.java b/test/distributed/org/apache/cassandra/fuzz/harry/integration/model/IntegrationTestBase.java
new file mode 100644
index 0000000000..fd410cef73
--- /dev/null
+++ b/test/distributed/org/apache/cassandra/fuzz/harry/integration/model/IntegrationTestBase.java
@@ -0,0 +1,116 @@
+/*
+ * 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 org.apache.cassandra.fuzz.harry.integration.model;
+
+import java.util.function.Supplier;
+
+import org.junit.AfterClass;
+import org.junit.Before;
+import org.junit.BeforeClass;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import org.apache.cassandra.harry.core.Configuration;
+import org.apache.cassandra.harry.ddl.SchemaGenerators;
+import org.apache.cassandra.harry.ddl.SchemaSpec;
+import org.apache.cassandra.harry.clock.OffsetClock;
+import org.apache.cassandra.harry.model.OpSelectors;
+import org.apache.cassandra.harry.sut.injvm.InJvmSut;
+import org.apache.cassandra.harry.sut.injvm.InJvmSutBase;
+import org.apache.cassandra.distributed.Cluster;
+import org.apache.cassandra.distributed.test.TestBaseImpl;
+
+public class IntegrationTestBase extends TestBaseImpl
+{
+    protected static final Logger logger = LoggerFactory.getLogger(IntegrationTestBase.class);
+    protected static Cluster cluster;
+    protected static InJvmSut sut;
+
+    @BeforeClass
+    public static void before() throws Throwable
+    {
+        cluster = Cluster.build()
+                         .withNodes(1)
+                         .withConfig(InJvmSutBase.defaultConfig())
+                         .createWithoutStarting();
+        cluster.setUncaughtExceptionsFilter(t -> {
+            logger.error("Caught exception, reporting during shutdown. Ignoring.", t);
+            return true;
+        });
+        cluster.startup();
+        cluster = init(cluster);
+        sut = new InJvmSut(cluster);
+    }
+
+    @AfterClass
+    public static void afterClass()
+    {
+        sut.shutdown();
+    }
+
+    @Before
+    public void beforeEach()
+    {
+        cluster.schemaChange("DROP KEYSPACE IF EXISTS harry");
+        cluster.schemaChange("CREATE KEYSPACE harry WITH replication = {'class': 'SimpleStrategy', 'replication_factor': 1};");
+    }
+
+    private static long seed = 0;
+    public static Supplier<Configuration.ConfigurationBuilder> sharedConfiguration()
+    {
+        Supplier<SchemaSpec> specGenerator = SchemaGenerators.progression(SchemaGenerators.DEFAULT_SWITCH_AFTER);
+        return () -> {
+            SchemaSpec schemaSpec = specGenerator.get();
+            return sharedConfiguration(seed, schemaSpec);
+        };
+    }
+
+    public static Configuration.CDSelectorConfigurationBuilder sharedCDSelectorConfiguration()
+    {
+        return new Configuration.CDSelectorConfigurationBuilder()
+               .setOperationsPerLtsDistribution(new Configuration.ConstantDistributionConfig(2))
+               .setMaxPartitionSize(100)
+               .setOperationKindWeights(new Configuration.OperationKindSelectorBuilder()
+                                        .addWeight(OpSelectors.OperationKind.DELETE_ROW, 1)
+                                        .addWeight(OpSelectors.OperationKind.DELETE_COLUMN, 1)
+                                        .addWeight(OpSelectors.OperationKind.DELETE_RANGE, 1)
+                                        .addWeight(OpSelectors.OperationKind.DELETE_SLICE, 1)
+                                        .addWeight(OpSelectors.OperationKind.DELETE_PARTITION, 1)
+                                        .addWeight(OpSelectors.OperationKind.DELETE_COLUMN_WITH_STATICS, 5)
+                                        .addWeight(OpSelectors.OperationKind.INSERT_WITH_STATICS, 20)
+                                        .addWeight(OpSelectors.OperationKind.INSERT, 20)
+                                        .addWeight(OpSelectors.OperationKind.UPDATE_WITH_STATICS, 25)
+                                        .addWeight(OpSelectors.OperationKind.UPDATE, 25)
+                                        .build());
+    }
+
+    public static Configuration.ConfigurationBuilder sharedConfiguration(long seed, SchemaSpec schema)
+    {
+        return new Configuration.ConfigurationBuilder().setSeed(seed)
+                                                       .setClock(() -> new OffsetClock(100000))
+                                                       .setCreateSchema(true)
+                                                       .setTruncateTable(false)
+                                                       .setDropSchema(true)
+                                                       .setSchemaProvider((seed1, sut) -> schema)
+                                                       .setClusteringDescriptorSelector(sharedCDSelectorConfiguration().build())
+                                                       .setPartitionDescriptorSelector(new Configuration.DefaultPDSelectorConfiguration(2, 200))
+                                                       .setSUT(() -> sut);
+    }
+}
\ No newline at end of file
diff --git a/test/distributed/org/apache/cassandra/fuzz/harry/integration/model/MockSchema.java b/test/distributed/org/apache/cassandra/fuzz/harry/integration/model/MockSchema.java
new file mode 100644
index 0000000000..8d7475be18
--- /dev/null
+++ b/test/distributed/org/apache/cassandra/fuzz/harry/integration/model/MockSchema.java
@@ -0,0 +1,117 @@
+/*
+ * 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 org.apache.cassandra.fuzz.harry.integration.model;
+
+import java.util.Arrays;
+
+import org.apache.cassandra.harry.ddl.ColumnSpec;
+import org.apache.cassandra.harry.ddl.SchemaGenerators;
+import org.apache.cassandra.harry.ddl.SchemaSpec;
+import org.apache.cassandra.harry.gen.Surjections;
+import org.apache.cassandra.harry.util.BitSet;
+
+public class MockSchema
+{
+    public static final String KEYSPACE = "harry";
+    public static final Surjections.Surjection<BitSet> columnMaskSelector1;
+    public static final Surjections.Surjection<BitSet> columnMaskSelector2;
+    public static final Surjections.Surjection<BitSet> compactColumnMaskSelector;
+
+    public static final SchemaSpec tbl1;
+    public static final SchemaSpec tbl2;
+    public static final SchemaSpec compact_schema;
+
+    static
+    {
+        columnMaskSelector1 = Surjections.pick(BitSet.create(0b0000111, 7),
+                                               BitSet.create(0b1110000, 7),
+                                               BitSet.create(0b1111111, 7));
+
+        columnMaskSelector2 = Surjections.pick(BitSet.create(0b11, 2),
+                                               BitSet.create(0b01, 2),
+                                               BitSet.create(0b10, 2));
+
+        compactColumnMaskSelector = Surjections.pick(BitSet.create(1, 1));
+
+        tbl1 = new SchemaSpec(KEYSPACE, "tbl1",
+                              Arrays.asList(ColumnSpec.pk("pk1", ColumnSpec.asciiType),
+                                            ColumnSpec.pk("pk2", ColumnSpec.int64Type)),
+                              Arrays.asList(ColumnSpec.ck("ck1", ColumnSpec.asciiType, false),
+                                            ColumnSpec.ck("ck2", ColumnSpec.int64Type, false)),
+                              Arrays.asList(ColumnSpec.regularColumn("v1", ColumnSpec.int32Type),
+                                            ColumnSpec.regularColumn("v2", ColumnSpec.int64Type),
+                                            // TODO: test boolean values; before that - submit a PR to Cassandra where we add boolean to ByteBufferUtil#objectToBytes
+                                            ColumnSpec.regularColumn("v3", ColumnSpec.int32Type),
+                                            ColumnSpec.regularColumn("v4", ColumnSpec.asciiType),
+                                            ColumnSpec.regularColumn("v5", ColumnSpec.int64Type),
+                                            ColumnSpec.regularColumn("v6", ColumnSpec.floatType),
+                                            ColumnSpec.regularColumn("v7", ColumnSpec.doubleType)),
+                              Arrays.asList(ColumnSpec.staticColumn("static1", ColumnSpec.asciiType),
+                                            ColumnSpec.staticColumn("static2", ColumnSpec.int64Type)));
+
+        tbl2 = new SchemaSpec(KEYSPACE, "tbl2",
+                              Arrays.asList(ColumnSpec.pk("pk1", ColumnSpec.asciiType),
+                                            ColumnSpec.pk("pk2", ColumnSpec.int64Type)),
+                              Arrays.asList(ColumnSpec.ck("ck1", ColumnSpec.asciiType, true)),
+                              Arrays.asList(ColumnSpec.regularColumn("v1", ColumnSpec.int32Type),
+                                            ColumnSpec.regularColumn("v2", ColumnSpec.asciiType)),
+                              Arrays.asList(ColumnSpec.regularColumn("static1", ColumnSpec.int32Type),
+                                            ColumnSpec.regularColumn("static2", ColumnSpec.asciiType)));
+
+        compact_schema = new SchemaSpec(KEYSPACE, "tbl3",
+                                        Arrays.asList(ColumnSpec.pk("pk1", ColumnSpec.asciiType),
+                                                      ColumnSpec.pk("pk2", ColumnSpec.int64Type)),
+                                        Arrays.asList(ColumnSpec.ck("ck1", ColumnSpec.asciiType, true),
+                                                      ColumnSpec.ck("ck2", ColumnSpec.int64Type, false)),
+                                        Arrays.asList(ColumnSpec.regularColumn("v1", ColumnSpec.asciiType)),
+                                        Arrays.asList(ColumnSpec.staticColumn("static1", ColumnSpec.asciiType)))
+                         .withCompactStorage();
+    }
+
+    public static SchemaSpec randomSchema(String keyspace, String table, long seed)
+    {
+        return new SchemaGenerators.Builder(keyspace, () -> table)
+               .partitionKeySpec(1, 4,
+//                                 ColumnSpec.int8Type,
+//                                 ColumnSpec.int16Type,
+//                                 ColumnSpec.int32Type,
+                                 ColumnSpec.int64Type,
+                                 ColumnSpec.floatType,
+                                 ColumnSpec.doubleType,
+                                 ColumnSpec.asciiType(4, 10))
+               .clusteringKeySpec(1, 4,
+//                                  ColumnSpec.int8Type,
+//                                  ColumnSpec.int16Type,
+//                                  ColumnSpec.int32Type,
+                                  ColumnSpec.int64Type,
+                                  ColumnSpec.floatType,
+                                  ColumnSpec.doubleType,
+                                  ColumnSpec.asciiType(4, 10))
+               .regularColumnSpec(2, 10,
+                                  ColumnSpec.int8Type,
+                                  ColumnSpec.int16Type,
+                                  ColumnSpec.int32Type,
+                                  ColumnSpec.int64Type,
+                                  ColumnSpec.floatType,
+                                  ColumnSpec.doubleType,
+                                  ColumnSpec.asciiType(5, 10))
+               .surjection()
+               .inflate(seed);
+    }
+}
diff --git a/test/distributed/org/apache/cassandra/fuzz/harry/integration/model/ModelTestBase.java b/test/distributed/org/apache/cassandra/fuzz/harry/integration/model/ModelTestBase.java
new file mode 100644
index 0000000000..2776aea930
--- /dev/null
+++ b/test/distributed/org/apache/cassandra/fuzz/harry/integration/model/ModelTestBase.java
@@ -0,0 +1,138 @@
+/*
+ * 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 org.apache.cassandra.fuzz.harry.integration.model;
+
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.concurrent.TimeUnit;
+import java.util.function.BiConsumer;
+import java.util.function.Function;
+import java.util.function.Supplier;
+
+import org.apache.cassandra.harry.core.Configuration;
+import org.apache.cassandra.harry.core.Run;
+import org.apache.cassandra.harry.ddl.SchemaGenerators;
+import org.apache.cassandra.harry.ddl.SchemaSpec;
+import org.apache.cassandra.harry.runner.Runner;
+import org.apache.cassandra.harry.runner.UpToLtsRunner;
+import org.apache.cassandra.harry.visitors.MutatingRowVisitor;
+import org.apache.cassandra.harry.visitors.MutatingVisitor;
+import org.apache.cassandra.harry.visitors.SingleValidator;
+
+public abstract class ModelTestBase extends IntegrationTestBase
+{
+    private final int ITERATIONS = 20_000;
+
+    void negativeTest(Function<Run, Boolean> corrupt, BiConsumer<Throwable, Run> validate) throws Throwable
+    {
+        Supplier<SchemaSpec> supplier = SchemaGenerators.progression(SchemaGenerators.DEFAULT_SWITCH_AFTER);
+        for (int i = 0; i < SchemaGenerators.DEFAULT_RUNS; i++)
+        {
+            SchemaSpec schema = supplier.get();
+            negativeTest(corrupt, validate, i, schema);
+        }
+    }
+
+    void negativeIntegrationTest(Configuration.RunnerConfiguration runnerConfig) throws Throwable
+    {
+        Supplier<SchemaSpec> supplier = SchemaGenerators.progression(1);
+        for (int i = 0; i < SchemaGenerators.DEFAULT_RUNS; i++)
+        {
+            SchemaSpec schema = supplier.get();
+            Configuration.ConfigurationBuilder builder = configuration(i, schema);
+
+            builder.setClock(new Configuration.ApproximateClockConfiguration((int) TimeUnit.MINUTES.toMillis(10),
+                                                                             1, TimeUnit.SECONDS))
+                   .setCreateSchema(false)
+                   .setDropSchema(false)
+                   .setRunner(runnerConfig);
+
+            Configuration config = builder.build();
+            Runner runner = config.createRunner();
+            
+            Run run = runner.getRun();
+            beforeEach();
+            run.sut.schemaChange(run.schemaSpec.compile().cql());
+            runner.run();
+        }
+    }
+
+    protected abstract Configuration.ModelConfiguration modelConfiguration();
+
+    protected SingleValidator validator(Run run)
+    {
+        return new SingleValidator(100, run , modelConfiguration());
+    }
+
+    public Configuration.ConfigurationBuilder configuration(long seed, SchemaSpec schema)
+    {
+        return sharedConfiguration(seed, schema);
+    }
+
+    void negativeTest(Function<Run, Boolean> corrupt, BiConsumer<Throwable, Run> validate, int counter, SchemaSpec schemaSpec) throws Throwable
+    {
+        Configuration config = configuration(counter, schemaSpec)
+                               .setCreateSchema(true)
+                               .setKeyspaceDdl(String.format("CREATE KEYSPACE IF NOT EXISTS %s WITH replication = {'class': 'SimpleStrategy', 'replication_factor': %d};",
+                                                             schemaSpec.keyspace, cluster.size()))
+                               .setDropSchema(true)
+                               .build();
+
+        Run run = config.createRun();
+
+        new Runner.ChainRunner(run, config,
+                               Arrays.asList(writer(ITERATIONS, 2, TimeUnit.MINUTES),
+                                             (r,  c) -> new Runner.SingleVisitRunner(r, c, Collections.singletonList(this::validator)) {
+                                                 @Override
+                                                 public void runInternal()
+                                                 {
+                                                     if (!corrupt.apply(run))
+                                                     {
+                                                         System.out.println("Could not corrupt");
+                                                         return;
+                                                     }
+                                                     try
+                                                     {
+                                                         super.runInternal();
+                                                         throw new ShouldHaveThrownException();
+                                                     }
+                                                     catch (Throwable t)
+                                                     {
+                                                         validate.accept(t, run);
+                                                     }
+                                                 }
+                                             })).run();
+    }
+
+    public static Configuration.RunnerConfiguration writer(long iterations, int runtime, TimeUnit timeUnit)
+    {
+        return (run, config) -> {
+            return new UpToLtsRunner(run, config,
+                                     Collections.singletonList((r_) -> new MutatingVisitor(r_, MutatingRowVisitor::new)),
+                                     iterations,
+                                     runtime, timeUnit);
+        };
+    }
+
+    public static class ShouldHaveThrownException extends AssertionError
+    {
+
+    }
+}
+
diff --git a/test/distributed/org/apache/cassandra/fuzz/harry/integration/model/QuerySelectorNegativeTest.java b/test/distributed/org/apache/cassandra/fuzz/harry/integration/model/QuerySelectorNegativeTest.java
new file mode 100644
index 0000000000..11c65e4ad9
--- /dev/null
+++ b/test/distributed/org/apache/cassandra/fuzz/harry/integration/model/QuerySelectorNegativeTest.java
@@ -0,0 +1,157 @@
+/*
+ * 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 org.apache.cassandra.fuzz.harry.integration.model;
+
+import java.util.Arrays;
+import java.util.Collection;
+import java.util.HashMap;
+import java.util.Map;
+import java.util.Random;
+import java.util.function.Supplier;
+
+import org.junit.Assert;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.Parameterized;
+
+import org.apache.cassandra.harry.core.Configuration;
+import org.apache.cassandra.harry.core.Run;
+import org.apache.cassandra.harry.corruptor.AddExtraRowCorruptor;
+import org.apache.cassandra.harry.corruptor.ChangeValueCorruptor;
+import org.apache.cassandra.harry.corruptor.HideRowCorruptor;
+import org.apache.cassandra.harry.corruptor.HideValueCorruptor;
+import org.apache.cassandra.harry.corruptor.QueryResponseCorruptor;
+import org.apache.cassandra.harry.corruptor.ShowValueCorruptor;
+import org.apache.cassandra.harry.ddl.SchemaGenerators;
+import org.apache.cassandra.harry.model.Model;
+import org.apache.cassandra.harry.model.QuiescentChecker;
+import org.apache.cassandra.harry.visitors.MutatingVisitor;
+import org.apache.cassandra.harry.visitors.MutatingRowVisitor;
+import org.apache.cassandra.harry.operations.Query;
+import org.apache.cassandra.harry.operations.QueryGenerator;
+import org.apache.cassandra.harry.visitors.Visitor;
+
+import static org.apache.cassandra.harry.corruptor.QueryResponseCorruptor.SimpleQueryResponseCorruptor;
+
+@RunWith(Parameterized.class)
+public class QuerySelectorNegativeTest extends IntegrationTestBase
+{
+    private final int CYCLES = 1000;
+
+    private final Random rnd = new Random();
+
+    private final QueryResponseCorruptorFactory corruptorFactory;
+
+    public QuerySelectorNegativeTest(QueryResponseCorruptorFactory corruptorFactory)
+    {
+        this.corruptorFactory = corruptorFactory;
+    }
+
+    @Parameterized.Parameters
+    public static Collection<QueryResponseCorruptorFactory> source()
+    {
+        return Arrays.asList((run) -> new SimpleQueryResponseCorruptor(run.schemaSpec,
+                                                                       run.clock,
+                                                                       ChangeValueCorruptor::new),
+                             (run) -> new SimpleQueryResponseCorruptor(run.schemaSpec,
+                                                                       run.clock,
+                                                                       HideValueCorruptor::new),
+                             (run) -> new SimpleQueryResponseCorruptor(run.schemaSpec,
+                                                                       run.clock,
+                                                                       ShowValueCorruptor::new),
+                             (run) -> new SimpleQueryResponseCorruptor(run.schemaSpec,
+                                                                       run.clock,
+                                                                       HideRowCorruptor::new),
+                             (run) -> new AddExtraRowCorruptor(run.schemaSpec,
+                                                               run.clock,
+                                                               run.tracker,
+                                                               run.descriptorSelector)
+        );
+    }
+
+    interface QueryResponseCorruptorFactory
+    {
+        QueryResponseCorruptor create(Run run);
+    }
+
+    @Test
+    public void selectRows()
+    {
+        Map<Query.QueryKind, Integer> stats = new HashMap<>();
+        Supplier<Configuration.ConfigurationBuilder> gen = sharedConfiguration();
+
+        int rounds = SchemaGenerators.DEFAULT_RUNS;
+        int failureCounter = 0;
+        outer:
+        for (int counter = 0; counter < rounds; counter++)
+        {
+            beforeEach();
+            Configuration config = gen.get()
+                                      .setClusteringDescriptorSelector(sharedCDSelectorConfiguration()
+                                                                       .setOperationsPerLtsDistribution(new Configuration.ConstantDistributionConfig(2))
+                                                                       .setMaxPartitionSize(2000)
+                                                                       .build())
+                                      .build();
+            Run run = config.createRun();
+            run.sut.schemaChange(run.schemaSpec.compile().cql());
+            System.out.println(run.schemaSpec.compile().cql());
+
+            Visitor visitor = new MutatingVisitor(run, MutatingRowVisitor::new);
+            Model model = new QuiescentChecker(run);
+
+            QueryResponseCorruptor corruptor = this.corruptorFactory.create(run);
+
+            for (int i = 0; i < CYCLES; i++)
+                visitor.visit();
+
+            while (true)
+            {
+                long verificationLts = rnd.nextInt(1000);
+                QueryGenerator queryGen = new QueryGenerator(run.schemaSpec,
+                                                             run.pdSelector,
+                                                             run.descriptorSelector,
+                                                             run.rng);
+
+                QueryGenerator.TypedQueryGenerator querySelector = new QueryGenerator.TypedQueryGenerator(run.rng, queryGen);
+                Query query = querySelector.inflate(verificationLts, counter);
+
+                model.validate(query);
+
+                if (!corruptor.maybeCorrupt(query, run.sut))
+                    continue;
+
+                try
+                {
+                    model.validate(query);
+                    Assert.fail("Should've failed");
+                }
+                catch (Throwable t)
+                {
+                    // expected
+                    failureCounter++;
+                    stats.compute(query.queryKind, (Query.QueryKind kind, Integer cnt) -> cnt == null ? 1 : (cnt + 1));
+                    continue outer;
+                }
+            }
+        }
+
+        Assert.assertTrue(String.format("Seen only %d failures", failureCounter),
+                          failureCounter > (rounds * 0.8));
+    }
+}
\ No newline at end of file
diff --git a/test/distributed/org/apache/cassandra/fuzz/harry/integration/model/QuerySelectorTest.java b/test/distributed/org/apache/cassandra/fuzz/harry/integration/model/QuerySelectorTest.java
new file mode 100644
index 0000000000..39fe2c89c5
--- /dev/null
+++ b/test/distributed/org/apache/cassandra/fuzz/harry/integration/model/QuerySelectorTest.java
@@ -0,0 +1,163 @@
+/*
+ * 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 org.apache.cassandra.fuzz.harry.integration.model;
+
+import java.util.HashSet;
+import java.util.Set;
+import java.util.function.Supplier;
+
+import org.junit.Assert;
+import org.junit.Test;
+
+import org.apache.cassandra.harry.core.Configuration;
+import org.apache.cassandra.harry.core.Run;
+import org.apache.cassandra.harry.ddl.SchemaGenerators;
+import org.apache.cassandra.harry.ddl.SchemaSpec;
+import org.apache.cassandra.harry.model.Model;
+import org.apache.cassandra.harry.model.QuiescentChecker;
+import org.apache.cassandra.harry.model.SelectHelper;
+import org.apache.cassandra.harry.sut.SystemUnderTest;
+import org.apache.cassandra.harry.operations.CompiledStatement;
+import org.apache.cassandra.harry.visitors.MutatingVisitor;
+import org.apache.cassandra.harry.visitors.MutatingRowVisitor;
+import org.apache.cassandra.harry.operations.Query;
+import org.apache.cassandra.harry.operations.QueryGenerator;
+import org.apache.cassandra.harry.visitors.Visitor;
+
+import static org.apache.cassandra.harry.gen.DataGenerators.NIL_DESCR;
+
+public class QuerySelectorTest extends IntegrationTestBase
+{
+    private static int CYCLES = 300;
+
+    @Test
+    public void basicQuerySelectorTest()
+    {
+        Supplier<SchemaSpec> schemaGen = SchemaGenerators.progression(SchemaGenerators.DEFAULT_SWITCH_AFTER);
+        for (int cnt = 0; cnt < SchemaGenerators.DEFAULT_RUNS; cnt++)
+        {
+            beforeEach();
+            SchemaSpec schemaSpec = schemaGen.get();
+            int partitionSize = 200;
+
+            int[] fractions = new int[schemaSpec.clusteringKeys.size()];
+            int last = partitionSize;
+            for (int i = fractions.length - 1; i >= 0; i--)
+            {
+                fractions[i] = last;
+                last = last / 2;
+            }
+
+            Configuration config = sharedConfiguration(cnt, schemaSpec)
+                                   .setClusteringDescriptorSelector(sharedCDSelectorConfiguration()
+                                                                    .setMaxPartitionSize(partitionSize)
+                                                                    .setFractions(fractions)
+                                                                    .build())
+                                   .build();
+
+            Run run = config.createRun();
+            run.sut.schemaChange(run.schemaSpec.compile().cql());
+
+            Visitor visitor = new MutatingVisitor(run, MutatingRowVisitor::new);
+
+            for (int i = 0; i < CYCLES; i++)
+                visitor.visit();
+
+            QueryGenerator.TypedQueryGenerator querySelector = new QueryGenerator.TypedQueryGenerator(run);
+
+            for (int i = 0; i < CYCLES; i++)
+            {
+                Query query = querySelector.inflate(i, i);
+
+                Object[][] results = run.sut.execute(query.toSelectStatement(), SystemUnderTest.ConsistencyLevel.QUORUM);
+                Set<Long> matchingClusterings = new HashSet<>();
+                for (Object[] row : results)
+                {
+                    long cd = SelectHelper.resultSetToRow(run.schemaSpec,
+                                                          run.clock,
+                                                          row).cd;
+                    matchingClusterings.add(cd);
+                }
+
+                // the simplest test there can be: every row that is in the partition and was returned by the query,
+                // has to "match", every other row has to be a non-match
+                CompiledStatement selectPartition = SelectHelper.select(run.schemaSpec, run.pdSelector.pd(i, schemaSpec));
+                Object[][] partition = run.sut.execute(selectPartition, SystemUnderTest.ConsistencyLevel.QUORUM);
+                for (Object[] row : partition)
+                {
+                    long cd = SelectHelper.resultSetToRow(run.schemaSpec,
+                                                          run.clock,
+                                                          row).cd;
+
+                    // Skip static clustering
+                    if (cd == NIL_DESCR)
+                        continue;
+
+                    boolean expected = matchingClusterings.contains(cd);
+                    boolean actual = query.matchCd(cd);
+                    Assert.assertEquals(String.format("Mismatch for clustering: %d. Expected: %s. Actual: %s.\nQuery: %s",
+                                                      cd, expected, actual, query.toSelectStatement()),
+                                        expected,
+                                        actual);
+                }
+            }
+        }
+    }
+
+    @Test
+    public void querySelectorModelTest()
+    {
+        Supplier<SchemaSpec> gen = SchemaGenerators.progression(SchemaGenerators.DEFAULT_SWITCH_AFTER);
+        for (int cnt = 0; cnt < SchemaGenerators.DEFAULT_RUNS; cnt++)
+        {
+            SchemaSpec schemaSpec = gen.get();
+            int[] fractions = new int[schemaSpec.clusteringKeys.size()];
+            int partitionSize = 200;
+            int last = partitionSize;
+            for (int i = fractions.length - 1; i >= 0; i--)
+            {
+                fractions[i] = last;
+                last = last / 2;
+            }
+
+            Configuration config = sharedConfiguration(cnt, schemaSpec)
+                                   .setClusteringDescriptorSelector(sharedCDSelectorConfiguration()
+                                                                    .setMaxPartitionSize(partitionSize)
+                                                                    .setFractions(fractions)
+                                                                    .build())
+                                   .build();
+            Run run = config.createRun();
+            run.sut.schemaChange(run.schemaSpec.compile().cql());
+            Visitor visitor = new MutatingVisitor(run, MutatingRowVisitor::new);
+
+            for (int i = 0; i < CYCLES; i++)
+                visitor.visit();
+
+            QueryGenerator.TypedQueryGenerator querySelector = new QueryGenerator.TypedQueryGenerator(run);
+            Model model = new QuiescentChecker(run);
+
+            long verificationLts = 10;
+            for (int i = 0; i < CYCLES; i++)
+            {
+                Query query = querySelector.inflate(verificationLts, i);
+                model.validate(query);
+            }
+        }
+    }
+}
\ No newline at end of file
diff --git a/test/distributed/org/apache/cassandra/fuzz/harry/integration/model/QuiescentCheckerIntegrationTest.java b/test/distributed/org/apache/cassandra/fuzz/harry/integration/model/QuiescentCheckerIntegrationTest.java
new file mode 100644
index 0000000000..ffb0177897
--- /dev/null
+++ b/test/distributed/org/apache/cassandra/fuzz/harry/integration/model/QuiescentCheckerIntegrationTest.java
@@ -0,0 +1,224 @@
+/*
+ * 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 org.apache.cassandra.fuzz.harry.integration.model;
+
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.concurrent.TimeUnit;
+
+import org.junit.Test;
+
+import org.apache.cassandra.harry.core.Configuration;
+import org.apache.cassandra.harry.core.Configuration.AllPartitionsValidatorConfiguration;
+import org.apache.cassandra.harry.core.Configuration.ConcurrentRunnerConfig;
+import org.apache.cassandra.harry.core.Configuration.LoggingVisitorConfiguration;
+import org.apache.cassandra.harry.core.Configuration.RecentPartitionsValidatorConfiguration;
+import org.apache.cassandra.harry.core.Configuration.SequentialRunnerConfig;
+import org.apache.cassandra.harry.core.Configuration.SingleVisitRunnerConfig;
+import org.apache.cassandra.harry.core.Run;
+import org.apache.cassandra.harry.corruptor.AddExtraRowCorruptor;
+import org.apache.cassandra.harry.corruptor.ChangeValueCorruptor;
+import org.apache.cassandra.harry.corruptor.HideRowCorruptor;
+import org.apache.cassandra.harry.corruptor.HideValueCorruptor;
+import org.apache.cassandra.harry.corruptor.QueryResponseCorruptor;
+import org.apache.cassandra.harry.corruptor.QueryResponseCorruptor.SimpleQueryResponseCorruptor;
+import org.apache.cassandra.harry.ddl.SchemaSpec;
+import org.apache.cassandra.harry.model.Model;
+import org.apache.cassandra.harry.operations.Query;
+import org.apache.cassandra.harry.runner.StagedRunner.StagedRunnerConfig;
+import org.apache.cassandra.harry.visitors.QueryLogger;
+import org.apache.cassandra.harry.visitors.SingleValidator;
+
+public class QuiescentCheckerIntegrationTest extends ModelTestBase
+{
+    private final long CORRUPT_LTS = 0L;
+
+    @Override
+    protected SingleValidator validator(Run run)
+    {
+        return new SingleValidator(100, run, modelConfiguration()) {
+            public void visit()
+            {
+                visit(CORRUPT_LTS);
+            }
+        };
+    }
+
+    @Test
+    public void testNormalCondition() throws Throwable
+    {
+        negativeTest((run) -> true,
+                     (t, run) -> {
+                         if (!(t instanceof ShouldHaveThrownException))
+                             throw new AssertionError(String.format("Throwable was supposed to be null. Schema: %s",
+                                                                    run.schemaSpec.compile().cql()),
+                                                      t);
+                     });
+    }
+
+    @Test
+    public void normalConditionIntegrationTest() throws Throwable
+    {
+        Model.ModelFactory factory = modelConfiguration();
+                
+        SequentialRunnerConfig sequential = 
+                new SequentialRunnerConfig(Arrays.asList(new LoggingVisitorConfiguration(new Configuration.MutatingRowVisitorConfiguration()),
+                                                         new RecentPartitionsValidatorConfiguration(10, 10, factory::make, () -> QueryLogger.NO_OP),
+                                                         new AllPartitionsValidatorConfiguration(10, factory::make, () -> QueryLogger.NO_OP)),
+                                           1, TimeUnit.MINUTES);
+        negativeIntegrationTest(sequential);
+    }
+
+    @Test
+    public void normalConditionStagedIntegrationTest() throws Throwable
+    {
+        Model.ModelFactory factory = modelConfiguration();
+
+        ConcurrentRunnerConfig concurrent = 
+                new ConcurrentRunnerConfig(Arrays.asList(new Configuration.VisitorPoolConfiguration("Writer", 4, new Configuration.MutatingVisitorConfiguation(new Configuration.MutatingRowVisitorConfiguration()))),
+                                           30, TimeUnit.SECONDS);
+        SingleVisitRunnerConfig sequential = 
+                new SingleVisitRunnerConfig(Collections.singletonList(new RecentPartitionsValidatorConfiguration(1024, 0, factory::make, () -> QueryLogger.NO_OP)));
+        
+        StagedRunnerConfig staged = new StagedRunnerConfig(Arrays.asList(concurrent, sequential), 2, TimeUnit.MINUTES);
+
+        negativeIntegrationTest(staged);
+    }
+
+    @Test
+    public void testDetectsMissingRow() throws Throwable
+    {
+        negativeTest((run) -> {
+                         SimpleQueryResponseCorruptor corruptor = new SimpleQueryResponseCorruptor(run.schemaSpec,
+                                                                                                   run.clock,
+                                                                                                   HideRowCorruptor::new);
+
+                         Query query = Query.selectPartition(run.schemaSpec,
+                                                             run.pdSelector.pd(CORRUPT_LTS, run.schemaSpec),
+                                                             false);
+
+                         return corruptor.maybeCorrupt(query, run.sut);
+                     },
+                     (t, run) -> {
+                         // TODO: We can actually pinpoint the difference
+                         String expected = "Expected results to have the same number of results, but expected result iterator has more results";
+                         String expected2 = "Found a row in the model that is not present in the resultset";
+
+                         if (t.getMessage().contains(expected) || t.getMessage().contains(expected2))
+                             return;
+
+                         throw new AssertionError(String.format("Exception string mismatch.\nExpected error: %s.\nActual error: %s", expected, t.getMessage()),
+                                                  t);
+                     });
+    }
+
+    @Test
+    public void testDetectsExtraRow() throws Throwable
+    {
+        negativeTest((run) -> {
+                         QueryResponseCorruptor corruptor = new AddExtraRowCorruptor(run.schemaSpec,
+                                                                                     run.clock,
+                                                                                     run.tracker,
+                                                                                     run.descriptorSelector);
+
+                         return corruptor.maybeCorrupt(Query.selectPartition(run.schemaSpec,
+                                                                             run.pdSelector.pd(CORRUPT_LTS, run.schemaSpec),
+                                                                             false),
+                                                       run.sut);
+                     },
+                     (t, run) -> {
+                         String expected = "Found a row in the model that is not present in the resultset";
+                         String expected2 = "Expected results to have the same number of results, but actual result iterator has more results";
+                         String expected3 = "Found a row while model predicts statics only";
+                         if (t.getMessage().contains(expected) || t.getMessage().contains(expected2) || t.getMessage().contains(expected3))
+                             return;
+
+                         throw new AssertionError(String.format("Exception string mismatch.\nExpected error: %s.\nActual error: %s", expected, t.getMessage()),
+                                                  t);
+                     });
+    }
+
+
+    @Test
+    public void testDetectsRemovedColumn() throws Throwable
+    {
+        negativeTest((run) -> {
+                         SimpleQueryResponseCorruptor corruptor = new SimpleQueryResponseCorruptor(run.schemaSpec,
+                                                                                                   run.clock,
+                                                                                                   HideValueCorruptor::new);
+
+                         return corruptor.maybeCorrupt(Query.selectPartition(run.schemaSpec,
+                                                                             run.pdSelector.pd(CORRUPT_LTS, run.schemaSpec),
+                                                                             false),
+                                                       run.sut);
+                     },
+                     (t, run) -> {
+                         String expected = "doesn't match the one predicted by the model";
+                         String expected2 = "don't match ones predicted by the model";
+                         String expected3 = "Found a row in the model that is not present in the resultset";
+                         if (t.getMessage().contains(expected) || t.getMessage().contains(expected2) || t.getMessage().contains(expected3))
+                             return;
+
+                         throw new AssertionError(String.format("Exception string mismatch.\nExpected error: %s.\nActual error: %s", expected, t.getMessage()),
+                                                  t);
+                     });
+    }
+
+
+    @Test
+    public void testDetectsOverwrittenRow() throws Throwable
+    {
+        negativeTest((run) -> {
+                         SimpleQueryResponseCorruptor corruptor = new SimpleQueryResponseCorruptor(run.schemaSpec,
+                                                                                                   run.clock,
+                                                                                                   ChangeValueCorruptor::new);
+
+                         return corruptor.maybeCorrupt(Query.selectPartition(run.schemaSpec,
+                                                                             run.pdSelector.pd(CORRUPT_LTS, run.schemaSpec),
+                                                                             false),
+                                                       run.sut);
+                     },
+                     (t, run) -> {
+                         String expected = "Returned row state doesn't match the one predicted by the model";
+                         String expected2 = "Timestamps in the row state don't match ones predicted by the model";
+
+                         if (t.getMessage() != null &&
+                             (t.getMessage().contains(expected) || t.getMessage().contains(expected2)))
+                             return;
+
+                         throw new AssertionError(String.format("Exception string mismatch.\nExpected error: %s.\nActual error: %s", expected, t.getMessage()),
+                                                  t);
+                     });
+    }
+
+    @Override
+    protected Configuration.ModelConfiguration modelConfiguration()
+    {
+        return new Configuration.QuiescentCheckerConfig();
+    }
+
+    public Configuration.ConfigurationBuilder configuration(long seed, SchemaSpec schema)
+    {
+        return super.configuration(seed, schema)
+                    .setClusteringDescriptorSelector(sharedCDSelectorConfiguration()
+                                                     .setOperationsPerLtsDistribution(new Configuration.ConstantDistributionConfig(2))
+                                                     .setMaxPartitionSize(100)
+                                                     .build());
+    }
+}
\ No newline at end of file
diff --git a/test/distributed/org/apache/cassandra/fuzz/harry/integration/model/QuiescentLocalStateCheckerIntegrationTest.java b/test/distributed/org/apache/cassandra/fuzz/harry/integration/model/QuiescentLocalStateCheckerIntegrationTest.java
new file mode 100644
index 0000000000..2ebfaa55cd
--- /dev/null
+++ b/test/distributed/org/apache/cassandra/fuzz/harry/integration/model/QuiescentLocalStateCheckerIntegrationTest.java
@@ -0,0 +1,79 @@
+/*
+ * 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 org.apache.cassandra.fuzz.harry.integration.model;
+
+import java.util.concurrent.TimeUnit;
+import java.util.function.Supplier;
+
+import org.junit.BeforeClass;
+import org.junit.Test;
+
+import org.apache.cassandra.harry.core.Configuration;
+import org.apache.cassandra.harry.ddl.SchemaGenerators;
+import org.apache.cassandra.harry.ddl.SchemaSpec;
+import org.apache.cassandra.harry.sut.TokenPlacementModel;
+import org.apache.cassandra.harry.sut.injvm.InJvmSut;
+import org.apache.cassandra.harry.sut.injvm.InJvmSutBase;
+import org.apache.cassandra.harry.sut.injvm.QuiescentLocalStateChecker;
+import org.apache.cassandra.harry.runner.Runner;
+import org.apache.cassandra.harry.visitors.AllPartitionsValidator;
+import org.apache.cassandra.harry.visitors.MutatingVisitor;
+import org.apache.cassandra.distributed.Cluster;
+import org.apache.cassandra.distributed.api.Feature;
+
+public class QuiescentLocalStateCheckerIntegrationTest extends ModelTestBase
+{
+    @BeforeClass
+    public static void before() throws Throwable
+    {
+        cluster = init(Cluster.build()
+                              .withNodes(2)
+                              .withConfig(InJvmSutBase.defaultConfig().andThen((cfg) -> cfg.with(Feature.GOSSIP)))
+                              .start());
+        sut = new InJvmSut(cluster);
+    }
+
+    @Test
+    public void testQuiescentLocalStateChecker() throws Throwable
+    {
+        Supplier<SchemaSpec> supplier = SchemaGenerators.progression(1);
+        for (int i = 0; i < SchemaGenerators.GENERATORS_COUNT; i++)
+        {
+            SchemaSpec schema = supplier.get();
+
+            Configuration config = configuration(i, schema)
+                                   .setKeyspaceDdl("CREATE KEYSPACE IF NOT EXISTS " + schema.keyspace + " WITH replication = {'class': 'SimpleStrategy', 'replication_factor': 1};")
+                                   .setCreateSchema(true)
+                                   .setDropSchema(true)
+                                   .build();
+
+            Runner.chain(config,
+                         Runner.sequential(MutatingVisitor::new, 2, TimeUnit.SECONDS),
+                         Runner.single(AllPartitionsValidator.factoryForTests(1, QuiescentLocalStateChecker.factory(new TokenPlacementModel.SimpleReplicationFactor(1)))))
+                  .run();
+            break;
+        }
+    }
+
+    @Override
+    protected Configuration.ModelConfiguration modelConfiguration()
+    {
+        return new Configuration.QuiescentCheckerConfig();
+    }
+}
\ No newline at end of file
diff --git a/test/distributed/org/apache/cassandra/fuzz/harry/integration/model/ReconcilerIntegrationTest.java b/test/distributed/org/apache/cassandra/fuzz/harry/integration/model/ReconcilerIntegrationTest.java
new file mode 100644
index 0000000000..fecc321bbb
--- /dev/null
+++ b/test/distributed/org/apache/cassandra/fuzz/harry/integration/model/ReconcilerIntegrationTest.java
@@ -0,0 +1,94 @@
+/*
+ * 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 org.apache.cassandra.fuzz.harry.integration.model;
+
+import java.util.Arrays;
+
+import org.junit.Test;
+
+import org.apache.cassandra.harry.ddl.ColumnSpec;
+import org.apache.cassandra.harry.ddl.SchemaSpec;
+import org.apache.cassandra.harry.dsl.ReplayingHistoryBuilder;
+import org.apache.cassandra.harry.gen.DataGenerators;
+import org.apache.cassandra.harry.sut.SystemUnderTest;
+import org.apache.cassandra.harry.sut.TokenPlacementModel;
+import org.apache.cassandra.harry.tracker.DefaultDataTracker;
+
+public class ReconcilerIntegrationTest extends IntegrationTestBase
+{
+    private final long seed = 1; // 88
+
+
+    @Test
+    public void testTrackingWithStatics() throws Throwable
+    {
+        SchemaSpec schema = new SchemaSpec(KEYSPACE, "tbl1",
+                                           Arrays.asList(ColumnSpec.pk("pk1", ColumnSpec.int64Type)),
+                                           Arrays.asList(ColumnSpec.ck("ck1", ColumnSpec.int64Type)),
+                                           Arrays.asList(ColumnSpec.regularColumn("v1", ColumnSpec.int64Type),
+                                                         ColumnSpec.regularColumn("v2", ColumnSpec.int64Type)),
+                                           Arrays.asList(ColumnSpec.staticColumn("static1", ColumnSpec.int64Type),
+                                                         ColumnSpec.staticColumn("static2", ColumnSpec.int64Type)))
+                            .trackLts();
+
+        beforeEach();
+        sut.schemaChange(schema.compile().cql());
+
+        TokenPlacementModel.ReplicationFactor rf = new TokenPlacementModel.SimpleReplicationFactor(1);
+        ReplayingHistoryBuilder historyBuilder = new ReplayingHistoryBuilder(seed, 100, 1, new DefaultDataTracker(), sut, schema, rf, SystemUnderTest.ConsistencyLevel.QUORUM);
+        historyBuilder.visitPartition(1).insert(1,
+                                                new long[]{ DataGenerators.UNSET_DESCR, DataGenerators.UNSET_DESCR },
+                                                new long[]{ 1L, 1L });
+        historyBuilder.validate(1);
+
+        historyBuilder.visitPartition(2).insert(2,
+                                                new long[]{ 1L, 1L },
+                                                new long[]{ 1L, 1L });
+        historyBuilder.visitPartition(2).deleteRowRange(1, 3, true, true);
+        historyBuilder.validate(2);
+    }
+
+    @Test
+    public void testTrackingWithoutStatics() throws Throwable
+    {
+        SchemaSpec schema = new SchemaSpec(KEYSPACE, "tbl1",
+                                           Arrays.asList(ColumnSpec.pk("pk1", ColumnSpec.int64Type)),
+                                           Arrays.asList(ColumnSpec.ck("ck1", ColumnSpec.int64Type)),
+                                           Arrays.asList(ColumnSpec.regularColumn("v1", ColumnSpec.int64Type),
+                                                         ColumnSpec.regularColumn("v2", ColumnSpec.int64Type)),
+                                           Arrays.asList())
+                            .trackLts();
+
+        beforeEach();
+        sut.schemaChange(schema.compile().cql());
+
+        TokenPlacementModel.ReplicationFactor rf = new TokenPlacementModel.SimpleReplicationFactor(1);
+        ReplayingHistoryBuilder historyBuilder = new ReplayingHistoryBuilder(seed, 100, 1, new DefaultDataTracker(), sut, schema, rf, SystemUnderTest.ConsistencyLevel.QUORUM);
+                                                                             historyBuilder.visitPartition(2).insert(2,
+                                                new long[]{ 1L, 1L });
+        historyBuilder.visitPartition(2).deleteRowRange(1, 3, true, true);
+        historyBuilder.validate(2);
+
+        historyBuilder = new ReplayingHistoryBuilder(seed, 100, 1, new DefaultDataTracker(), sut, schema, rf, SystemUnderTest.ConsistencyLevel.QUORUM);
+        historyBuilder.visitPartition(2).insert(2,
+                                                new long[]{ 1L, 1L });
+        historyBuilder.visitPartition(2).deleteRowRange(1, 3, true, true);
+        historyBuilder.validate(2);
+    }
+}
diff --git a/test/distributed/org/apache/cassandra/fuzz/harry/integration/model/reconciler/SimpleReconcilerTest.java b/test/distributed/org/apache/cassandra/fuzz/harry/integration/model/reconciler/SimpleReconcilerTest.java
new file mode 100644
index 0000000000..d240ecf037
--- /dev/null
+++ b/test/distributed/org/apache/cassandra/fuzz/harry/integration/model/reconciler/SimpleReconcilerTest.java
@@ -0,0 +1,341 @@
+/*
+ * 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 org.apache.cassandra.fuzz.harry.integration.model.reconciler;
+
+import java.util.*;
+
+import org.junit.Test;
+
+import org.apache.cassandra.harry.core.Configuration;
+import org.apache.cassandra.harry.core.Run;
+import org.apache.cassandra.harry.ddl.ColumnSpec;
+import org.apache.cassandra.harry.ddl.SchemaGenerators;
+import org.apache.cassandra.harry.ddl.SchemaSpec;
+import org.apache.cassandra.harry.gen.Surjections;
+import org.apache.cassandra.harry.model.OpSelectors;
+import org.apache.cassandra.harry.model.QuiescentChecker;
+import org.apache.cassandra.harry.model.SelectHelper;
+import org.apache.cassandra.harry.model.reconciler.PartitionState;
+import org.apache.cassandra.harry.sut.injvm.InJvmSut;
+import org.apache.cassandra.harry.operations.CompiledStatement;
+import org.apache.cassandra.harry.operations.Query;
+import org.apache.cassandra.harry.operations.WriteHelper;
+import org.apache.cassandra.harry.util.BitSet;
+import org.apache.cassandra.distributed.api.ConsistencyLevel;
+import org.apache.cassandra.fuzz.harry.integration.model.IntegrationTestBase;
+
+public class SimpleReconcilerTest extends IntegrationTestBase
+{
+    public static Surjections.Surjection<SchemaSpec> defaultSchemaSpecGen(String ks, String table)
+    {
+        return new SchemaGenerators.Builder(ks, () -> table)
+               .partitionKeySpec(1, 3,
+                                 ColumnSpec.int64Type,
+                                 ColumnSpec.asciiType(5, 256))
+               .clusteringKeySpec(1, 3,
+                                  ColumnSpec.int64Type,
+                                  ColumnSpec.asciiType(2, 3),
+                                  ColumnSpec.ReversedType.getInstance(ColumnSpec.int64Type),
+                                  ColumnSpec.ReversedType.getInstance(ColumnSpec.asciiType(2, 3)))
+               .regularColumnSpec(50, 50,
+                                  ColumnSpec.int64Type,
+                                  ColumnSpec.asciiType(5, 256))
+               .staticColumnSpec(50, 50,
+                                 ColumnSpec.int64Type,
+                                 ColumnSpec.asciiType(4, 256))
+               .surjection();
+    }
+
+    @Test
+    public void testStatics() throws Throwable
+    {
+        int rowsPerPartition = 50;
+        SchemaSpec schema = defaultSchemaSpecGen("harry", "tbl").inflate(1);
+        Configuration config = sharedConfiguration(1, schema).build();
+        Run run = config.createRun();
+        SyntheticTest test = new SyntheticTest(run.rng, schema);
+        beforeEach();
+        cluster.schemaChange(schema.compile().cql());
+
+        ModelState state = new ModelState(new HashMap<>());
+        InJvmSut sut = (InJvmSut) run.sut;
+        Random rng = new Random(1);
+
+        int partitionIdx = 0;
+
+        for (int i = 0; i < 100; i++)
+        {
+            BitSet subset = BitSet.allUnset(schema.allColumns.size());
+            for (int j = 0; j < subset.size(); j++)
+            {
+                if (rng.nextBoolean())
+                    subset.set(j);
+            }
+            if (!isValidSubset(schema.allColumns, subset))
+                continue;
+            int pdIdx = partitionIdx++;
+            long pd = test.pd(pdIdx);
+
+            for (int j = 0; j < 10; j++)
+            {
+                int cdIdx = rng.nextInt(rowsPerPartition);
+                long cd = test.cd(pdIdx, cdIdx);
+
+                long[] vds = run.descriptorSelector.descriptors(pd, cd, state.lts, 0, schema.regularColumns,
+                                                                schema.regularColumnsMask(),
+                                                                subset,
+                                                                schema.regularColumnsOffset);
+                long[] sds = run.descriptorSelector.descriptors(pd, cd, state.lts, 0, schema.staticColumns,
+                                                                schema.staticColumnsMask,
+                                                                subset,
+                                                                schema.staticColumnsOffset);
+
+                CompiledStatement statement = WriteHelper.inflateUpdate(schema, pd, cd, vds, sds, run.clock.rts(state.lts));
+                sut.cluster.coordinator(1).execute(statement.cql(), ConsistencyLevel.QUORUM, statement.bindings());
+
+                PartitionState partitionState = state.state.get(pd);
+                if (partitionState == null)
+                {
+                    partitionState = new PartitionState(pd, -1,  schema);
+                    state.state.put(pd, partitionState);
+                }
+
+                partitionState.writeStaticRow(sds, state.lts);
+                partitionState.write(cd, vds, state.lts, true);
+
+                state.lts++;
+            }
+        }
+
+        // Validate that all partitions correspond to our expectations
+        for (Long pd : state.state.keySet())
+        {
+            ArrayList<Long> clusteringDescriptors = new ArrayList<>(state.state.get(pd).rows().keySet());
+
+            // TODO: allow sub-selection
+            // Try different column subsets
+            for (int i = 0; i < 10; i++)
+            {
+                BitSet bitset = BitSet.allUnset(schema.allColumns.size());
+                for (int j = 0; j < bitset.size(); j++)
+                {
+                    if (rng.nextBoolean())
+                        bitset.set(j);
+                }
+                Set<ColumnSpec<?>> subset = i == 0 ? null : subset(schema.allColumns, bitset);
+                if (subset != null && !isValidSubset(schema.allColumns, bitset))
+                    continue;
+
+                int a = rng.nextInt(clusteringDescriptors.size());
+                long cd1tmp = clusteringDescriptors.get(a);
+                long cd2tmp;
+                int b;
+                while (true)
+                {
+                    b = rng.nextInt(clusteringDescriptors.size());
+                    long tmp = clusteringDescriptors.get(b);
+                    if (tmp != cd1tmp)
+                    {
+                        cd2tmp = tmp;
+                        break;
+                    }
+                }
+
+                long cd1 = Math.min(cd1tmp, cd2tmp);
+                long cd2 = Math.max(cd1tmp, cd2tmp);
+
+                for (boolean reverse : new boolean[]{ true, false })
+                {
+                    Query query;
+
+                    query = Query.selectPartition(schema, pd, reverse);
+
+                    QuiescentChecker.validate(schema,
+                                              run.tracker,
+                                              subset,
+                                              state.state.get(pd),
+                                              SelectHelper.execute(sut, run.clock, query, subset),
+                                              query);
+
+                    query = Query.singleClustering(schema, pd, cd1, false);
+                    QuiescentChecker.validate(schema,
+                                              run.tracker,
+                                              subset,
+                                              state.state.get(pd).apply(query),
+                                              SelectHelper.execute(sut, run.clock, query, subset),
+                                              query);
+
+                    for (boolean isGt : new boolean[]{ true, false })
+                    {
+                        for (boolean isEquals : new boolean[]{ true, false })
+                        {
+                            try
+                            {
+                                query = Query.clusteringSliceQuery(schema, pd, cd1, rng.nextLong(), isGt, isEquals, reverse);
+                            }
+                            catch (IllegalArgumentException impossibleQuery)
+                            {
+                                continue;
+                            }
+
+                            QuiescentChecker.validate(schema,
+                                                      run.tracker,
+                                                      subset,
+                                                      state.state.get(pd).apply(query),
+                                                      SelectHelper.execute(sut, run.clock, query, subset),
+                                                      query);
+                        }
+                    }
+
+                    for (boolean isMinEq : new boolean[]{ true, false })
+                    {
+                        for (boolean isMaxEq : new boolean[]{ true, false })
+                        {
+                            try
+                            {
+                                query = Query.clusteringRangeQuery(schema, pd, cd1, cd2, rng.nextLong(), isMinEq, isMaxEq, reverse);
+                            }
+                            catch (IllegalArgumentException impossibleQuery)
+                            {
+                                continue;
+                            }
+                            QuiescentChecker.validate(schema,
+                                                      run.tracker,
+                                                      subset,
+                                                      state.state.get(pd).apply(query),
+                                                      SelectHelper.execute(sut, run.clock, query, subset),
+                                                      query);
+                        }
+                    }
+                }
+            }
+        }
+    }
+
+    public static boolean isValidSubset(List<ColumnSpec<?>> superset, BitSet bitSet)
+    {
+        boolean hasRegular = false;
+        for (int i = 0; i < superset.size(); i++)
+        {
+            ColumnSpec<?> column = superset.get(i);
+            if (column.kind == ColumnSpec.Kind.PARTITION_KEY && !bitSet.isSet(i))
+                return false;
+            if (column.kind == ColumnSpec.Kind.CLUSTERING && !bitSet.isSet(i))
+                return false;
+            if (column.kind == ColumnSpec.Kind.REGULAR && bitSet.isSet(i))
+                hasRegular = true;
+        }
+
+        return hasRegular;
+    }
+
+    public static Set<ColumnSpec<?>> subset(List<ColumnSpec<?>> superset, BitSet bitSet)
+    {
+        Set<ColumnSpec<?>> subset = new HashSet<>();
+        for (int i = 0; i < superset.size(); i++)
+        {
+            if (bitSet.isSet(i))
+                subset.add(superset.get(i));
+        }
+
+        return subset;
+    }
+
+    public static Set<ColumnSpec<?>> randomSubset(List<ColumnSpec<?>> superset, Random e)
+    {
+        Set<ColumnSpec<?>> set = new HashSet<>();
+        boolean hadRegular = false;
+        for (ColumnSpec<?> v : superset)
+        {
+            // TODO: allow selecting without partition and clustering key, too
+            if (e.nextBoolean() || v.kind == ColumnSpec.Kind.CLUSTERING || v.kind == ColumnSpec.Kind.PARTITION_KEY)
+            {
+                set.add(v);
+                hadRegular |= v.kind == ColumnSpec.Kind.REGULAR;
+            }
+        }
+
+        // TODO: this is an oversimplification and a workaround for "Invalid restrictions on clustering columns since the UPDATE statement modifies only static columns"
+        if (!hadRegular)
+            return randomSubset(superset, e);
+
+        return set;
+    }
+
+    public static <T> BitSet subsetToBitset(List<T> superset, Set<T> subset)
+    {
+        BitSet bitSet = new BitSet.BitSet64Bit(superset.size());
+        for (int i = 0; i < superset.size(); i++)
+        {
+            if (subset.contains(superset.get(i)))
+                bitSet.set(i);
+        }
+        return bitSet;
+    }
+
+    public static class ModelState
+    {
+        public long lts = 0;
+        public final Map<Long, PartitionState> state;
+
+        public ModelState(Map<Long, PartitionState> state)
+        {
+            this.state = state;
+        }
+    }
+
+    public static class SyntheticTest // TODO: horrible name
+    {
+        private static long PD_STREAM = System.nanoTime();
+        private final OpSelectors.PureRng rng;
+        private final SchemaSpec schema;
+
+        public SyntheticTest(OpSelectors.PureRng rng, SchemaSpec schema)
+        {
+            this.schema = schema;
+            this.rng = rng;
+        }
+
+        public long pd(int pdIdx)
+        {
+            long pd = this.rng.randomNumber(pdIdx + 1, PD_STREAM);
+            long adjusted = schema.adjustPdEntropy(pd);
+            assert adjusted == pd : "Partition descriptors not utilising all entropy bits are not supported.";
+            return pd;
+        }
+
+        public long pdIdx(long pd)
+        {
+            return this.rng.sequenceNumber(pd, PD_STREAM) - 1;
+        }
+
+        public long cd(int pdIdx, int cdIdx)
+        {
+            long cd = this.rng.randomNumber(cdIdx + 1, pd(pdIdx));
+            long adjusted = schema.adjustCdEntropy(cd);
+            assert adjusted == cd : "Clustering descriptors not utilising all entropy bits are not supported.";
+            return cd;
+        }
+
+        public long cdIdx(long pd)
+        {
+            return this.rng.sequenceNumber(pd, PD_STREAM) - 1;
+        }
+    }
+}
diff --git a/test/distributed/org/apache/cassandra/fuzz/harry/integration/op/RowVisitorTest.java b/test/distributed/org/apache/cassandra/fuzz/harry/integration/op/RowVisitorTest.java
new file mode 100644
index 0000000000..5978f1e877
--- /dev/null
+++ b/test/distributed/org/apache/cassandra/fuzz/harry/integration/op/RowVisitorTest.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 org.apache.cassandra.fuzz.harry.integration.op;
+
+import java.util.function.Supplier;
+
+import org.junit.Before;
+import org.junit.Test;
+
+import org.apache.cassandra.harry.core.MetricReporter;
+import org.apache.cassandra.harry.core.Run;
+import org.apache.cassandra.harry.ddl.SchemaGenerators;
+import org.apache.cassandra.harry.ddl.SchemaSpec;
+import org.apache.cassandra.harry.gen.EntropySource;
+import org.apache.cassandra.harry.gen.distribution.Distribution;
+import org.apache.cassandra.harry.model.OpSelectors;
+import org.apache.cassandra.harry.clock.OffsetClock;
+import org.apache.cassandra.harry.sut.SystemUnderTest;
+import org.apache.cassandra.harry.tracker.DataTracker;
+import org.apache.cassandra.harry.visitors.GeneratingVisitor;
+import org.apache.cassandra.harry.visitors.MutatingRowVisitor;
+import org.apache.cassandra.harry.visitors.MutatingVisitor;
+import org.apache.cassandra.cql3.CQLTester;
+
+import static org.apache.cassandra.harry.model.OpSelectors.DefaultDescriptorSelector.DEFAULT_OP_SELECTOR;
+
+public class RowVisitorTest extends CQLTester
+{
+    @Before
+    public void beforeTest() throws Throwable {
+        super.beforeTest();
+        schemaChange(String.format("CREATE KEYSPACE IF NOT EXISTS %s WITH replication = {'class': 'SimpleStrategy', 'replication_factor': '1'}", SchemaGenerators.DEFAULT_KEYSPACE_NAME));
+    }
+
+    @Test
+    public void rowWriteGeneratorTest()
+    {
+        Supplier<SchemaSpec> specGenerator = SchemaGenerators.progression(SchemaGenerators.DEFAULT_SWITCH_AFTER);
+        EntropySource rand = EntropySource.forTests(6371747244598697093L);
+
+        OpSelectors.PureRng rng = new OpSelectors.PCGFast(1);
+
+        OpSelectors.PdSelector pdSelector = new OpSelectors.DefaultPdSelector(rng, 10, 10);
+
+        for (int i = 0; i < SchemaGenerators.DEFAULT_RUNS; i++)
+        {
+            SchemaSpec schema = specGenerator.get();
+            createTable(schema.compile().cql());
+
+            OpSelectors.DescriptorSelector descriptorSelector = new OpSelectors.DefaultDescriptorSelector(rng,
+                                                                                                          new OpSelectors.ColumnSelectorBuilder().forAll(schema)
+                                                                                                                                                 .build(),
+                                                                                                          DEFAULT_OP_SELECTOR,
+                                                                                                          new Distribution.ScaledDistribution(1, 30),
+                                                                                                          100);
+
+            Run run = new Run(rng,
+                              new OffsetClock(10000),
+                              pdSelector,
+                              descriptorSelector,
+                              schema,
+                              DataTracker.NO_OP,
+                              SystemUnderTest.NO_OP,
+                              MetricReporter.NO_OP);
+
+            long[] descriptors = new long[4];
+            for (int j = 0; j < descriptors.length; j++)
+                descriptors[j] = rand.next();
+
+            // some improvement wont hurt here
+            new GeneratingVisitor(run, new MutatingVisitor(run, MutatingRowVisitor::new)).visit();
+        }
+    }
+}
\ No newline at end of file
diff --git a/test/distributed/org/apache/cassandra/fuzz/harry/model/ApproximateClockTest.java b/test/distributed/org/apache/cassandra/fuzz/harry/model/ApproximateClockTest.java
new file mode 100644
index 0000000000..4751b4789c
--- /dev/null
+++ b/test/distributed/org/apache/cassandra/fuzz/harry/model/ApproximateClockTest.java
@@ -0,0 +1,180 @@
+/*
+ * 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 org.apache.cassandra.fuzz.harry.model;
+
+import java.util.Iterator;
+import java.util.Map;
+import java.util.concurrent.ConcurrentHashMap;
+import java.util.concurrent.ExecutorService;
+import java.util.concurrent.Executors;
+import java.util.concurrent.ScheduledExecutorService;
+import java.util.concurrent.TimeUnit;
+import java.util.concurrent.atomic.AtomicReference;
+import java.util.concurrent.locks.Condition;
+import java.util.concurrent.locks.Lock;
+import java.util.concurrent.locks.LockSupport;
+import java.util.concurrent.locks.ReentrantLock;
+
+import org.junit.Assert;
+import org.junit.Test;
+
+import org.apache.cassandra.harry.clock.ApproximateClock;
+
+public class ApproximateClockTest
+{
+    @Test
+    public void approximateClockTest() throws InterruptedException
+    {
+        ConcurrentHashMap<Long, Long> m = new ConcurrentHashMap<>();
+        ConcurrentHashMap<Long, Long> inverse = new ConcurrentHashMap<>();
+        TimeUnit timeUnit = TimeUnit.MILLISECONDS;
+        int duration = 1000;
+        int concurrency = 5;
+        long maxTicks = timeUnit.toMicros(duration)  / (4 * concurrency);
+
+        ApproximateClock clock = new ApproximateClock(duration, timeUnit);
+
+        ScheduledExecutorService scheduledExecutor = Executors.newScheduledThreadPool(1);
+        ExecutorService executor = Executors.newFixedThreadPool(concurrency);
+        final Lock lock = new ReentrantLock();
+        AtomicReference<Throwable> throwable = new AtomicReference();
+        final Condition signalError = lock.newCondition();
+        lock.lock();
+        for (int i = 0; i < concurrency; i++)
+        {
+            executor.submit(() -> {
+                try
+                {
+                    int sleepCnt = 0;
+
+                    while (!executor.isShutdown() && !Thread.currentThread().isInterrupted())
+                    {
+                        sleepCnt++;
+                        if (sleepCnt >= maxTicks)
+                        {
+                            LockSupport.parkNanos(timeUnit.toNanos(duration));
+                            sleepCnt = 0;
+                        }
+
+                        if (executor.isShutdown() || Thread.currentThread().isInterrupted())
+                            return;
+
+                        long lts = clock.nextLts();
+
+                        // Make sure to test "history" path
+                        if (lts % 10000 == 0)
+                        {
+                            scheduledExecutor.schedule(() -> {
+                                try
+                                {
+                                    long rts = clock.rts(lts);
+                                    Assert.assertNull(m.put(lts, rts));
+                                    Assert.assertNull(inverse.put(rts, lts));
+                                }
+                                catch (Throwable t)
+                                {
+                                    throwable.set(t);
+                                    signalError.signalAll();
+                                    t.printStackTrace();
+                                }
+                            }, 2 * duration, timeUnit);
+                            continue;
+                        }
+
+                        try
+                        {
+                            long rts = clock.rts(lts);
+                            Assert.assertNull(m.put(lts, rts));
+                            Assert.assertNull(inverse.put(rts, lts));
+                        }
+                        catch (Throwable t)
+                        {
+                            throwable.set(t);
+                            signalError.signalAll();
+                        }
+                    }
+                }
+                catch (Throwable t)
+                {
+                    throwable.set(t);
+                    signalError.signalAll();
+                    t.printStackTrace();
+                }
+            });
+        }
+        signalError.await(10, TimeUnit.SECONDS);
+        lock.unlock();
+        executor.shutdown();
+        Assert.assertTrue(executor.awaitTermination(30, TimeUnit.SECONDS));
+        scheduledExecutor.shutdown();
+        Assert.assertTrue(scheduledExecutor.awaitTermination(10, TimeUnit.SECONDS));
+        Throwable t = throwable.get();
+        if (t != null)
+            throw new AssertionError("Caught an exception while executing", t);
+
+        Assert.assertEquals(m.size(), inverse.size());
+        Iterator<Map.Entry<Long, Long>> iter = m.entrySet().iterator();
+
+        Map.Entry<Long, Long> previous = iter.next();
+        while (iter.hasNext())
+        {
+            if (previous == null)
+            {
+                previous = iter.next();
+                continue;
+            }
+
+            Map.Entry<Long, Long> current = iter.next();
+            long lts = current.getKey();
+            long rts = current.getValue();
+            Assert.assertEquals(String.format("%s and %s sort wrong", previous, current),
+                                Long.compare(previous.getKey(), current.getKey()),
+                                Long.compare(previous.getValue(), current.getValue()));
+
+            Assert.assertEquals(clock.rts(lts), rts);
+            Assert.assertEquals(clock.lts(rts), lts);
+            previous = current;
+        }
+    }
+
+    @Test
+    public void approximateClockInvertibilityTest()
+    {
+        ConcurrentHashMap<Long, Long> m = new ConcurrentHashMap<>();
+        TimeUnit timeUnit = TimeUnit.MILLISECONDS;
+        int duration = 100;
+        int cycles = 10_000;
+        ApproximateClock clock = new ApproximateClock(duration, timeUnit);
+
+        for (long i = 0; i < cycles; i++)
+        {
+            long lts = clock.nextLts();
+            Assert.assertEquals(lts, i);
+            long rts = clock.rts(lts);
+            Assert.assertNull(m.put(lts, rts));
+        }
+
+        for (Map.Entry<Long, Long> entry : m.entrySet())
+        {
+            Assert.assertEquals(entry.getKey(),
+                                Long.valueOf(clock.lts(entry.getValue())));
+        }
+    }
+
+}
diff --git a/test/distributed/org/apache/cassandra/fuzz/harry/model/OpSelectorsTest.java b/test/distributed/org/apache/cassandra/fuzz/harry/model/OpSelectorsTest.java
new file mode 100644
index 0000000000..e9984c1ef3
--- /dev/null
+++ b/test/distributed/org/apache/cassandra/fuzz/harry/model/OpSelectorsTest.java
@@ -0,0 +1,418 @@
+/*
+ * 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 org.apache.cassandra.fuzz.harry.model;
+
+import java.util.*;
+import java.util.function.BiConsumer;
+import java.util.function.Supplier;
+
+import org.junit.Assert;
+import org.junit.Test;
+
+import org.apache.cassandra.harry.core.MetricReporter;
+import org.apache.cassandra.harry.core.Run;
+import org.apache.cassandra.harry.ddl.ColumnSpec;
+import org.apache.cassandra.harry.ddl.SchemaGenerators;
+import org.apache.cassandra.harry.ddl.SchemaSpec;
+import org.apache.cassandra.harry.gen.Surjections;
+import org.apache.cassandra.harry.gen.distribution.Distribution;
+import org.apache.cassandra.harry.clock.OffsetClock;
+import org.apache.cassandra.harry.model.OpSelectors;
+import org.apache.cassandra.harry.sut.SystemUnderTest;
+import org.apache.cassandra.harry.operations.CompiledStatement;
+import org.apache.cassandra.harry.tracker.DataTracker;
+import org.apache.cassandra.harry.visitors.LtsVisitor;
+import org.apache.cassandra.harry.visitors.MutatingVisitor;
+import org.apache.cassandra.harry.visitors.OperationExecutor;
+import org.apache.cassandra.harry.util.BitSet;
+import org.apache.cassandra.harry.visitors.VisitExecutor;
+
+public class OpSelectorsTest
+{
+    private static int RUNS = 1000;
+    private static SchemaSpec SCHEMA = new SchemaSpec("ks", "tbl",
+                                                      Collections.singletonList(ColumnSpec.pk("pk", ColumnSpec.int64Type)),
+                                                      Collections.singletonList(ColumnSpec.pk("ck", ColumnSpec.int64Type)),
+                                                      Collections.emptyList(), Collections.emptyList());
+
+    @Test
+    public void testRowDataDescriptorSupplier()
+    {
+        OpSelectors.PureRng rng = new OpSelectors.PCGFast(1);
+        SchemaSpec schema = new SchemaSpec("ks", "tbl1",
+                                           Arrays.asList(ColumnSpec.pk("pk1", ColumnSpec.asciiType),
+                                                         ColumnSpec.pk("pk2", ColumnSpec.int64Type)),
+                                           Arrays.asList(ColumnSpec.ck("ck1", ColumnSpec.asciiType, false),
+                                                         ColumnSpec.ck("ck2", ColumnSpec.int64Type, false)),
+                                           Arrays.asList(ColumnSpec.regularColumn("v1", ColumnSpec.int32Type),
+                                                         ColumnSpec.regularColumn("v2", ColumnSpec.int64Type)),
+                                           Arrays.asList(ColumnSpec.staticColumn("static1", ColumnSpec.asciiType),
+                                                         ColumnSpec.staticColumn("static2", ColumnSpec.int64Type)));
+        OpSelectors.DefaultDescriptorSelector descriptorSelector = new OpSelectors.DefaultDescriptorSelector(rng,
+                                                                                                             new OpSelectors.ColumnSelectorBuilder().forAll(schema)
+                                                                                                                                                    .build(),
+                                                                                                             OpSelectors.DefaultDescriptorSelector.DEFAULT_OP_SELECTOR,
+                                                                                                             new Distribution.ScaledDistribution(2, 10),
+                                                                                                             50);
+
+        OpSelectors.PdSelector pdSupplier = new OpSelectors.DefaultPdSelector(rng,
+                                                                              100,
+                                                                              100);
+
+        for (int lts = 0; lts < RUNS; lts++)
+        {
+            long pd = pdSupplier.pd(lts, schema);
+
+            int opsPerLts = descriptorSelector.operationsPerLts(lts);
+            for (int opId = 0; opId < opsPerLts; opId++)
+            {
+                long cd = descriptorSelector.cd(pd, lts, opId, schema);
+                Assert.assertEquals(opId, descriptorSelector.opId(pd, lts, cd));
+                Assert.assertTrue(descriptorSelector.isCdVisitedBy(pd, lts, cd));
+                for (int col = 0; col < 10; col++)
+                {
+                    long vd = descriptorSelector.vd(pd, cd, lts, opId, col);
+                }
+            }
+        }
+    }
+
+    @Test
+    public void pdSelectorSymmetryTest()
+    {
+        OpSelectors.PureRng rng = new OpSelectors.PCGFast(1);
+        Supplier<SchemaSpec> gen = SchemaGenerators.progression(SchemaGenerators.DEFAULT_SWITCH_AFTER);
+        SchemaSpec schema = gen.get();
+
+        for (long[] positions : new long[][]{ { 0, Long.MAX_VALUE }, { 100, Long.MAX_VALUE }, { 1000, Long.MAX_VALUE } })
+        {
+            for (int repeats = 2; repeats <= 1000; repeats++)
+            {
+                for (int windowSize = 2; windowSize <= 10; windowSize++)
+                {
+                    OpSelectors.DefaultPdSelector pdSelector = new OpSelectors.DefaultPdSelector(rng, windowSize, repeats, positions[0], positions[1]);
+
+                    Map<Long, List<Long>> m = new HashMap<>();
+                    final long maxLts = 10_000;
+                    for (long lts = 0; lts <= maxLts; lts++)
+                    {
+                         long pd = pdSelector.pd(lts, schema);
+                         m.computeIfAbsent(pd, (k) -> new ArrayList<>()).add(lts);
+                    }
+
+                    for (Long pd : m.keySet())
+                    {
+                        long currentLts =  pdSelector.minLtsFor(pd);
+                        List<Long> predicted = new ArrayList<>();
+                        while (currentLts <= maxLts && currentLts >= 0)
+                        {
+                            predicted.add(currentLts);
+                            currentLts = pdSelector.nextLts(currentLts);
+                        }
+                        Assert.assertEquals(m.get(pd), predicted);
+                    }
+
+
+                }
+            }
+        }
+    }
+
+    @Test
+    public void pdSelectorTest()
+    {
+        OpSelectors.PureRng rng = new OpSelectors.PCGFast(1);
+        int cycles = 10000;
+        for (long[] positions : new long[][]{ { 0, Long.MAX_VALUE }, { 100, Long.MAX_VALUE }, { 1000, Long.MAX_VALUE } })
+        {
+            for (int repeats = 2; repeats <= 1000; repeats++)
+            {
+                for (int windowSize = 2; windowSize <= 10; windowSize++)
+                {
+                    OpSelectors.DefaultPdSelector pdSupplier = new OpSelectors.DefaultPdSelector(rng, windowSize, repeats, positions[0], positions[1]);
+                    long[] pds = new long[cycles];
+                    for (int i = 0; i < cycles; i++)
+                    {
+                        long pd = pdSupplier.pd(i, SCHEMA);
+                        pds[i] = pd;
+                        Assert.assertEquals(pdSupplier.positionFor(i), pdSupplier.positionForPd(pd));
+                    }
+
+                    Set<Long> noNext = new HashSet<>();
+                    for (int i = 0; i < cycles; i++)
+                    {
+                        long nextLts = pdSupplier.nextLts(i);
+                        Assert.assertFalse(noNext.contains(pds[i]));
+                        if (nextLts == -1)
+                        {
+                            noNext.add(nextLts);
+                        }
+                        else if (nextLts < cycles)
+                        {
+                            Assert.assertEquals(pds[(int) nextLts], pdSupplier.pd(i, SCHEMA));
+                        }
+                    }
+
+                    Set<Long> noPrev = new HashSet<>();
+                    for (int i = cycles - 1; i >= 0; i--)
+                    {
+                        long prevLts = pdSupplier.prevLts(i);
+                        Assert.assertFalse(noPrev.contains(pds[i]));
+                        if (prevLts == -1)
+                        {
+                            noPrev.add(prevLts);
+                        }
+                        else if (prevLts >= 0)
+                        {
+                            Assert.assertEquals(pds[(int) prevLts], pdSupplier.pd(i, SCHEMA));
+                        }
+                    }
+
+                    Set<Long> seen = new HashSet<>();
+                    for (int i = 0; i < cycles; i++)
+                    {
+                        long pd = pdSupplier.pd(i, SCHEMA);
+                        if (!seen.contains(pd))
+                        {
+                            Assert.assertEquals(i, pdSupplier.minLtsAt(pdSupplier.positionFor(i)));
+                            seen.add(pd);
+                        }
+                    }
+
+                    for (int i = 0; i < cycles; i++)
+                    {
+                        long pd = pdSupplier.pd(i, SCHEMA);
+                        long maxLts = pdSupplier.maxLtsFor(pd);
+                        Assert.assertEquals(-1, pdSupplier.nextLts(maxLts));
+                        Assert.assertEquals(pdSupplier.pd(i, SCHEMA), pdSupplier.pd(maxLts, SCHEMA));
+                    }
+                }
+            }
+        }
+    }
+
+    @Test
+    public void ckSelectorTest()
+    {
+        Supplier<SchemaSpec> gen = SchemaGenerators.progression(SchemaGenerators.DEFAULT_SWITCH_AFTER);
+        for (int i = 0; i < SchemaGenerators.DEFAULT_RUNS; i++)
+            ckSelectorTest(gen.get());
+    }
+
+    public void ckSelectorTest(SchemaSpec schema)
+    {
+        OpSelectors.PureRng rng = new OpSelectors.PCGFast(1);
+        OpSelectors.PdSelector pdSelector = new OpSelectors.DefaultPdSelector(rng, 10, 10);
+        OpSelectors.DescriptorSelector ckSelector = new OpSelectors.DefaultDescriptorSelector(rng,
+                                                                                              new OpSelectors.ColumnSelectorBuilder().forAll(schema, Surjections.pick(schema.regularColumnsMask())).build(),
+                                                                                              OpSelectors.OperationSelector.weighted(Surjections.weights(10, 10, 40, 40),
+                                                                                                                                     OpSelectors.OperationKind.DELETE_ROW,
+                                                                                                                                     OpSelectors.OperationKind.DELETE_COLUMN,
+                                                                                                                                     OpSelectors.OperationKind.INSERT,
+                                                                                                                                     OpSelectors.OperationKind.UPDATE),
+                                                                                              new Distribution.ConstantDistribution(10),
+                                                                                              10);
+
+        Map<Long, Set<Long>> partitionMap = new HashMap<>();
+        CompiledStatement compiledStatement = new CompiledStatement("");
+        BiConsumer<Long, Long> consumer = (pd, cd) -> {
+            partitionMap.compute(pd, (pk, list) -> {
+                if (list == null)
+                    list = new HashSet<>();
+                list.add(cd);
+                return list;
+            });
+        };
+
+        Run run = new Run(rng,
+                          new OffsetClock(0),
+                          pdSelector,
+                          ckSelector,
+                          schema,
+                          DataTracker.NO_OP,
+                          SystemUnderTest.NO_OP,
+                          MetricReporter.NO_OP);
+
+        LtsVisitor visitor = new MutatingVisitor(run,
+                                                 (r) -> new OperationExecutor()
+                                                        {
+                                                            public CompiledStatement insert(VisitExecutor.WriteOp op)
+                                                            {
+                                                                consumer.accept(op.pd(), op.cd());
+                                                                return compiledStatement;
+                                                            }
+
+                                                            public CompiledStatement update(VisitExecutor.WriteOp op)
+                                                            {
+                                                                consumer.accept(op.pd(), op.cd());
+                                                                return compiledStatement;
+                                                            }
+
+                                                            public CompiledStatement insertWithStatics(VisitExecutor.WriteStaticOp op)
+                                                            {
+                                                                consumer.accept(op.pd(), op.cd());
+                                                                return compiledStatement;
+                                                            }
+
+                                                            public CompiledStatement updateWithStatics(VisitExecutor.WriteStaticOp op)
+                                                            {
+                                                                consumer.accept(op.pd(), op.cd());
+                                                                return compiledStatement;
+                                                            }
+
+                                                            public CompiledStatement deleteColumn(VisitExecutor.DeleteColumnsOp op)
+                                                            {
+                                                                consumer.accept(op.pd(), op.cd());
+                                                                return compiledStatement;
+                                                            }
+
+                                                            public CompiledStatement deleteColumnWithStatics(VisitExecutor.DeleteColumnsOp op)
+                                                            {
+                                                                consumer.accept(op.pd(), op.cd());
+                                                                return compiledStatement;
+                                                            }
+
+                                                            public CompiledStatement deleteRow(VisitExecutor.DeleteRowOp op)
+                                                            {
+                                                                consumer.accept(op.pd(), op.cd());
+                                                                return compiledStatement;
+                                                            }
+
+                                                            public CompiledStatement deletePartition(VisitExecutor.DeleteOp op)
+                                                            {
+                                                                // ignore
+                                                                return compiledStatement;
+                                                            }
+
+                                                            public CompiledStatement deleteRange(VisitExecutor.DeleteOp op)
+                                                            {
+                                                                // ignore
+                                                                return compiledStatement;
+                                                            }
+
+                                                            public CompiledStatement deleteSlice(VisitExecutor.DeleteOp op)
+                                                            {
+                                                                // ignore
+                                                                return compiledStatement;
+                                                            }
+                                                        });
+
+        for (int lts = 0; lts < 1000; lts++)
+            visitor.visit();
+
+        for (Collection<Long> value : partitionMap.values())
+            Assert.assertEquals(10, value.size());
+    }
+
+    @Test
+    public void hierarchicalDescriptorSelector()
+    {
+        SchemaSpec schema = new SchemaSpec("ks", "tbl1",
+                                           Collections.singletonList(ColumnSpec.pk("pk1", ColumnSpec.asciiType)),
+                                           Arrays.asList(ColumnSpec.ck("ck1", ColumnSpec.asciiType),
+                                                         ColumnSpec.ck("ck2", ColumnSpec.asciiType),
+                                                         ColumnSpec.ck("ck3", ColumnSpec.asciiType)),
+                                           Collections.singletonList(ColumnSpec.regularColumn("v1", ColumnSpec.asciiType)),
+                                           Collections.emptyList());
+
+        OpSelectors.PureRng rng = new OpSelectors.PCGFast(1);
+        OpSelectors.DescriptorSelector ckSelector = new OpSelectors.HierarchicalDescriptorSelector(rng,
+                                                                                                   new int[] {10, 20},
+                                                                                                   OpSelectors.columnSelectorBuilder().forAll(schema, Surjections.pick(BitSet.allUnset(0))).build(),
+                                                                                                   OpSelectors.OperationSelector.weighted(Surjections.weights(10, 10, 40, 40),
+                                                                                                                                          OpSelectors.OperationKind.DELETE_ROW,
+                                                                                                                                          OpSelectors.OperationKind.DELETE_COLUMN,
+                                                                                                                                          OpSelectors.OperationKind.INSERT,
+                                                                                                                                          OpSelectors.OperationKind.UPDATE),
+                                                                                                   new Distribution.ConstantDistribution(10),
+                                                                                                   100);
+
+        Set<Long> ck1 = new TreeSet<>();
+        Set<Long> ck2 = new TreeSet<>();
+        Set<Long> ck3 = new TreeSet<>();
+        for (int i = 0; i < 1000; i++)
+        {
+            long[] part = schema.ckGenerator.slice(ckSelector.cd(0, i, 0, schema));
+            ck1.add(part[0]);
+            ck2.add(part[1]);
+            ck3.add(part[2]);
+        }
+        Assert.assertEquals(10, ck1.size());
+        Assert.assertEquals(20, ck2.size());
+        Assert.assertEquals(100, ck3.size());
+    }
+
+    @Test
+    public void testWeights()
+    {
+        Map<OpSelectors.OperationKind, Integer> config = new EnumMap<>(OpSelectors.OperationKind.class);
+        config.put(OpSelectors.OperationKind.DELETE_RANGE, 1);
+        config.put(OpSelectors.OperationKind.DELETE_SLICE, 1);
+        config.put(OpSelectors.OperationKind.DELETE_ROW, 1);
+        config.put(OpSelectors.OperationKind.DELETE_COLUMN, 1);
+        config.put(OpSelectors.OperationKind.DELETE_PARTITION, 1);
+        config.put(OpSelectors.OperationKind.DELETE_COLUMN_WITH_STATICS, 1);
+        config.put(OpSelectors.OperationKind.UPDATE, 500);
+        config.put(OpSelectors.OperationKind.INSERT, 500);
+        config.put(OpSelectors.OperationKind.UPDATE_WITH_STATICS, 500);
+        config.put(OpSelectors.OperationKind.INSERT_WITH_STATICS, 500);
+
+        int[] weights = new int[config.size()];
+        for (int i = 0; i < config.values().size(); i++)
+            weights[i] = config.get(OpSelectors.OperationKind.values()[i]);
+        OpSelectors.OperationSelector selector = OpSelectors.OperationSelector.weighted(Surjections.weights(weights),
+        OpSelectors.OperationKind.values());
+
+        OpSelectors.PureRng rng = new OpSelectors.PCGFast(1);
+        OpSelectors.PdSelector pdSelector = new OpSelectors.DefaultPdSelector(rng, 10, 10);
+        OpSelectors.DescriptorSelector descriptorSelector = new OpSelectors.DefaultDescriptorSelector(rng,
+                                                                                                      null,
+                                                                                                      selector,
+                                                                                                      new Distribution.ConstantDistribution(10),
+                                                                                                      100);
+
+        EnumMap<OpSelectors.OperationKind, Integer> m = new EnumMap<OpSelectors.OperationKind, Integer>(OpSelectors.OperationKind.class);
+        for (int lts = 0; lts < 1000000; lts++)
+        {
+            int total = descriptorSelector.operationsPerLts(lts);
+            long pd = pdSelector.pd(lts, SCHEMA);
+            for (int opId = 0; opId < total; opId++)
+            {
+                m.compute(descriptorSelector.operationType(pd, lts, opId),
+                          (OpSelectors.OperationKind k, Integer old) -> {
+                              if (old == null) return 1;
+                              else return old + 1;
+                          });
+            }
+        }
+
+        for (OpSelectors.OperationKind l : OpSelectors.OperationKind.values())
+        {
+            for (OpSelectors.OperationKind r : OpSelectors.OperationKind.values())
+            {
+                if (l != r)
+                {
+                    Assert.assertEquals(m.get(l) * 1.0 / m.get(r),
+                                        config.get(l) * 1.0 / config.get(r),
+                                        (config.get(l) * 1.0 / config.get(r)) * 0.10);
+                }
+            }
+        }
+    }
+}
\ No newline at end of file
diff --git a/test/distributed/org/apache/cassandra/fuzz/harry/operations/RelationTest.java b/test/distributed/org/apache/cassandra/fuzz/harry/operations/RelationTest.java
new file mode 100644
index 0000000000..975a87a13e
--- /dev/null
+++ b/test/distributed/org/apache/cassandra/fuzz/harry/operations/RelationTest.java
@@ -0,0 +1,204 @@
+/*
+ * 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 org.apache.cassandra.fuzz.harry.operations;
+
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.Iterator;
+import java.util.List;
+
+import org.junit.Assert;
+import org.junit.Test;
+
+import org.apache.cassandra.harry.ddl.ColumnSpec;
+import org.apache.cassandra.harry.ddl.SchemaSpec;
+import org.apache.cassandra.fuzz.harry.gen.DataGeneratorsTest;
+import org.apache.cassandra.harry.model.OpSelectors;
+import org.apache.cassandra.harry.operations.Query;
+import org.apache.cassandra.harry.operations.QueryGenerator;
+import org.apache.cassandra.harry.util.BitSet;
+
+public class RelationTest
+{
+    private static int RUNS = 50;
+
+    @Test
+    public void testKeyGenerators()
+    {
+        for (int size = 1; size < 5; size++)
+        {
+            Iterator<ColumnSpec.DataType[]> iter = DataGeneratorsTest.permutations(size,
+                                                                                   ColumnSpec.DataType.class,
+                                                                                   ColumnSpec.int8Type,
+                                                                                   ColumnSpec.asciiType,
+                                                                                   ColumnSpec.int16Type,
+                                                                                   ColumnSpec.int32Type,
+                                                                                   ColumnSpec.int64Type,
+                                                                                   ColumnSpec.floatType,
+                                                                                   ColumnSpec.doubleType
+            );
+            while (iter.hasNext())
+            {
+                ColumnSpec.DataType[] types = iter.next();
+                List<ColumnSpec<?>> spec = new ArrayList<>(types.length);
+                for (int i = 0; i < types.length; i++)
+                    spec.add(ColumnSpec.ck("r" + i, types[i], false));
+
+                SchemaSpec schemaSpec = new SchemaSpec("ks",
+                                                       "tbl",
+                                                       Collections.singletonList(ColumnSpec.pk("pk", ColumnSpec.int64Type)),
+                                                       spec,
+                                                       Collections.emptyList(),
+                                                       Collections.emptyList());
+
+                long[] cds = new long[RUNS];
+
+                int[] fractions = new int[schemaSpec.clusteringKeys.size()];
+                int last = cds.length;
+                for (int i = fractions.length - 1; i >= 0; i--)
+                {
+                    fractions[i] = last;
+                    last = last / 2;
+                }
+
+                for (int i = 0; i < cds.length; i++)
+                {
+                    long cd = OpSelectors.HierarchicalDescriptorSelector.cd(i, fractions, schemaSpec, new OpSelectors.PCGFast(1L), 1L);
+                    cds[i] = schemaSpec.adjustPdEntropy(cd);
+                }
+                Arrays.sort(cds);
+
+                OpSelectors.PureRng rng = new OpSelectors.PCGFast(1L);
+
+                // TODO: replace with mocks?
+                QueryGenerator querySelector = new QueryGenerator(schemaSpec,
+                                                                  new OpSelectors.PdSelector()
+                                                                  {
+                                                                      protected long pd(long lts)
+                                                                      {
+                                                                          return lts;
+                                                                      }
+
+                                                                      public long nextLts(long lts)
+                                                                      {
+                                                                          throw new RuntimeException("not implemented");
+                                                                      }
+
+                                                                      public long prevLts(long lts)
+                                                                      {
+                                                                          throw new RuntimeException("not implemented");
+                                                                      }
+
+                                                                      public long maxLtsFor(long pd)
+                                                                      {
+                                                                          throw new RuntimeException("not implemented");
+                                                                      }
+
+                                                                      public long minLtsAt(long position)
+                                                                      {
+                                                                          throw new RuntimeException("not implemented");
+                                                                      }
+
+
+                                                                      public long minLtsFor(long pd)
+                                                                      {
+                                                                          throw new RuntimeException("not implemented");
+                                                                      }
+
+                                                                      public long maxPosition(long maxLts)
+                                                                      {
+                                                                          throw new RuntimeException("not implemented");
+                                                                      }
+                                                                  },
+                                                                  new OpSelectors.DescriptorSelector()
+                                                                  {
+                                                                      public int operationsPerLts(long lts)
+                                                                      {
+                                                                          throw new RuntimeException("not implemented");
+                                                                      }
+
+                                                                      public int maxPartitionSize()
+                                                                      {
+                                                                          throw new RuntimeException("not implemented");
+                                                                      }
+
+                                                                      public boolean isCdVisitedBy(long pd, long lts, long cd)
+                                                                      {
+                                                                          throw new RuntimeException("not implemented");
+                                                                      }
+
+                                                                      protected long cd(long pd, long lts, long opId)
+                                                                      {
+                                                                          throw new RuntimeException("not implemented");
+                                                                      }
+
+                                                                      public long randomCd(long pd, long entropy)
+                                                                      {
+                                                                          return Math.abs(rng.prev(entropy)) % cds.length;
+                                                                      }
+
+                                                                      protected long vd(long pd, long cd, long lts, long opId, int col)
+                                                                      {
+                                                                          throw new RuntimeException("not implemented");
+                                                                      }
+
+                                                                      public OpSelectors.OperationKind operationType(long pd, long lts, long opId)
+                                                                      {
+                                                                          throw new RuntimeException("not implemented");
+                                                                      }
+
+                                                                      public BitSet columnMask(long pd, long lts, long opId, OpSelectors.OperationKind opType)
+                                                                      {
+                                                                          throw new RuntimeException("not implemented");
+                                                                      }
+
+                                                                      public long opId(long pd, long lts, long cd)
+                                                                      {
+                                                                          return 0;
+                                                                      }
+                                                                  },
+                                                                  rng);
+
+                QueryGenerator.TypedQueryGenerator gen = new QueryGenerator.TypedQueryGenerator(rng, querySelector);
+
+                try
+                {
+                    for (int i = 0; i < RUNS; i++)
+                    {
+                        Query query = gen.inflate(i, 0);
+                        for (int j = 0; j < cds.length; j++)
+                        {
+                            long cd = schemaSpec.ckGenerator.adjustEntropyDomain(cds[i]);
+                            // the only thing we care about here is that query
+                            Assert.assertEquals(String.format("Error caught while running a query %s with cd %d",
+                                                              query, cd),
+                                                Query.simpleMatch(query, cd),
+                                                query.matchCd(cd));
+                        }
+                    }
+                }
+                catch (Throwable t)
+                {
+                    throw new AssertionError("Caught error for the type combination " + Arrays.toString(types), t);
+                }
+            }
+        }
+    }
+}
diff --git a/test/distributed/org/apache/cassandra/fuzz/harry/runner/LockingDataTrackerTest.java b/test/distributed/org/apache/cassandra/fuzz/harry/runner/LockingDataTrackerTest.java
new file mode 100644
index 0000000000..94b9f2e131
--- /dev/null
+++ b/test/distributed/org/apache/cassandra/fuzz/harry/runner/LockingDataTrackerTest.java
@@ -0,0 +1,133 @@
+/*
+ * 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 org.apache.cassandra.fuzz.harry.runner;
+
+import java.util.List;
+import java.util.concurrent.CopyOnWriteArrayList;
+import java.util.concurrent.TimeUnit;
+import java.util.concurrent.atomic.AtomicInteger;
+import java.util.concurrent.atomic.AtomicReference;
+
+import org.junit.Assert;
+import org.junit.Test;
+
+import org.apache.cassandra.concurrent.ExecutorFactory;
+import org.apache.cassandra.concurrent.Interruptible;
+import org.apache.cassandra.harry.ddl.SchemaGenerators;
+import org.apache.cassandra.harry.ddl.SchemaSpec;
+import org.apache.cassandra.harry.model.OpSelectors;
+import org.apache.cassandra.harry.runner.Runner;
+import org.apache.cassandra.harry.tracker.LockingDataTracker;
+import org.apache.cassandra.utils.concurrent.WaitQueue;
+
+import static org.apache.cassandra.concurrent.InfiniteLoopExecutor.Daemon.NON_DAEMON;
+import static org.apache.cassandra.concurrent.InfiniteLoopExecutor.Interrupts.UNSYNCHRONIZED;
+import static org.apache.cassandra.concurrent.InfiniteLoopExecutor.SimulatorSafe.SAFE;
+
+public class LockingDataTrackerTest
+{
+    @Test
+    public void testDataTracker() throws Throwable
+    {
+        SchemaSpec schemaSpec = SchemaGenerators.defaultSchemaSpecGen("test").inflate(1L);
+        OpSelectors.PureRng rng = new OpSelectors.PCGFast(1L);
+        OpSelectors.PdSelector pdSelector = new OpSelectors.DefaultPdSelector(rng, 5, 2);
+        LockingDataTracker tracker = new LockingDataTracker(pdSelector, schemaSpec);
+
+        AtomicReference<State> excluded = new AtomicReference<>(State.UNLOCKED);
+        AtomicInteger readers = new AtomicInteger(0);
+        AtomicInteger writers = new AtomicInteger(0);
+
+        WaitQueue queue = WaitQueue.newWaitQueue();
+        WaitQueue.Signal interrupt = queue.register();
+        List<Throwable> errors = new CopyOnWriteArrayList<>();
+
+        long lts = 1;
+        long pd = pdSelector.pd(lts, schemaSpec);
+        int parallelism = 2;
+        for (int i = 0; i < parallelism; i++)
+        {
+            ExecutorFactory.Global.executorFactory().infiniteLoop("write-" + i, Runner.wrapInterrupt(state -> {
+                try
+                {
+                    if (state == Interruptible.State.NORMAL)
+                    {
+                        tracker.beginModification(lts);
+                        Assert.assertEquals(0, readers.get());
+                        writers.incrementAndGet();
+                        excluded.updateAndGet((prev) -> {
+                            assert (prev == State.UNLOCKED || prev == State.LOCKED_FOR_WRITE) : prev;
+                            return State.LOCKED_FOR_WRITE;
+                        });
+                        Assert.assertEquals(0, readers.get());
+                        excluded.updateAndGet((prev) -> {
+                            assert (prev == State.UNLOCKED || prev == State.LOCKED_FOR_WRITE) : prev;
+                            return State.UNLOCKED;
+                        });
+                        Assert.assertEquals(0, readers.get());
+                        writers.decrementAndGet();
+                        tracker.endModification(lts);
+                    }
+                }
+                catch (Throwable t)
+                {
+                    t.printStackTrace();
+                    throw t;
+                }
+            }, interrupt::signal, errors::add), SAFE, NON_DAEMON, UNSYNCHRONIZED);
+        }
+
+        for (int i = 0; i < parallelism; i++)
+        {
+            ExecutorFactory.Global.executorFactory().infiniteLoop("read-" + i, Runner.wrapInterrupt(state -> {
+                try
+                {
+                    if (state == Interruptible.State.NORMAL)
+                    {
+                        tracker.beginValidation(pd);
+                        Assert.assertEquals(0, writers.get());
+                        readers.incrementAndGet();
+                        excluded.updateAndGet((prev) -> {
+                            assert (prev == State.UNLOCKED || prev == State.LOCKED_FOR_READ) : prev;
+                            return State.LOCKED_FOR_READ;
+                        });
+                        Assert.assertEquals(0, writers.get());
+                        excluded.updateAndGet((prev) -> {
+                            assert (prev == State.UNLOCKED || prev == State.LOCKED_FOR_READ) : prev;
+                            return State.UNLOCKED;
+                        });
+                        Assert.assertEquals(0, writers.get());
+                        readers.decrementAndGet();
+                        tracker.endValidation(pd);
+                    }
+                }
+                catch (Throwable t)
+                {
+                    t.printStackTrace();
+                    throw t;
+                }
+            }, interrupt::signal, errors::add), SAFE, NON_DAEMON, UNSYNCHRONIZED);
+        }
+
+        interrupt.await(1, TimeUnit.MINUTES);
+        if (!errors.isEmpty())
+            Runner.mergeAndThrow(errors);
+    }
+    enum State { UNLOCKED, LOCKED_FOR_READ, LOCKED_FOR_WRITE }
+}
diff --git a/test/distributed/org/apache/cassandra/fuzz/harry/util/BitSetTest.java b/test/distributed/org/apache/cassandra/fuzz/harry/util/BitSetTest.java
new file mode 100644
index 0000000000..ee992a36dc
--- /dev/null
+++ b/test/distributed/org/apache/cassandra/fuzz/harry/util/BitSetTest.java
@@ -0,0 +1,66 @@
+/*
+ * 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 org.apache.cassandra.fuzz.harry.util;
+
+import org.junit.Assert;
+import org.junit.Test;
+
+import org.apache.cassandra.harry.util.BitSet;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertTrue;
+
+// TODO: these are not real tests
+public class BitSetTest
+{
+    @Test
+    public void testBitMask()
+    {
+        Assert.assertEquals(0b00000000_00000000_00000000_00000000_00000000_00000000_00000000_00000000L, BitSet.bitMask(-1));
+        assertEquals(0b00000000_00000000_00000000_00000000_00000000_00000000_00000000_00000000L, BitSet.bitMask(0));
+        assertEquals(0b00000000_00000000_00000000_00000000_00000000_00000000_00000000_00000001L, BitSet.bitMask(1));
+        assertEquals(0b00000000_00000000_00000000_00000000_00000000_00000000_00000000_00000011L, BitSet.bitMask(2));
+        assertEquals(0b00000000_00000000_00000000_00000000_00000000_00000000_00000000_00001111L, BitSet.bitMask(4));
+        assertEquals(0b00000000_00000000_00000000_00000000_00000000_00000000_00000000_11111111L, BitSet.bitMask(8));
+        assertEquals(0b00000000_00000000_00000000_00000000_00000000_00000000_11111111_11111111L, BitSet.bitMask(16));
+        assertEquals(0b00000000_00000000_00000000_00000000_11111111_11111111_11111111_11111111L, BitSet.bitMask(32));
+        assertEquals(0b11111111_11111111_11111111_11111111_11111111_11111111_11111111_11111111L, BitSet.bitMask(64));
+        assertEquals(0b11111111_11111111_11111111_11111111_11111111_11111111_11111111_11111111L, BitSet.bitMask(65));
+    }
+
+    @Test
+    public void testSetBits()
+    {
+        BitSet bs = BitSet.allUnset(32);
+        bs.set(10);
+        bs.set(13);
+        bs.set(15);
+        assertEquals(3, bs.setCount());
+    }
+
+    @Test
+    public void testEachSetBit()
+    {
+        BitSet bs = BitSet.allUnset(32);
+        bs.set(10);
+        bs.set(13);
+        bs.set(15);
+        bs.eachSetBit(i -> assertTrue(i == 10 || i == 13 || i == 15));
+    }
+}
diff --git a/test/distributed/org/apache/cassandra/fuzz/harry/util/RangesTest.java b/test/distributed/org/apache/cassandra/fuzz/harry/util/RangesTest.java
new file mode 100644
index 0000000000..38231ee249
--- /dev/null
+++ b/test/distributed/org/apache/cassandra/fuzz/harry/util/RangesTest.java
@@ -0,0 +1,102 @@
+/*
+ * 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 org.apache.cassandra.fuzz.harry.util;
+
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.List;
+import java.util.Random;
+
+import org.junit.Assert;
+import org.junit.Test;
+
+import org.apache.cassandra.harry.util.DescriptorRanges;
+
+public class RangesTest
+{
+
+    @Test
+    public void simpleRangesTest()
+    {
+        List<DescriptorRanges.DescriptorRange> list = Arrays.asList(inclusiveRange(10, 20, 10),
+                                                                    inclusiveRange(40, 50, 10),
+                                                                    inclusiveRange(60, 70, 10),
+                                                                    inclusiveRange(80, 90, 10));
+        Collections.shuffle(list);
+        DescriptorRanges ranges = new DescriptorRanges(list);
+
+        Assert.assertTrue(ranges.isShadowed(10, 5));
+        Assert.assertFalse(ranges.isShadowed(10, 20));
+        Assert.assertFalse(ranges.isShadowed(15, 20));
+        Assert.assertTrue(ranges.isShadowed(49, 5));
+        Assert.assertFalse(ranges.isShadowed(55, 5));
+        Assert.assertFalse(ranges.isShadowed(50, 20));
+        Assert.assertTrue(ranges.isShadowed(90, 9));
+    }
+
+    @Test
+    public void randomizedRangesTest()
+    {
+        for (int i = 0; i < 1000; i++)
+            _randomizedRangesTest();
+    }
+
+    private void _randomizedRangesTest()
+    {
+        List<DescriptorRanges.DescriptorRange> rangesList = new ArrayList<>();
+        Random rnd = new Random();
+        for (int i = 0; i < 100; i++)
+        {
+            long a = rnd.nextInt(1000);
+            long b = rnd.nextInt(1000);
+            DescriptorRanges.DescriptorRange range = new DescriptorRanges.DescriptorRange(Math.min(a, b),
+                                                                                          Math.max(a,b),
+                                                                                          rnd.nextBoolean(),
+                                                                                          rnd.nextBoolean(),
+                                                                                          rnd.nextInt(1000));
+            rangesList.add(range);
+        }
+        DescriptorRanges ranges = new DescriptorRanges(rangesList);
+
+        for (int i = 0; i < 10000; i++)
+        {
+            long descriptor = rnd.nextLong();
+            long ts = rnd.nextInt(1000);
+            Assert.assertEquals(matchLinear(rangesList, descriptor, ts),
+                                ranges.isShadowed(descriptor, ts));
+
+        }
+    }
+
+    public boolean matchLinear(List<DescriptorRanges.DescriptorRange> ranges, long descriptor, long ts)
+    {
+        for (DescriptorRanges.DescriptorRange range : ranges)
+        {
+            if (range.contains(descriptor, ts))
+                return true;
+        }
+        return false;
+    }
+
+    public DescriptorRanges.DescriptorRange inclusiveRange(long start, long end, long ts)
+    {
+        return new DescriptorRanges.DescriptorRange(start, end, true, true, 10);
+    }
+}
diff --git a/test/distributed/org/apache/cassandra/distributed/upgrade/ClusterMetadataUpgradeHarryTest.java b/test/distributed/org/apache/cassandra/fuzz/ring/ClusterMetadataUpgradeHarryTest.java
similarity index 74%
rename from test/distributed/org/apache/cassandra/distributed/upgrade/ClusterMetadataUpgradeHarryTest.java
rename to test/distributed/org/apache/cassandra/fuzz/ring/ClusterMetadataUpgradeHarryTest.java
index 776e7b1073..1d8a5db820 100644
--- a/test/distributed/org/apache/cassandra/distributed/upgrade/ClusterMetadataUpgradeHarryTest.java
+++ b/test/distributed/org/apache/cassandra/fuzz/ring/ClusterMetadataUpgradeHarryTest.java
@@ -16,7 +16,7 @@
  * limitations under the License.
  */
 
-package org.apache.cassandra.distributed.upgrade;
+package org.apache.cassandra.fuzz.ring;
 
 import java.util.Collections;
 import java.util.concurrent.ExecutorService;
@@ -26,32 +26,37 @@ import java.util.concurrent.atomic.AtomicInteger;
 import java.util.concurrent.atomic.AtomicReference;
 
 import com.google.common.util.concurrent.Uninterruptibles;
+
+import org.apache.cassandra.distributed.upgrade.UpgradeTestBase;
+import org.apache.cassandra.harry.HarryHelper;
+import org.apache.cassandra.harry.core.Configuration;
+import org.apache.cassandra.harry.ddl.SchemaSpec;
+
+import org.apache.cassandra.harry.runner.FlaggedRunner;
+import org.apache.cassandra.harry.sut.injvm.ClusterState;
+import org.apache.cassandra.harry.sut.injvm.ExistingClusterSUT;
+
+import org.apache.cassandra.harry.visitors.MutatingVisitor;
+import org.apache.cassandra.harry.visitors.QueryLogger;
+import org.apache.cassandra.harry.visitors.RandomPartitionValidator;
+
 import org.junit.Test;
 
-import harry.core.Configuration;
-import harry.ddl.SchemaSpec;
-import harry.visitors.MutatingVisitor;
-import harry.visitors.QueryLogger;
-import harry.visitors.RandomPartitionValidator;
 import org.apache.cassandra.distributed.Constants;
 import org.apache.cassandra.distributed.api.Feature;
-import org.apache.cassandra.distributed.fuzz.FixedSchemaProviderConfiguration;
-import org.apache.cassandra.distributed.fuzz.HarryHelper;
-import org.apache.cassandra.distributed.harry.ClusterState;
-import org.apache.cassandra.distributed.harry.ExistingClusterSUT;
-import org.apache.cassandra.distributed.harry.FlaggedRunner;
 import org.apache.cassandra.schema.SchemaConstants;
 import org.apache.cassandra.utils.concurrent.CountDownLatch;
 
-import static harry.core.Configuration.VisitorPoolConfiguration.pool;
-import static harry.ddl.ColumnSpec.asciiType;
-import static harry.ddl.ColumnSpec.ck;
-import static harry.ddl.ColumnSpec.int64Type;
-import static harry.ddl.ColumnSpec.pk;
-import static harry.ddl.ColumnSpec.regularColumn;
-import static harry.ddl.ColumnSpec.staticColumn;
+
 import static java.util.Arrays.asList;
 import static org.apache.cassandra.concurrent.ExecutorFactory.Global.executorFactory;
+import static org.apache.cassandra.harry.core.Configuration.VisitorPoolConfiguration.pool;
+import static org.apache.cassandra.harry.ddl.ColumnSpec.asciiType;
+import static org.apache.cassandra.harry.ddl.ColumnSpec.int64Type;
+import static org.apache.cassandra.harry.ddl.ColumnSpec.ck;
+import static org.apache.cassandra.harry.ddl.ColumnSpec.pk;
+import static org.apache.cassandra.harry.ddl.ColumnSpec.regularColumn;
+import static org.apache.cassandra.harry.ddl.ColumnSpec.staticColumn;
 import static org.apache.cassandra.tcm.log.SystemKeyspaceStorage.NAME;
 import static org.junit.Assert.assertEquals;
 
@@ -72,15 +77,15 @@ public class ClusterMetadataUpgradeHarryTest extends UpgradeTestBase
         .upgradesToCurrentFrom(v41)
         .withUpgradeListener(listener)
         .setup((cluster) -> {
-            SchemaSpec schema = new SchemaSpec("harry", "test_table",
-                                               asList(pk("pk1", asciiType), pk("pk2", int64Type)),
-                                               asList(ck("ck1", asciiType), ck("ck2", int64Type)),
-                                               asList(regularColumn("regular1", asciiType), regularColumn("regular2", int64Type)),
-                                               asList(staticColumn("static1", asciiType), staticColumn("static2", int64Type)));
+             SchemaSpec schema = new SchemaSpec("harry", "test_table",
+                                                asList(pk("pk1", asciiType), pk("pk2", int64Type)),
+                                                asList(ck("ck1", asciiType), ck("ck2", int64Type)),
+                                                asList(regularColumn("regular1", asciiType), regularColumn("regular2", int64Type)),
+                                                asList(staticColumn("static1", asciiType), staticColumn("static2", int64Type)));
 
             Configuration config = HarryHelper.defaultConfiguration()
                                               .setKeyspaceDdl(String.format("CREATE KEYSPACE IF NOT EXISTS %s WITH replication = {'class': 'SimpleStrategy', 'replication_factor': %d};", schema.keyspace, 3))
-                                              .setSchemaProvider(new FixedSchemaProviderConfiguration(schema))
+                                              .setSchemaProvider(new Configuration.FixedSchemaProviderConfiguration(schema))
                                               .setDataTracker(new Configuration.LockingDataTrackerConfiguration(-1l, -1l, Collections.emptyList()))
                                               .setSUT(new ExistingClusterSUT(cluster, listener))
                                               .build();
@@ -91,7 +96,7 @@ public class ClusterMetadataUpgradeHarryTest extends UpgradeTestBase
                     new FlaggedRunner(config.createRun(),
                                       config,
                                       asList(pool("Writer", 1, MutatingVisitor::new),
-                                             pool("Reader", 1, (run) -> new RandomPartitionValidator(run, new Configuration.QuiescentCheckerConfig(), QueryLogger.NO_OP))),
+                                             pool("Reader", 1, (run) -> new RandomPartitionValidator(run, new Configuration.QuiescentCheckerConfig(), QueryLogger.NO_OP.NO_OP))),
                                       stopLatch).run();
                 }
                 catch (Throwable e)
diff --git a/test/distributed/org/apache/cassandra/distributed/test/ring/ConsistentBootstrapTest.java b/test/distributed/org/apache/cassandra/fuzz/ring/ConsistentBootstrapTest.java
similarity index 68%
rename from test/distributed/org/apache/cassandra/distributed/test/ring/ConsistentBootstrapTest.java
rename to test/distributed/org/apache/cassandra/fuzz/ring/ConsistentBootstrapTest.java
index cab68089af..c03047478f 100644
--- a/test/distributed/org/apache/cassandra/distributed/test/ring/ConsistentBootstrapTest.java
+++ b/test/distributed/org/apache/cassandra/fuzz/ring/ConsistentBootstrapTest.java
@@ -16,20 +16,13 @@
  * limitations under the License.
  */
 
-package org.apache.cassandra.distributed.test.ring;
+package org.apache.cassandra.fuzz.ring;
 
 import java.util.concurrent.Callable;
 
 import org.junit.Assert;
 import org.junit.Test;
 
-import harry.core.Configuration;
-import harry.core.Run;
-import harry.operations.Query;
-import harry.visitors.LoggingVisitor;
-import harry.visitors.MutatingRowVisitor;
-import harry.visitors.MutatingVisitor;
-import harry.visitors.Visitor;
 import org.apache.cassandra.distributed.Cluster;
 import org.apache.cassandra.distributed.Constants;
 import org.apache.cassandra.distributed.api.ConsistencyLevel;
@@ -37,10 +30,13 @@ import org.apache.cassandra.distributed.api.Feature;
 import org.apache.cassandra.distributed.api.IInstanceConfig;
 import org.apache.cassandra.distributed.api.IInvokableInstance;
 import org.apache.cassandra.distributed.api.TokenSupplier;
-import org.apache.cassandra.distributed.fuzz.HarryHelper;
-import org.apache.cassandra.distributed.fuzz.InJvmSut;
+import org.apache.cassandra.harry.HarryHelper;
 import org.apache.cassandra.distributed.shared.NetworkTopology;
 import org.apache.cassandra.distributed.test.log.FuzzTestBase;
+import org.apache.cassandra.harry.dsl.ReplayingHistoryBuilder;
+import org.apache.cassandra.harry.sut.SystemUnderTest;
+import org.apache.cassandra.harry.sut.TokenPlacementModel;
+import org.apache.cassandra.harry.sut.injvm.InJvmSut;
 import org.apache.cassandra.metrics.TCMMetrics;
 import org.apache.cassandra.net.Verb;
 import org.apache.cassandra.tcm.Epoch;
@@ -55,49 +51,35 @@ public class ConsistentBootstrapTest extends FuzzTestBase
 {
     private static int WRITES = 500;
 
-    private static final Configuration.ConfigurationBuilder configBuilder;
-
-    static
-    {
-        try
-        {
-            configBuilder = HarryHelper.defaultConfiguration()
-                                               .setPartitionDescriptorSelector(new Configuration.DefaultPDSelectorConfiguration(1, 1))
-                                               .setClusteringDescriptorSelector(HarryHelper.defaultClusteringDescriptorSelectorConfiguration().setMaxPartitionSize(100).build());
-        }
-        catch (Exception e)
-        {
-            throw new RuntimeException(e);
-        }
-    }
-
     @Test
     public void bootstrapFuzzTest() throws Throwable
     {
+        IInvokableInstance cmsInstance = null;
         try (Cluster cluster = builder().withNodes(3)
                                         .withTokenSupplier(TokenSupplier.evenlyDistributedTokens(4))
                                         .withNodeIdTopology(NetworkTopology.singleDcNetworkTopology(4, "dc0", "rack0"))
                                         .withConfig((config) -> config.with(Feature.NETWORK, Feature.GOSSIP).set("metadata_snapshot_frequency", 5))
                                         .start())
         {
-            IInvokableInstance cmsInstance = cluster.get(1);
+            cmsInstance = cluster.get(1);
             waitForCMSToQuiesce(cluster, cmsInstance);
-            configBuilder.setSUT(() -> new InJvmSut(cluster));
-            Run run = configBuilder.build().createRun();
 
-            cluster.coordinator(1).execute("CREATE KEYSPACE " + run.schemaSpec.keyspace +
-                                           " WITH replication = {'class': 'SimpleStrategy', 'replication_factor' : 3};",
+            ReplayingHistoryBuilder harry = HarryHelper.dataGen(new InJvmSut(cluster),
+                                                                new TokenPlacementModel.SimpleReplicationFactor(3),
+                                                                SystemUnderTest.ConsistencyLevel.ALL);
+            cluster.coordinator(1).execute(String.format("CREATE KEYSPACE %s WITH replication = {'class': 'SimpleStrategy', 'replication_factor' : 3};", HarryHelper.KEYSPACE),
                                            ConsistencyLevel.ALL);
-            cluster.coordinator(1).execute(run.schemaSpec.compile().cql(), ConsistencyLevel.ALL);
+            cluster.coordinator(1).execute(harry.schema().compile().cql(), ConsistencyLevel.ALL);
             waitForCMSToQuiesce(cluster, cluster.get(1));
-            Visitor visitor = new LoggingVisitor(run, MutatingRowVisitor::new);
-            QuiescentLocalStateChecker model = new QuiescentLocalStateChecker(run);
-            System.out.println("Starting write phase...");
-            for (int i = 0; i < WRITES; i++)
-                visitor.visit();
-            System.out.println("Starting validate phase...");
-            for (int lts = 0; lts < run.clock.peek(); lts++)
-                model.validate(Query.selectPartition(run.schemaSpec, run.pdSelector.pd(lts, run.schemaSpec), false));
+            Runnable writeAndValidate = () -> {
+                System.out.println("Starting write phase...");
+                for (int i = 0; i < WRITES; i++)
+                    harry.insert();
+
+                System.out.println("Starting validate phase...");
+                harry.validateAll(harry.quiescentLocalChecker());
+            };
+            writeAndValidate.run();
 
             IInstanceConfig config = cluster.newInstanceConfig()
                                             .set("auto_bootstrap", true)
@@ -109,20 +91,7 @@ public class ConsistentBootstrapTest extends FuzzTestBase
             new Thread(() -> newInstance.startup()).start();
             pending.call();
 
-            for (int i = 0; i < WRITES; i++)
-                visitor.visit();
-
-            try
-            {
-                for (int lts = 0; lts < run.clock.peek(); lts++)
-                    model.validate(Query.selectPartition(run.schemaSpec, run.pdSelector.pd(lts, run.schemaSpec), false));
-            }
-            catch (Throwable t)
-            {
-                // Unpause, since otherwise validation exception will prevent graceful shutdown
-                unpauseCommits(cmsInstance);
-                throw t;
-            }
+            writeAndValidate.run();
 
             // Make sure there can be only one FinishJoin in flight
             waitForCMSToQuiesce(cluster, cmsInstance);
@@ -133,43 +102,50 @@ public class ConsistentBootstrapTest extends FuzzTestBase
             unpauseCommits(cmsInstance);
             waitForCMSToQuiesce(cluster, bootstrapVisible.call());
 
-            for (int i = 0; i < WRITES; i++)
-                visitor.visit();
-            model.validateAll();
+            writeAndValidate.run();
+        }
+        catch (Throwable t)
+        {
+            if (cmsInstance != null)
+                unpauseCommits(cmsInstance);
+            throw t;
         }
     }
 
     @Test
     public void coordinatorIsBehindTest() throws Throwable
     {
+        IInvokableInstance cmsInstance = null;
         try (Cluster cluster = builder().withNodes(3)
                                         .withTokenSupplier(TokenSupplier.evenlyDistributedTokens(4))
                                         .withNodeIdTopology(NetworkTopology.singleDcNetworkTopology(4, "dc0", "rack0"))
                                         .withConfig((config) -> config.with(Feature.NETWORK, Feature.GOSSIP).set("metadata_snapshot_frequency", 5))
                                         .start())
         {
-            IInvokableInstance cmsInstance = cluster.get(1);
+            cmsInstance = cluster.get(1);
             waitForCMSToQuiesce(cluster, cmsInstance);
-            configBuilder.setSUT(() -> new InJvmSut(cluster, () -> 2, (t) -> false) {
-                public Object[][] execute(String statement, ConsistencyLevel cl, int coordinator, Object... bindings)
-                {
-                    try
-                    {
-                        return super.execute(statement, cl, coordinator, bindings);
-                    }
-                    catch (Throwable t)
-                    {
-                        // Avoid retries
-                        return new Object[][]{};
-                    }
-                }
-            });
-            Run run = configBuilder.build().createRun();
 
-            cluster.coordinator(1).execute("CREATE KEYSPACE " + run.schemaSpec.keyspace +
-                                           " WITH replication = {'class': 'SimpleStrategy', 'replication_factor' : 3};",
+            ReplayingHistoryBuilder harry = HarryHelper.dataGen(new InJvmSut(cluster, () -> 2, (t) -> false)
+                                                                {
+                                                                    public Object[][] execute(String statement, ConsistencyLevel cl, int coordinator, Object... bindings)
+                                                                    {
+                                                                        try
+                                                                        {
+                                                                            return super.execute(statement, cl, coordinator, bindings);
+                                                                        }
+                                                                        catch (Throwable t)
+                                                                        {
+                                                                            // Avoid retries
+                                                                            return new Object[][]{};
+                                                                        }
+                                                                    }
+                                                                },
+                                                                new TokenPlacementModel.SimpleReplicationFactor(3),
+                                                                SystemUnderTest.ConsistencyLevel.ALL);
+
+            cluster.coordinator(1).execute(String.format("CREATE KEYSPACE %s WITH replication = {'class': 'SimpleStrategy', 'replication_factor' : 3};", HarryHelper.KEYSPACE),
                                            ConsistencyLevel.ALL);
-            cluster.coordinator(1).execute(run.schemaSpec.compile().cql(), ConsistencyLevel.ALL);
+            cluster.coordinator(1).execute(harry.schema().compile().cql(), ConsistencyLevel.ALL);
             waitForCMSToQuiesce(cluster, cluster.get(1));
 
             cluster.filters().verbs(Verb.TCM_REPLICATION.id,
@@ -180,7 +156,6 @@ public class ConsistentBootstrapTest extends FuzzTestBase
                    .drop()
                    .on();
 
-            Visitor visitor = new MutatingVisitor(run, MutatingRowVisitor::new);
             IInstanceConfig config = cluster.newInstanceConfig()
                                             .set("auto_bootstrap", true)
                                             .set(Constants.KEY_DTEST_FULL_STARTUP, true)
@@ -189,7 +164,7 @@ public class ConsistentBootstrapTest extends FuzzTestBase
 
             // Prime the CMS node to pause before the finish join event is committed
             Callable<?> pending = pauseBeforeCommit(cmsInstance, (e) -> e instanceof PrepareJoin.MidJoin);
-            long [] metricCounts = new long[4];
+            long[] metricCounts = new long[4];
             for (int i = 1; i <= 4; i++)
                 metricCounts[i - 1] = cluster.get(i).callOnInstance(() -> TCMMetrics.instance.coordinatorBehindPlacements.getCount());
             Thread thread = new Thread(() -> newInstance.startup());
@@ -206,7 +181,7 @@ public class ConsistentBootstrapTest extends FuzzTestBase
 
                 try
                 {
-                    visitor.visit();
+                    harry.insert();
                 }
                 catch (Throwable t)
                 {
@@ -246,6 +221,11 @@ public class ConsistentBootstrapTest extends FuzzTestBase
             unpauseCommits(cmsInstance);
             thread.join();
         }
+        catch (Throwable t)
+        {
+            if (cmsInstance != null)
+                unpauseCommits(cmsInstance);
+            throw t;
+        }
     }
-
-}
+}
\ No newline at end of file
diff --git a/test/distributed/org/apache/cassandra/distributed/test/log/ConsistentLeaveTest.java b/test/distributed/org/apache/cassandra/fuzz/ring/ConsistentLeaveTest.java
similarity index 75%
rename from test/distributed/org/apache/cassandra/distributed/test/log/ConsistentLeaveTest.java
rename to test/distributed/org/apache/cassandra/fuzz/ring/ConsistentLeaveTest.java
index 7e5a608e30..8c98eb2e2f 100644
--- a/test/distributed/org/apache/cassandra/distributed/test/log/ConsistentLeaveTest.java
+++ b/test/distributed/org/apache/cassandra/fuzz/ring/ConsistentLeaveTest.java
@@ -16,7 +16,7 @@
  * limitations under the License.
  */
 
-package org.apache.cassandra.distributed.test.log;
+package org.apache.cassandra.fuzz.ring;
 
 import java.util.List;
 import java.util.concurrent.Callable;
@@ -27,23 +27,22 @@ import com.google.common.util.concurrent.Uninterruptibles;
 import org.junit.Assert;
 import org.junit.Test;
 
-import harry.core.Configuration;
-import harry.core.Run;
-import harry.model.sut.SystemUnderTest;
-import harry.visitors.*;
 import org.apache.cassandra.distributed.Cluster;
 import org.apache.cassandra.distributed.api.ConsistencyLevel;
 import org.apache.cassandra.distributed.api.Feature;
 import org.apache.cassandra.distributed.api.IInvokableInstance;
 import org.apache.cassandra.distributed.api.TokenSupplier;
-import org.apache.cassandra.distributed.fuzz.HarryHelper;
-import org.apache.cassandra.distributed.fuzz.InJvmSut;
-import org.apache.cassandra.distributed.fuzz.InJvmSutBase;
+import org.apache.cassandra.harry.HarryHelper;
 import org.apache.cassandra.distributed.shared.NetworkTopology;
+import org.apache.cassandra.distributed.test.log.FuzzTestBase;
 import org.apache.cassandra.gms.ApplicationState;
 import org.apache.cassandra.gms.Gossiper;
+import org.apache.cassandra.harry.dsl.ReplayingHistoryBuilder;
+import org.apache.cassandra.harry.sut.SystemUnderTest;
+import org.apache.cassandra.harry.sut.TokenPlacementModel;
+import org.apache.cassandra.harry.sut.injvm.InJvmSut;
+import org.apache.cassandra.harry.sut.injvm.InJvmSutBase;
 import org.apache.cassandra.locator.InetAddressAndPort;
-import org.apache.cassandra.locator.ReplicationFactor;
 import org.apache.cassandra.service.StorageService;
 import org.apache.cassandra.tcm.Epoch;
 import org.apache.cassandra.tcm.transformations.PrepareLeave;
@@ -62,35 +61,36 @@ public class ConsistentLeaveTest extends FuzzTestBase
     @Test
     public void decommissionTest() throws Throwable
     {
-        Configuration.ConfigurationBuilder configBuilder = HarryHelper.defaultConfiguration()
-                                                                      .setPartitionDescriptorSelector(new Configuration.DefaultPDSelectorConfiguration(1, 1))
-                                                                      .setClusteringDescriptorSelector(HarryHelper.defaultClusteringDescriptorSelectorConfiguration().setMaxPartitionSize(100).build());
-
+        IInvokableInstance cmsInstance = null;
         try (Cluster cluster = builder().withNodes(3)
                                         .withTokenSupplier(TokenSupplier.evenlyDistributedTokens(3))
                                         .withNodeIdTopology(NetworkTopology.singleDcNetworkTopology(3, "dc0", "rack0"))
                                         .appendConfig(c -> c.with(Feature.NETWORK))
                                         .start())
         {
-            IInvokableInstance cmsInstance = cluster.get(1);
+            cmsInstance = cluster.get(1);
             IInvokableInstance leavingInstance = cluster.get(2);
             waitForCMSToQuiesce(cluster, cmsInstance);
 
-            configBuilder.setSUT(() -> new InJvmSut(cluster, () -> 1, InJvmSutBase.retryOnTimeout()));
-            Run run = configBuilder.build().createRun();
 
-            cluster.coordinator(1).execute("CREATE KEYSPACE " + run.schemaSpec.keyspace +
-                                           " WITH replication = {'class': 'SimpleStrategy', 'replication_factor' : 2};",
+            ReplayingHistoryBuilder harry = HarryHelper.dataGen(new InJvmSut(cluster, () -> 1, InJvmSutBase.retryOnTimeout()),
+                                                                new TokenPlacementModel.SimpleReplicationFactor(2),
+                                                                SystemUnderTest.ConsistencyLevel.ALL);
+            cluster.coordinator(1).execute(String.format("CREATE KEYSPACE %s WITH replication = {'class': 'SimpleStrategy', 'replication_factor' : 2};", HarryHelper.KEYSPACE),
                                            ConsistencyLevel.ALL);
-            cluster.coordinator(1).execute(run.schemaSpec.compile().cql(), ConsistencyLevel.ALL);
+            cluster.coordinator(1).execute(harry.schema().compile().cql(), ConsistencyLevel.ALL);
             waitForCMSToQuiesce(cluster, cmsInstance);
 
-            QuiescentLocalStateChecker model = new QuiescentLocalStateChecker(run, ReplicationFactor.fullOnly(2));
-            Visitor visitor = new LoggingVisitor(run, MutatingRowVisitor::new);
-            for (int i = 0; i < WRITES; i++)
-                visitor.visit();
+            Runnable writeAndValidate = () -> {
+                System.out.println("Starting write phase...");
+                for (int i = 0; i < WRITES; i++)
+                    harry.insert();
+
+                System.out.println("Starting validate phase...");
+                harry.validateAll(harry.quiescentLocalChecker());
+            };
+            writeAndValidate.run();
 
-            model.validateAll();
             // Prime the CMS node to pause before the finish leave event is committed
             Callable<?> pending = pauseBeforeCommit(cmsInstance, (e) -> e instanceof PrepareLeave.FinishLeave);
             new Thread(() -> leavingInstance.runOnInstance(() -> StorageService.instance.decommission(true))).start();
@@ -98,11 +98,8 @@ public class ConsistentLeaveTest extends FuzzTestBase
 
             waitForCMSToQuiesce(cluster, cmsInstance);
             assertGossipStatus(cluster, leavingInstance.config().num(), "LEAVING");
-            visitor = new GeneratingVisitor(run, new MutatingVisitor.MutatingVisitExecutor(run, new MutatingRowVisitor(run), SystemUnderTest.ConsistencyLevel.ALL));
-            for (int i = 0; i < WRITES; i++)
-                visitor.visit();
 
-            model.validateAll();
+            writeAndValidate.run();
 
             // Make sure there can be only one FinishLeave in flight
             waitForCMSToQuiesce(cluster, cmsInstance);
@@ -119,9 +116,13 @@ public class ConsistentLeaveTest extends FuzzTestBase
 
             assertGossipStatus(cluster, leavingInstance.config().num(), "LEFT");
 
-            for (int i = 0; i < WRITES; i++)
-                visitor.visit();
-            model.validateAll();
+            writeAndValidate.run();
+        }
+        catch (Throwable t)
+        {
+            if (cmsInstance != null)
+                unpauseCommits(cmsInstance);
+            throw t;
         }
     }
 
diff --git a/test/distributed/org/apache/cassandra/distributed/test/log/ConsistentMoveTest.java b/test/distributed/org/apache/cassandra/fuzz/ring/ConsistentMoveTest.java
similarity index 74%
rename from test/distributed/org/apache/cassandra/distributed/test/log/ConsistentMoveTest.java
rename to test/distributed/org/apache/cassandra/fuzz/ring/ConsistentMoveTest.java
index 4c6a75358e..4ee7bc6487 100644
--- a/test/distributed/org/apache/cassandra/distributed/test/log/ConsistentMoveTest.java
+++ b/test/distributed/org/apache/cassandra/fuzz/ring/ConsistentMoveTest.java
@@ -16,7 +16,7 @@
  * limitations under the License.
  */
 
-package org.apache.cassandra.distributed.test.log;
+package org.apache.cassandra.fuzz.ring;
 
 import java.util.List;
 import java.util.Random;
@@ -27,26 +27,21 @@ import java.util.stream.Collectors;
 import com.google.common.util.concurrent.Uninterruptibles;
 import org.junit.Test;
 
-import harry.core.Configuration;
-import harry.core.Run;
-import harry.model.sut.SystemUnderTest;
-import harry.visitors.GeneratingVisitor;
-import harry.visitors.LoggingVisitor;
-import harry.visitors.MutatingRowVisitor;
-import harry.visitors.MutatingVisitor;
-import harry.visitors.Visitor;
 import org.apache.cassandra.distributed.Cluster;
 import org.apache.cassandra.distributed.api.ConsistencyLevel;
 import org.apache.cassandra.distributed.api.Feature;
 import org.apache.cassandra.distributed.api.IInvokableInstance;
 import org.apache.cassandra.distributed.api.TokenSupplier;
-import org.apache.cassandra.distributed.fuzz.HarryHelper;
-import org.apache.cassandra.distributed.fuzz.InJvmSut;
+import org.apache.cassandra.harry.HarryHelper;
 import org.apache.cassandra.distributed.shared.NetworkTopology;
+import org.apache.cassandra.distributed.test.log.FuzzTestBase;
 import org.apache.cassandra.gms.ApplicationState;
 import org.apache.cassandra.gms.Gossiper;
+import org.apache.cassandra.harry.dsl.ReplayingHistoryBuilder;
+import org.apache.cassandra.harry.sut.SystemUnderTest;
+import org.apache.cassandra.harry.sut.TokenPlacementModel;
+import org.apache.cassandra.harry.sut.injvm.InJvmSut;
 import org.apache.cassandra.locator.InetAddressAndPort;
-import org.apache.cassandra.locator.ReplicationFactor;
 import org.apache.cassandra.service.StorageService;
 import org.apache.cassandra.tcm.Epoch;
 import org.apache.cassandra.tcm.transformations.PrepareMove;
@@ -65,35 +60,35 @@ public class ConsistentMoveTest extends FuzzTestBase
     @Test
     public void moveTest() throws Throwable
     {
-        Configuration.ConfigurationBuilder configBuilder = HarryHelper.defaultConfiguration()
-                                                                      .setPartitionDescriptorSelector(new Configuration.DefaultPDSelectorConfiguration(1, 1))
-                                                                      .setClusteringDescriptorSelector(HarryHelper.defaultClusteringDescriptorSelectorConfiguration().setMaxPartitionSize(100).build());
-
+        IInvokableInstance cmsInstance = null;
         try (Cluster cluster = builder().withNodes(3)
                                         .withTokenSupplier(TokenSupplier.evenlyDistributedTokens(3))
                                         .withNodeIdTopology(NetworkTopology.singleDcNetworkTopology(3, "dc0", "rack0"))
                                         .appendConfig(c -> c.with(Feature.NETWORK))
                                         .start())
         {
-            IInvokableInstance cmsInstance = cluster.get(1);
+            cmsInstance = cluster.get(1);
             IInvokableInstance movingInstance = cluster.get(2);
             waitForCMSToQuiesce(cluster, cmsInstance);
 
-            configBuilder.setSUT(() -> new InJvmSut(cluster));
-            Run run = configBuilder.build().createRun();
-
-            cluster.coordinator(1).execute("CREATE KEYSPACE " + run.schemaSpec.keyspace +
-                                           " WITH replication = {'class': 'SimpleStrategy', 'replication_factor' : 2};",
+            ReplayingHistoryBuilder harry = HarryHelper.dataGen(new InJvmSut(cluster),
+                                                                new TokenPlacementModel.SimpleReplicationFactor(2),
+                                                                SystemUnderTest.ConsistencyLevel.ALL);
+            cluster.coordinator(1).execute(String.format("CREATE KEYSPACE %s WITH replication = {'class': 'SimpleStrategy', 'replication_factor' : 2};", HarryHelper.KEYSPACE),
                                            ConsistencyLevel.ALL);
-            cluster.coordinator(1).execute(run.schemaSpec.compile().cql(), ConsistencyLevel.ALL);
+            cluster.coordinator(1).execute(harry.schema().compile().cql(), ConsistencyLevel.ALL);
             waitForCMSToQuiesce(cluster, cmsInstance);
 
-            FuzzTestBase.QuiescentLocalStateChecker model = new FuzzTestBase.QuiescentLocalStateChecker(run, ReplicationFactor.fullOnly(2));
-            Visitor visitor = new LoggingVisitor(run, MutatingRowVisitor::new);
-            for (int i = 0; i < WRITES; i++)
-                visitor.visit();
+            Runnable writeAndValidate = () -> {
+                System.out.println("Starting write phase...");
+                for (int i = 0; i < WRITES; i++)
+                    harry.insert();
+
+                System.out.println("Starting validate phase...");
+                harry.validateAll(harry.quiescentLocalChecker());
+            };
+            writeAndValidate.run();
 
-            model.validateAll();
 
             // Make sure there can be only one FinishLeave in flight
             waitForCMSToQuiesce(cluster, cmsInstance);
@@ -114,15 +109,7 @@ public class ConsistentMoveTest extends FuzzTestBase
             waitForCMSToQuiesce(cluster, nextEpoch);
 
             // TODO: rewrite the test to check only PENDING ranges.
-            visitor = new GeneratingVisitor(run, new MutatingVisitor.MutatingVisitExecutor(run, new MutatingRowVisitor(run), SystemUnderTest.ConsistencyLevel.ALL));
-            for (int i = 0; i < WRITES; i++)
-                visitor.visit();
-
-            model.validateAll();
-
-            for (int i = 0; i < WRITES; i++)
-                visitor.visit();
-            model.validateAll();
+            writeAndValidate.run();
 
             int clusterSize = cluster.size();
             List<InetAddressAndPort> endpoints = cluster.stream().map(i -> InetAddressAndPort.getByAddress(i.config().broadcastAddress())).collect(Collectors.toList());
@@ -135,6 +122,12 @@ public class ConsistentMoveTest extends FuzzTestBase
                 }
             }));
         }
+        catch (Throwable t)
+        {
+            if (cmsInstance != null)
+                unpauseCommits(cmsInstance);
+            throw t;
+        }
     }
 
     private void assertGossipStatus(Cluster cluster, int leavingInstance, String status)
diff --git a/test/harry/main/README.md b/test/harry/main/README.md
new file mode 100644
index 0000000000..a2f2cfacb9
--- /dev/null
+++ b/test/harry/main/README.md
@@ -0,0 +1,647 @@
+# Harry, a fuzz testing tool for Apache Cassandra
+
+The project aims to generate _reproducible_ workloads that are as close to real-life as possible, while being able to
+_efficiently_ verify the cluster state against the model without pausing the workload itself.
+
+## Getting Started in under 5 minutes
+
+Harry can operate as a straightforward read/write "correctness stress tool" that will check to ensure reads on a cluster
+are consistent with what it knows it wrote to the cluster. You have a couple options for this.
+
+### Option 2: Running things manually lower in the stack:
+
+To start a workload that performs a concurrent read/write workload, 2 read and 2 write threads for 60 seconds
+against a in-jvm cluster you can use the following code:
+
+```
+try (Cluster cluster = builder().withNodes(3)
+                                .start())
+{
+    SchemaSpec schema = new SchemaSpec("harry", "test_table",
+                                       asList(pk("pk1", asciiType), pk("pk1", int64Type)),
+                                       asList(ck("ck1", asciiType), ck("ck1", int64Type)),
+                                       asList(regularColumn("regular1", asciiType), regularColumn("regular1", int64Type)),
+                                       asList(staticColumn("static1", asciiType), staticColumn("static1", int64Type)));
+
+    Configuration config = HarryHelper.defaultConfiguration()
+                                        .setKeyspaceDdl(String.format("CREATE KEYSPACE IF NOT EXISTS %s WITH replication = {'class': 'SimpleStrategy', 'replication_factor': %d};", schema.keyspace, 3))
+                                        .setSUT(() -> new InJvmSut(cluster))
+                                        .build();
+
+    Run run = config.createRun();
+
+    concurrent(run, config,
+               asList(pool("Writer", 2, MutatingVisitor::new),
+                      pool("Reader", 2, RandomPartitionValidator::new)),
+               2, TimeUnit.MINUTES)
+    .run();
+}
+```
+
+# I've found a falsification. What now?
+
+There is no one-size-fits-all solution for debugging a falsification. We did try to create a shrinker, but unfortunately
+without Simulator, shrinker only works for issues that are non-concurrent in nature, since there's no way to create a
+stable repro otherwise. That said, there are several things that might get you started and inspire further ideas about
+how to debug the issue.
+
+First of all, understand whether or not the issue is likely to be concurrent in nature. If you re-run your test with the
+same seed, but see no falsification, and it fails only sporadically, and often on different logical timestamp, it is
+likely that the issue is, in fact concurrent. Here, it is important to note that when you are running concurrent
+read/write workload, you will get different interleaving of reads and writes every time you do this. 
+
+If you can get a stable repro with a sequential runner, you're in luck. Now all you need to do is to add logs everywhere
+and understand what might be causing it. But even if you do not have a stable repro, you are still likely to follow the
+same steps:
+
+* Inspect the error itself. Do Cassandra-returned results make sense? Is anything out of order? Are there any duplicates
+  or gaps?
+* Switch to logging mutating visitor and closely inspect its output. Closely inspect the output of the model. Do the
+  values make sense?
+* Check the output of data tracker. Does the model or Cassandra have missing columns or rows? Do these outputs contain
+  latest logical timestamps for each of the operations from the log? How about in-flight operations?
+* Filter out relevant operation log entries and inspect them closely. Given these operations, does the output of the
+  model, or output of the database make most sense?
+
+Next, you might want to try to narrow down the scope of the problem. Depending on what the falsification looks like, use
+your Cassandra knowledge to see what might apply in your situation:
+
+* Try checking if changing schema to use different column types does anything.
+* Try disabling range deletes, regular deletes, or column deletes.
+* Try changing the size of partition and see if the issue still reproduces.
+* Try disabling static columns.
+
+To avoid listing every feature in Harry, it suffices to say you should try to enable/disable features that make sense
+in the given context, and try to find the combination that avoids the failure, or a minimal combination that still
+reproduces the issue. Your first goal should be to find a _stable repro_, even if it involves modifying Cassandra or
+Harry, or taking the operations, and composing the repro manually. Having a stable repro will make finding a cause much
+simpler. Sometimes you will find the cause before you have a stable repro, in which case, you _still_ have to produce a
+stable repro to make things simpler for the reviewer, and to include it into the test suite of your patch.
+
+Lastly, *be patient*. Debugging falsifications is often a multi-hour endeavour, and things do not always jump out at you,
+so you might have to spend a significant amount of time tracking the problem down. Once you have found it, it is very
+rewarding.
+
+## Further Reading
+* [Harry: An open-source fuzz testing and verification tool for Apache Cassandra](https://cassandra.apache.org/_/blog/Harry-an-Open-Source-Fuzz-Testing-and-Verification-Tool-for-Apache-Cassandra.html)
+
+---
+# Technical and Implementation Details
+
+## System Under Test implementations
+
+* `in_jvm/InJvmSut` - simple in-JVM-dtest system under test.
+* `println/PrintlnSut` - system under test that prints to sdtout instead of executing queries on the cluster; useful for
+  debugging.
+* `mixed_in_jvm/MixedVersionInJvmSut` - in-JVM-dtest system under test that works with mixed version clusters.
+* `external/ExternalClusterSut` - system under test that works with CCM, Docker, Kubernetes, or cluster you may. have
+  deployed elsewhere
+
+Both in-JVM SUTs have fault-injecting functionality available.
+
+## Visitors
+
+* `single/SingleValidator` - visitor that runs several different read queries against a single partition that is
+  associated with current logical timestamp, and validates their results using given model.
+* `all_partitions/AllPartitionsValidator` - concurrently validates all partitions that were visited during this run.
+* `repair_and_validate_local_states/RepairingLocalStateValidator` - similar to `AllPartitionsValidator`, but performs
+  repair before checking node states.
+* `mutating/MutatingVisitor` - visitor that performs all sorts of mutations.
+* `logging/LoggingVisitor` - similar to `MutatingVisitor`, but also logs all operations to a file; useful for debug
+  purposes.
+* `corrupting/CorruptingVisitor` - visitor that will deliberately change data in the partition it visits. Useful for
+  negative tests (i.e. to ensure that your model actually detects data inconsistencies).
+
+And more.
+
+## Models
+
+* `querying_no_op/QueryingNoOpValidator` - a model that can be used to "simply" run random queries.
+* `quiescent_checker/QuiescentChecker` - a model that can be used to verify results of any read that has no writes to
+  the same partition_ concurrent to it. Should be used in conjunction with locking data tracker.
+* `quiescent_local_state_checker/QuiescentLocalStateChecker` - a model that can check local states of each replica that
+  has to own
+
+## Runners
+
+* `sequential/SequentialRunner` - runs all visitors sequentially, in the loop, for a specified amount of time; useful
+  for simple tests that do not have to exercise concurrent read/write path.
+* `concurrent/ConcurrentRunner` - runs all visitors concurrently, each visitor in its own thread, looped, for a
+  specified amount of time; useful for things like concurrent read/write workloads.
+* `chain/ChainRunner` - receives other runners as input, and runs them one after another once. Useful for both simple
+  and complex scenarios that involve both read/write workloads, validating all partitions, exercising other node-local
+  or cluster-wide operations.
+* `staged/StagedRunner` - receives other runners (stages) as input, and runs them one after another in a loop; useful
+  for implementing complex scenarios, such as read/write workloads followed by some cluster changing operations.
+
+## Clock
+
+* `approximate_monotonic/ApproximateMonotonicClock` - a timestamp supplier implementation that tries to keep as close to
+  real time as possible, while preserving mapping from real-time to logical timestamps.
+* `offset/OffsetClock` - a (monotonic) clock that supplies timestamps that do not have any relation to real time.
+
+# Introduction
+
+Harry has two primary modes of functionality:
+
+* Unit test mode: in which you define specific sequences of
+  operations and let Harry test these operations using different
+  schemas and conditions.
+* Exploratory/fuzz mode: in which you define distributions of events
+  rather rather than sequences themselves, and let Harry try out
+  different things.
+
+Usually, in unit-test mode, we’re applying several write operations to the cluster state and then run different read
+queries and validate their results. To learn more about writing unit tests, refer to the "Writing Unit Tests" section.
+
+In exploratory mode, we continuously apply write operations to the cluster and validate their state, allowing data size
+to grow and simulating real-life behaviour. To learn more about implementing test cases using fuzz mode, refer to the "
+Implementing Tests" section of this guide, but it's likely you'll have to read the rest of this document to implement
+more complex scenarios.
+
+# Writing Unit Tests
+
+To write unit tests with Harry, there's no special knowledge required. Usually, unit tests are written by simply
+hardcoding the schema and then writing several modification statements one after the other, and then manually validating
+results of a `SELECT` query. This might work for simple scenarios, but there’s still a chance that for some other schema
+or some combination of values the tested feature may not work.
+
+To improve the situation, we can express the test in more abstract terms and, instead of writing it using specific
+statements, we can describe which statement _types_ are to be used:
+
+```
+test(new SchemaGenerators.Builder("harry")
+                         .partitionKeySpec(1, 5)
+                         .clusteringKeySpec(1, 5)
+                         .regularColumnSpec(1, 10)
+                         .generator(),
+     historyBuilder -> {
+         historyBuilder.insert();
+         historyBuilder.deletePartition();
+         historyBuilder.deleteRowSlice();
+     });
+```
+
+This spec can be used to generate clusters of different sizes, configured with different schemas, executing the given a
+sequence of actions both in isolation, and combined with other randomly generated ones, with failure-injection.
+
+Best of all is that this test will _not only_ ensure that such a sequence of actions does not produce an exception, but
+also would ensure that cluster will respond with correct results to _any_ allowed read query.
+
+To begin specifying operations for a new partition, either start calling methods on the `HistoryBuilder`, or, if you 
+would like to specify the partition which Harry needs to visit use `#visitPartition` or `#beginBatch` have to be called, 
+for starting a visit to a particular partition with a single or multiple actions.
+
+After that, the actions are self-explanatory: `#insert`, `#update`, `#deleteRow`, `#deleteColumns`, `#deleteRowRange`, `#deleteRowSlice`
+`#deletePartition`.
+
+After history generated by `HistoryBuilder` is replayed using `ReplayingVisitor` (or by using a `ReplayingHistoryBuilder` 
+which combines the two for your convenience), you can use any model (`QuiescentChecker` by default) to validate queries.
+Queries can be provided manually or generated using `QueryGenerator` or `TypedQueryGenerator`.
+
+# Basic Terminology
+
+* Inflate / inflatable: a process of producing a value (for example, string, or a blob) from a `long` descriptor that
+  uniquely identifies the value. See data generation section of this guide for more details.
+* Deflate / deflatable: a process of producing the descriptor the value was inflated from during verification.
+  See model section of this guide for more details.
+
+For definitions of logical timestamp, descriptor, and other entities used during inflation and deflation, refer
+to formal relationships section.
+
+# Features
+
+Currently, Harry can exercise the following Cassandra functionality:
+
+* Supported data types: `int8`, `int16`, `int32`, `int64`, `boolean`, `float`, `double`, `ascii`, `uuid`, `timestamp`.
+  Collections are only _inflatable_.
+* Random schema generation, with an arbitrary number of partition and clustering keys.
+* Schemas with arbitrary `CLUSTERING ORDER BY`
+* Randomly generated `INSERT` and `UPDATE` queries with all columns or arbitrary column subset
+* Randomly generated `DELETE` queries: for a single column, single row, or a range of rows
+* Inflating and validating entire partitions (with allowed in-flight queries)
+* Inflating and validating random `SELECT` queries: single row, slices (with single open end), and ranges (with both
+  ends of clusterings specified)
+
+Inflating partitions is done using `Reconciler`. Validating partitions and random queries can be done using `QuiescentChecker`.
+
+## Outstanding Work
+
+#### The following have not yet been implemented:
+
+* Some types (such as collections) are not deflatable
+* 2i queries are not implemented
+* Fault injection is not implemented (available via Cassandra Simulator)
+* TTL is not supported
+* Some SELECT queries are not supported: `LIMIT`, `IN`, `GROUP BY`, token range queries
+
+#### Some features can be improved upon or further optimized:
+
+* Pagination is currently implemented but hard-coded to a page size of 1
+* RNG should be able to yield less than 64 bits of entropy per step
+* State tracking should be done in a compact off-heap data structure
+* Inflated partition state and per-row operation log should be done in a compact
+  off-heap data structure
+* Decision-making about _when_ we visit partitions and/or rows should be improved
+
+While this list of improvements is incomplete, t should give the reader a rough idea about the state of the project.
+The original goal of the project was to drive to stability after the significant storage engine rewrite in
+CASSANDRA-8099 and help remove data loss bugs from the codebase before they got out into the wild. Next steps are to
+integrate it into both CI and into regular daily dev workflows.
+
+# Goals: Reproducibility and Efficiency
+
+_Reproducibility_ is achieved by using the PCG family of random number generators and generating schema, configuration,
+and every step of the workload from the repeatable sequence of random numbers. Schema and configuration are generated
+from the _seed_. Each operation is assigned its own monotonically increasing _logical timestamp_, which preserves
+logical operation order between different runs.
+
+_Efficiency_ is achieved by employing the features of the PCG random number generator (walking the sequence of random
+numbers back and forth), and writing value generators in a way that preserves properties of the descriptor it was
+generated from.
+
+Given a `long` _descriptor_ can be _inflated_ into some value:
+
+* value can be _deflated_ back to the descriptor it was generated from (in other words, generation is *invertible*)
+* two inflated values will sort the same way as two descriptors they were generated from (in other words, generation is
+  *order-preserving*)
+
+These properties are also preserved for the composite values, such as clustering and partition keys.
+
+# Components
+
+Every Harry run starts from Configuration. You can find an example configuration under `conf/example.yml`.
+
+*Clock* is a component responsible for mapping _logical_ timestamps to _real-time_ ones. When reproducing test failures,
+and for validation purposes, a snapshot of such be taken to map a real-time timestamp from the value retrieved from the
+database to map it back to the logical timestamp of the operation that wrote this value. Given a real-time timestamp,
+the clock can return a logical timestamp, and vice versa.
+
+*Runner* is a component that schedules operations that change the cluster (system under test) and model state.
+
+*System under test*: a Cassandra node or cluster. Default implementation is in_jvm (in-JVM DTest cluster). Harry also
+supports external clusters.
+
+*Model* is responsible for tracking logical timestamps that system under test was notified about.
+
+*Partition descriptor selector* controls how partitions are selected based on the current logical timestamp. The default
+implementation is a sliding window of partition descriptors that will visit one partition after the other in the
+window `slide_after_repeats` times. After that, it will retire one partition descriptor, and pick a new one.
+
+*Clustering descriptor selector* controls how clustering keys are picked within the partition: how many rows there can
+be in a partition, how many rows are visited for a logical timestamp, how many operations there will be in batch, what
+kind of operations there will and how often each kind of operation is going to occur.
+
+# Implementing Tests
+
+All Harry components are pluggable and can be redefined. However, many of the default implementations will cover most of
+the use-cases, so in this guide we’ll focus on ones that are most often used to implement different use cases:
+
+* System Under Test: defines how Harry can communicate with Cassandra instances and issue common queries. Examples of a
+  system under test can be a CCM cluster, a “real” Cassandra cluster, or an in-JVM dtest cluster.
+* Visitor: defines behaviour that gets triggered at a specific logical timestamp. One of the default implementations is
+  MutatingVisitor, which executes write workload against SystemUnderTest. Examples of a visitor, besides a mutating
+  visitor, could be a validator that uses the model to validate results of different queries, a repair runner, or a
+  fault injector.
+* Model: validates results of read queries by comparing its own internal representation against the results returned by
+  system under test. You can find three simplified implementations of model in this document: Visible Rows Checker,
+  Quiescent Checker, and an Exhaustive Checker.
+* Runner: defines how operations defined by visitors are executed. Harry includes two default implementations: a
+  sequential and a concurrent runner. Sequential runner allows no overlap between different visitors or logical
+  timestamps. Concurrent runner allows visitors for different timestamps to overlap.
+
+System under test is the simplest one to implement: you only need a way to execute Cassandra queries. At the moment of
+writing, all custom things, such as nodetool commands, failure injection, etc, are implemented using a SUT / visitor
+combo: visitor knows about internals of the cluster it is dealing with.
+
+Generally, visitor has to follow the rules specified by DescriptorSelector and PdSelector: (it can only visit issue
+mutations against the partition that PdSelector has picked for this LTS), and DescriptorSelector (it can visit exactly
+DescriptorSelector#numberOfModifications rows within this partition, operations have to have a type specified by
+#operationKind, clustering and value descriptors have to be in accordance with DescriptorSelector#cd and
+DescriptorSelector#vds). The reason for these limitations is because model has to be able to reproduce the exact
+sequence of events that was applied to system under test.
+
+Default implementations of partition and clustering descriptors, used in fuzz mode allow to optimise verification. For
+example, it is possible to go find logical timestamps that are visiting the same partition as any given logical
+timestamp. When running Harry in unit-test mode, we use a special generating visitor that keeps an entire given sequence
+of events in memory rather than producing it on the fly. For reasons of efficiency, we do not use generating visitors
+for generating and verifying large datasets.
+
+# Formal Relations Between Entities
+
+To be able to implement efficient models, we had to reduce the amount of state required for validation to a minimum and
+try to operate on primitive data values most of the time. Any Harry run starts with a `seed`. Given the same
+configuration, and the same seed, we're able to make runs deterministic (in other words, records in two clusters created
+from the same seed are going to have different real-time timestamps, but will be otherwise identical; logical time
+stamps will also be identical).
+
+Since it's clear how to generate things like random schemas, cluster configurations, etc., let's discuss how we're
+generating data, and why this type of generation makes validation efficient.
+
+First, we're using PCG family of random number generators, which, besides having nice characteristics that any RNG
+should have, have two important features:
+
+* Streams: for single seed, we can have several independent _different_ streams of random numbers.
+* Walkability: PCG generators generate a stream of numbers you can walk _back_ and _forth_. That is, for any number _n_
+  that represents a _position_ of the random number in the stream of random numbers, we can get the random number at
+  this position. Conversely, given a random number, we can determine what is its position in the stream. Moreover,
+  knowing a random number, we can determine which number precedes it in the stream of random numbers, and, finally, we
+  can determine how many numbers there are in a stream between the two random numbers.
+
+Out of these operations, determining the _next_ random number in the sequence can be done in constant time, `O(1)`.
+Advancing generator by _n_ steps can be done in `O(log(n))` steps. Since generation is cyclical, advancing the iterator
+backward is equivalent to advancing it by `cardinality - 1` steps. If we're generating 64 bits of entropy, advancing
+by `-1` can be done in 64 steps.
+
+Let's introduce some definitions:
+
+* `lts` is a *logical timestamp*, an entity (number in our case), given by the clock, on which some action occurs
+* `m` is a *modification id*, a sequential number of the modification that occurs on `lts`
+* `rts` is an approximate real-time as of clock for this run
+* `pid` is a partition position, a number between `0` and N, for `N` unique generated partitions
+* `pd` is a partition descriptor, a unique descriptor identifying the partition
+* `cd` is a clustering descriptor, a unique descriptor identifying row within some partition
+
+A small introduction that can help to understand the relation between these
+entities. Hierarchically, the generation process looks as follows:
+
+* `lts` is an entry point, from which the decision process starts
+* `pd` is picked from `lts`, and determines which partition is going to be visited
+* for `(pd, lts)` combination, `#mods` (the number of modification batches) and `#rows` (the number of rows per
+  modification batch) is determined. `m` is an index of the modification batch, and `i` is an index of the operation in
+  the modification batch.
+* `cd` is picked based on `(pd, lts)`, and `n`, a sequential number of the operation among all modification batches
+* operation type (whether we're going to perform a write, delete, range delete, etc), columns involved in this
+  operation, and values for the modification are picked depending on the `pd`, `lts`, `m`, and `i`
+
+Most of this formalization is implemented in `OpSelectors`, and is relied upon in`PartitionVisitor` and any
+implementation of a `Model`.
+
+Random number generation (see `OpSelectors#Rng`):
+
+* `rng(i, stream[, e])`: returns i'th number drawn from random sequence `stream` producing values with `e` bits of
+  entropy (64 bits for now).
+* `rng'(s, stream[, e])`: returns `i` of the random number `s` drawn from the random sequence `stream`. This function is
+  an inverse of `rng`.
+* `next(rnd, stream[, e])` and `prev(rnd, stream[, e])`: the next/previous number relative to `rnd` drawn from random
+  sequence `stream`.
+
+A simple example of a partition descriptor selector is one that is based on a sliding window of a size `s`, that slides
+every `n` iterations. First, we determine _where_ the window should start for a given `lts` (in other words, how many
+times it has already slid). After that, we determine which `pd` we pick out of `s` available ones. After picking each
+one of the `s` descriptors `n` times, we retire the oldest descriptor and pick a new one to the window. Window start and
+offset are then used as input for the `rng(start + offset, stream)` to make sure descriptors are uniformly distributed.
+
+We can build a clustering descriptor selector in a similar manner. Each partition will use its `pd` as a stream id, and
+pick `cd` from a universe of possible `cds` of size `#cds`. On each `lts`, we pick a random `offset`, and start
+picking `#ops` clusterings from this `offset < #cds`, and wrap around to index 0 after that. This way, each operation
+maps to a unique `cd`, and `#op` can be determined from `cd` deterministically.
+
+# Data Generation
+
+So far, we have established how to generate partition, clustering, and value _descriptors_. Now, we need to understand
+how we can generate data modification statements out of these descriptors in a way that helps us to validate data later.
+
+Since every run has a predefined schema, and by the time we visit a partition we have a logical timestamp, we can make
+the rest of the decisions: pick a number of batches we're about to perform, determine what kind of operations each one
+of the batches is going to contain, which rows we're going to visit (clustering for each modification operation).
+
+To generate a write, we need to know _which partition_ we're going to visit (in other words, partition descriptor),
+_which row_ we'd like to modify (in other words, clustering descriptor), _which columns_ we're modifying (in other
+words, a column mask), and, for each modified column - its value. By the time we're ready to make an actual query to the
+database, we already know `pd`, `cd`, `rts`, and `vds[]`, which is all we need to "inflate" a write.
+
+To inflate each value descriptor, we take a generator for its datatype, and turn its descriptor into the object. This
+generation process has the following important properties:
+
+* it is invertible: for every `inflate(vd) -> value`, there's `deflate(value) -> vd`
+* it is order-preserving: `compare(vd1, vd2) == compare(inflate(vd1), inflate(vd2))`
+
+Inflating `pd` and `cd` is slightly more involved than inflating `vds`, since partition and clustering keys are often
+composite. This means that `inflate(pd)` returns an array of objects, rather just a single
+object: `inflate(pd) -> value[]`, and `deflate(value[]) -> pd`. Just like inflating value descriptors, inflating keys
+preserves order.
+
+It is easy to see that, given two modifications: `Update(pd1, cd1, [vd1_1, vd2_1, vd3_1], lts1)`
+and `Update(pd1, cd1, [vd1_2, vd3_2], lts2)`, we will end up with a resultset that contains effects of both
+operations: `ResultSetRow(pd1, cd1, [vd1_2@rts2, vd2_1@rts1, vd3_2@rts2])`.
+
+# Model
+
+`Model` in Harry ties the rest of the components together and allows us to check whether or not data returned by the
+cluster actually makes sense. The model relies on the clock, since we have to convert real-time timestamps of the
+returned values back to logical timestamps, and on descriptor selectors to pick the right partition and rows.
+
+## Visible Rows Checker
+
+Let's try to put it all together and build a simple model. The simplest one is a visible row checker. It can check if
+any row in the response returned from the database could have been produced by one of the operations. However, it won't
+be able to find errors related to missing rows, and will only notice some cases of erroneously overwritten rows.
+
+In the model, we can see a response from the database in its deflated state. In other words, instead of the actual
+values returned, we see their descriptors. Every resultset row consists of `pd`, `cd`, `vds[]` (value descriptors),
+and `lts[]` (logical timestamps at which these values were written).
+
+To validate, we need to iterate through all operations for this partition, starting with the latest one the model is
+aware of. This model has no internal state, and validates entire partitions:
+
+```
+void validatePartitionState(long validationLts, List<ResultSetRow> rows) {
+  long pd = pdSelector.pd(validationLts, schema);
+
+  for (ResultSetRow row : rows) {
+      // iterator that gives us unique lts from the row in descending order
+      LongIterator rowLtsIter = descendingIterator(row.lts);
+      // iterator that gives us unique lts from the model in descending order
+      LongIterator modelLtsIter = descendingIterator(pdSelector, validationLts);
+
+      outer:
+      while (rowLtsIter.hasNext()) {
+        long rowLts = rowLtsIter.nextLong();
+
+ 	// this model can not check columns whose values were never written or were deleted
+        if (rowLts == NO_TIMESTAMP)
+          continue outer;
+
+        if (!modelLtsIter.hasNext())
+          throw new ValidationException(String.format("Model iterator is exhausted, could not verify %d lts for the row: \n%s %s",
+                                                       rowLts, row));
+
+        while (modelLtsIter.hasNext()) {
+          long modelLts = modelLtsIter.nextLong();
+	  // column was written by the operation that has a lower lts than the current one from the model
+          if (modelLts > rowLts)
+              continue;
+	  // column was written by the operation that has a higher lts, which contradicts to the model, since otherwise we'd validate it by now
+          if (modelLts < rowLts)
+              throw new RuntimeException("Can't find a corresponding event id in the model for: " + rowLts + " " + modelLts);
+
+          // Compare values for columns that were supposed to be written with this lts
+	  for (int col = 0; col < row.lts.length; col++) {
+             if (row.lts[col] != rowLts)
+               continue;
+
+             long m = descriptorSelector.modificationId(pd, row.cd, rowLts, row.vds[col], col);
+             long vd = descriptorSelector.vd(pd, row.cd, rowLts, m, col);
+
+	     // If the value model predicts doesn't match the one received from the database, throw an exception
+             if (vd != row.vds[col])
+                throw new RuntimeException("Returned value doesn't match the model");
+	  }
+	  continue outer;
+        }
+    }
+  }
+}
+```
+
+As you can see, all validation is done using deflated `ResultSetRows`, which contain enough data to say which logical
+timestamp each value was written with, and which value descriptor each value has. This model can also validate data
+concurrently to the ongoing data modification operations.
+
+## Quiescent Checker
+
+Let's consider one more checker. It'll be more powerful than the visible rows checker in one way since it can find any
+inconsistency in data (incorrect timestamp, missing or additional row, rows coming in the wrong order, etc), but it'll
+also have one limitation: it won't be able to run concurrently with data modification statements. This means that for
+this model to be used, we should have no _in-flight_ queries, and all queries have to be in a deterministic state by the
+time we're validating their results.
+
+For this checker, we assume that we have a component that is called `Reconciler`, which can inflate partition state _up
+to some_ `lts`. `Reconciler` works by simply applying each modification in the same order they were applied to the
+cluster, and using standard Cassandra data reconciliation rules (last write wins / DELETE wins over INSERT in case of a
+timestamp collision).
+
+With this component, and knowing that there can be no in-fight queries, we can validate data in the following way:
+
+```
+public void validatePartitionState(Iterator<ResultSetRow> actual, Query query) {
+  // find out up the highest completed logical timestamp
+  long maxCompleteLts = tracker.maxComplete();
+
+  // get the expected state from reconciler
+  Iterator<Reconciler.RowState> expected = reconciler.inflatePartitionState(query.pd, maxCompleteLts, query).iterator(query.reverse);
+
+  // compare actual and expected rows one-by-one in-order
+  while (actual.hasNext() && expected.hasNext()) {
+    ResultSetRow actualRowState = actual.next();
+    Reconciler.RowState expectedRowState = expected.next();
+
+    if (actualRowState.cd != expectedRowState.cd)
+      throw new ValidationException("Found a row in the model that is not present in the resultset:\nExpected: %s\nActual: %s",
+                                    expectedRowState, actualRowState);
+
+    if (!Arrays.equals(actualRowState.vds, expectedRowState.vds))
+      throw new ValidationException("Returned row state doesn't match the one predicted by the model:\nExpected: %s (%s)\nActual:   %s (%s).",
+                                    Arrays.toString(expectedRowState.vds), expectedRowState,
+                                    Arrays.toString(actualRowState.vds), actualRowState);
+
+    if (!Arrays.equals(actualRowState.lts, expectedRowState.lts))
+      throw new ValidationException("Timestamps in the row state don't match ones predicted by the model:\nExpected: %s (%s)\nActual:   %s (%s).",
+                                    Arrays.toString(expectedRowState.lts), expectedRowState,
+                                    Arrays.toString(actualRowState.lts), actualRowState);
+  }
+
+  if (actual.hasNext() || expected.hasNext()) {
+    throw new ValidationException("Expected results to have the same number of results, but %s result iterator has more results",
+                                   actual.hasNext() ? "actual" : "expected");
+  }
+}
+```
+
+If there's any mismatch, it'll be caught right away: if there's an extra row
+(for example, there were issues in Cassandra that caused it to have duplicate
+rows), or if some row or even value in the row is missing.
+
+## Exhaustive Checker
+
+To be able to both run validation concurrently to modifications and be able to
+catch all kinds of inconsistencies, we need a more involved checker.
+
+In this checker, we rely on inflating partition state. However, we're most
+interested in `lts`, `opId`, and visibility (whether or not it is still
+in-flight) of each modification operation. To be able to give a reliable result,
+we need to make sure we follow these rules:
+
+* every operation model _thinks_ should be visible, has to be visible
+* every operation model _thinks_ should be invisible, has to be invisible
+* every operation model doesn't know the state of (i.e., it is still
+  in-flight) can be _either_ visible _invisible_
+* there can be no state in the database that model is not aware of (in other words,
+  we either can _explain_ how a row came to be, or we conclude that the row is
+  erroneous)
+
+A naive way to do this would be to inflate every possible partition state, where
+every in-flight operation would be either visible or invisible, but this gets
+costly very quickly since the number of possible combinations grows
+exponentially. A better (and simpler) way to do this is to iterate all
+operations and keep the state of "explained" operations:
+
+```
+public class RowValidationState {
+  // every column starts in UNOBSERVED, and has to move to either REMOVED, or OBSERVED state
+  private final ColumnState[] columnStates;
+  // keep track of operations related to each column state
+  private final Operation[] causingOperations;
+}
+```
+
+Now, we move through all operations for a given row, starting from the _newest_ ones, towards
+the oldest ones:
+
+```
+public void validatePartitionState(long verificationLts, PeekingIterator<ResultSetRow> actual_, Query query) {
+  // get a list of operations for each cd
+  NavigableMap<Long, List<Operation>> operations = inflatePartitionState(query);
+
+  for (Map.Entry<Long, List<Operation>> entry : operations.entrySet()) {
+    long cd = entry.getKey();
+    List<Operation> ops = entry.getValue();
+
+    // Found a row that is present both in the model and in the resultset
+    if (actual.hasNext() && actual.peek().cd == cd) {
+      validateRow(new RowValidationState(actual.next), operations);
+    } else {
+      validateNoRow(cd, operations);
+
+      // Row is not present in the resultset, and we currently look at modifications with a clustering past it
+      if (actual.hasNext() && cmp.compare(actual.peek().cd, cd) < 0)
+         throw new ValidationException("Couldn't find a corresponding explanation for the row in the model");
+    }
+  }
+
+  // if there are more rows in the resultset, and we don't have model explanation for them, we've found an issue
+  if (actual.hasNext())
+    throw new ValidationException("Observed unvalidated rows");
+}
+```
+
+Now, we have to implement `validateRow` and `validateNoRow`. `validateNoRow` is easy: we only need to make sure that a
+set of operations results in an invisible row. Since we're iterating operations in reverse order, if we encounter a
+delete not followed by any writes, we can conclude that the row is invisible and exit early. If there's a write that is
+not followed by a delete, and the row isn't covered by a range tombstone, we know it's an error.
+
+`validateRow` only has to iterate operations in reverse order until it can explain the value in every column. For
+example, if a value is `UNOBSERVED`, and the first thing we encounter is a `DELETE` that removes this column, we only
+need to make sure that the value is actually `null`, in which case we can conclude that the value can be explained
+as `REMOVED`.
+
+Similarly, if we encounter an operation that has written the expected value, we conclude that the value is `OBSERVED`.
+If there are any seeming inconsistencies between the model and resultset, we have to check whether or not the operation
+in question is still in flight. If it is, its results may still not be visible, so we can't reliably say it's an error.
+
+To summarize, in order for us to implement an exhaustive checker, we have to iterate operations for each of the rows
+present in the model in reverse order until we either detect inconsistency that can't be explained by an in-flight
+operation or until we explain every value in the row.
+
+## Conclusion
+
+As you can see, all checkers up till now are almost entirely stateless. Exhaustive and quiescent models rely
+on `DataTracker` component that is aware of the in-flight and completed `lts`, but don't need any other state apart from
+that, since we can always inflate a complete partition from scratch every time we validate.
+
+While not relying on the state is a useful feature, at least _some_ state is useful to have. For example, if we're
+validating just a few rows in the partition, right now we have to iterate through each and every `lts` that has visited
+this partition and filter out only modifications that have visited it. However, since the model is notified of each
+_started_, and, later, finished modification via `recordEvent`, we can keep track of `pd -> (cd -> lts)` map. You can
+check out `VisibleRowsChecker` as an example of that.
diff --git a/test/distributed/org/apache/cassandra/distributed/fuzz/HarryHelper.java b/test/harry/main/org/apache/cassandra/harry/HarryHelper.java
similarity index 84%
rename from test/distributed/org/apache/cassandra/distributed/fuzz/HarryHelper.java
rename to test/harry/main/org/apache/cassandra/harry/HarryHelper.java
index e2d8a0bbb6..09cc574082 100644
--- a/test/distributed/org/apache/cassandra/distributed/fuzz/HarryHelper.java
+++ b/test/harry/main/org/apache/cassandra/harry/HarryHelper.java
@@ -16,32 +16,37 @@
  * limitations under the License.
  */
 
-package org.apache.cassandra.distributed.fuzz;
+package org.apache.cassandra.harry;
 
 import java.io.File;
-import java.util.concurrent.TimeUnit;
 import java.util.concurrent.atomic.AtomicInteger;
 
-import harry.core.Configuration;
-import harry.ddl.ColumnSpec;
-import harry.ddl.SchemaGenerators;
-import harry.ddl.SchemaSpec;
-import harry.generators.Surjections;
-import harry.model.OpSelectors;
-import harry.model.clock.OffsetClock;
-import harry.model.sut.PrintlnSut;
+import org.apache.cassandra.harry.clock.OffsetClock;
+import org.apache.cassandra.harry.core.Configuration;
+import org.apache.cassandra.harry.ddl.ColumnSpec;
+import org.apache.cassandra.harry.ddl.SchemaGenerators;
+import org.apache.cassandra.harry.ddl.SchemaSpec;
+import org.apache.cassandra.harry.dsl.ReplayingHistoryBuilder;
+import org.apache.cassandra.harry.gen.Surjections;
+import org.apache.cassandra.harry.model.OpSelectors;
+import org.apache.cassandra.harry.runner.HarryRunner;
+import org.apache.cassandra.harry.sut.SystemUnderTest;
+import org.apache.cassandra.harry.sut.TokenPlacementModel;
+import org.apache.cassandra.harry.tracker.DefaultDataTracker;
 
 import static org.apache.cassandra.config.CassandraRelevantProperties.CASSANDRA_ALLOW_SIMPLE_STRATEGY;
 import static org.apache.cassandra.config.CassandraRelevantProperties.CASSANDRA_MINIMUM_REPLICATION_FACTOR;
 import static org.apache.cassandra.config.CassandraRelevantProperties.DISABLE_TCACTIVE_OPENSSL;
+import static org.apache.cassandra.config.CassandraRelevantProperties.IO_NETTY_TRANSPORT_NONATIVE;
 import static org.apache.cassandra.config.CassandraRelevantProperties.LOG4J2_DISABLE_JMX;
 import static org.apache.cassandra.config.CassandraRelevantProperties.LOG4J2_DISABLE_JMX_LEGACY;
 import static org.apache.cassandra.config.CassandraRelevantProperties.LOG4J_SHUTDOWN_HOOK_ENABLED;
 import static org.apache.cassandra.config.CassandraRelevantProperties.ORG_APACHE_CASSANDRA_DISABLE_MBEAN_REGISTRATION;
-import static org.apache.cassandra.config.CassandraRelevantProperties.IO_NETTY_TRANSPORT_NONATIVE;
 
 public class HarryHelper
 {
+    public static final String KEYSPACE = "harry";
+
     public static void init()
     {
         // setting both ways as changes between versions
@@ -53,17 +58,11 @@ public class HarryHelper
         DISABLE_TCACTIVE_OPENSSL.setBoolean(true);
         IO_NETTY_TRANSPORT_NONATIVE.setBoolean(true);
         ORG_APACHE_CASSANDRA_DISABLE_MBEAN_REGISTRATION.setBoolean(true);
-
-        InJvmSut.init();
-        QueryingNoOpChecker.init();
-        Configuration.registerSubtypes(PrintlnSut.PrintlnSutConfiguration.class);
-        Configuration.registerSubtypes(Configuration.NoOpMetricReporterConfiguration.class);
-        Configuration.registerSubtypes(Configuration.RecentPartitionsValidatorConfiguration.class);
     }
     
     public static Configuration configuration(String... args) throws Exception
     {
-        File configFile = harry.runner.HarryRunner.loadConfig(args);
+        File configFile = HarryRunner.loadConfig(args);
         Configuration configuration = Configuration.fromFile(configFile);
         System.out.println("Using configuration generated from: " + configFile);
         return configuration;
@@ -121,10 +120,8 @@ public class HarryHelper
                .setTruncateTable(false)
                .setDropSchema(false)
                .setSchemaProvider((seed, sut) -> schemaSpecGen("harry", "tbl_").inflate(seed))
-               .setClock(new Configuration.ApproximateMonotonicClockConfiguration(7300, 1, TimeUnit.SECONDS))
                .setClusteringDescriptorSelector(defaultClusteringDescriptorSelectorConfiguration().build())
-               .setPartitionDescriptorSelector(new Configuration.DefaultPDSelectorConfiguration(100, 10))
-               .setSUT(new PrintlnSut.PrintlnSutConfiguration())
+               .setPartitionDescriptorSelector(new Configuration.DefaultPDSelectorConfiguration(1, 1))
                .setDataTracker(new Configuration.DefaultDataTrackerConfiguration())
                .setRunner((run, configuration) -> {
                    throw new IllegalArgumentException("Runner is not configured by default.");
@@ -132,11 +129,17 @@ public class HarryHelper
                .setMetricReporter(new Configuration.NoOpMetricReporterConfiguration());
     }
 
+    public static ReplayingHistoryBuilder dataGen(SystemUnderTest sut, TokenPlacementModel.ReplicationFactor rf, SystemUnderTest.ConsistencyLevel writeCl)
+    {
+        long seed = 1L;
+        SchemaSpec schema = schemaSpecGen("harry", "tbl_").inflate(seed);
+        return new ReplayingHistoryBuilder(seed, 100, 1, new DefaultDataTracker(), sut, schema, rf, writeCl);
+    }
+
     public static Configuration.CDSelectorConfigurationBuilder defaultClusteringDescriptorSelectorConfiguration()
     {
         return new Configuration.CDSelectorConfigurationBuilder()
-               .setNumberOfModificationsDistribution(new Configuration.ConstantDistributionConfig(1))
-               .setRowsPerModificationDistribution(new Configuration.ConstantDistributionConfig(1))
+               .setOperationsPerLtsDistribution(new Configuration.ConstantDistributionConfig(1))
                .setMaxPartitionSize(100)
                .setOperationKindWeights(new Configuration.OperationKindSelectorBuilder()
                                         .addWeight(OpSelectors.OperationKind.DELETE_ROW, 1)
@@ -155,8 +158,7 @@ public class HarryHelper
     public static Configuration.CDSelectorConfigurationBuilder singleRowPerModification()
     {
         return new Configuration.CDSelectorConfigurationBuilder()
-               .setNumberOfModificationsDistribution(new Configuration.ConstantDistributionConfig(1))
-               .setRowsPerModificationDistribution(new Configuration.ConstantDistributionConfig(1))
+               .setOperationsPerLtsDistribution(new Configuration.ConstantDistributionConfig(1))
                .setMaxPartitionSize(100)
                .setOperationKindWeights(new Configuration.OperationKindSelectorBuilder()
                                         .addWeight(OpSelectors.OperationKind.INSERT_WITH_STATICS, 100)
diff --git a/test/harry/main/org/apache/cassandra/harry/checker/ModelChecker.java b/test/harry/main/org/apache/cassandra/harry/checker/ModelChecker.java
new file mode 100644
index 0000000000..829075d871
--- /dev/null
+++ b/test/harry/main/org/apache/cassandra/harry/checker/ModelChecker.java
@@ -0,0 +1,230 @@
+/*
+  * 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 org.apache.cassandra.harry.checker;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Random;
+import java.util.function.Consumer;
+
+import org.apache.cassandra.harry.gen.EntropySource;
+import org.apache.cassandra.harry.gen.rng.JdkRandomEntropySource;
+
+public class ModelChecker<STATE>
+{
+    private final List<StepExecutor<STATE>> steps;
+    private final List<Precondition<STATE>> invariants;
+    private Precondition<STATE> exitCondition;
+    private Consumer<STATE> beforeAll;
+    private Consumer<STATE> afterAll;
+    private STATE init;
+
+    public ModelChecker()
+    {
+        steps = new ArrayList<>();
+        invariants = new ArrayList<>();
+    }
+
+    public void run() throws Throwable
+    {
+        run(Integer.MAX_VALUE, System.currentTimeMillis());
+    }
+
+    public void run(int maxSteps, long seed) throws Throwable
+    {
+        assert init != null : "Initial condition is not specified";
+
+        Ref<STATE> state = new Ref<>(init);
+        EntropySource entropySource = new JdkRandomEntropySource(new Random(seed));
+        if (beforeAll != null)
+            beforeAll.accept(state.get());
+
+        for (int i = 0; i < maxSteps; i++)
+        {
+            if (exitCondition != null && exitCondition.test(state.get()))
+                return;
+
+            // TODO: add randomisation / probability for triggering a specific step
+            steps.get(entropySource.nextInt(steps.size())).execute(state, entropySource.derive());
+            for (Precondition<STATE> invariant : invariants)
+                invariant.test(state.get());
+        }
+
+        if (afterAll != null)
+            afterAll.accept(state.get());
+    }
+    public ModelChecker<STATE> init(STATE state)
+    {
+        this.init = state;
+        return this;
+    }
+
+    public ModelChecker<STATE> beforeAll(Consumer<STATE> precondition)
+    {
+        this.beforeAll = precondition;
+        return this;
+    }
+
+    public ModelChecker<STATE> afterAll(Consumer<STATE> postcondition)
+    {
+        this.afterAll = postcondition;
+        return this;
+    }
+
+    public ModelChecker<STATE> exitCondition(Precondition<STATE> precondition)
+    {
+        this.exitCondition = precondition;
+        return this;
+    }
+
+    public ModelChecker<STATE> step(Precondition<STATE> precondition, Step<STATE> step)
+    {
+        steps.add((ref, entropySource) -> {
+            ref.map(state -> {
+                if (!precondition.test(state))
+                    return state;
+
+                return step.next(state, entropySource);
+            });
+        });
+
+        return this;
+    }
+
+    public ModelChecker<STATE> invariant(Precondition<STATE> invariant)
+    {
+        invariants.add(invariant);
+        return this;
+    }
+
+    public ModelChecker<STATE> step(Step<STATE> step)
+    {
+        return step(Precondition.alwaysTrue(), step);
+    }
+
+    public ModelChecker<STATE> step(StatePrecondition<STATE> precondition, ThrowingFunction<STATE, STATE> step)
+    {
+        steps.add((ref, entropySource) -> {
+            ref.map(state -> {
+                if (!precondition.test(state))
+                    return state;
+
+                return step.apply(state);
+            });
+        });
+
+        return this;
+    }
+
+    public ModelChecker<STATE> step(ThrowingConsumer<STATE> step)
+    {
+        return step((t, entropySource) -> {
+            step.consume(t);
+            return t;
+        });
+    }
+    public ModelChecker<STATE> step(ThrowingFunction<STATE, STATE> step)
+    {
+        return step((t, entropySource) -> {
+            return step.apply(t);
+        });
+    }
+
+    interface StepExecutor<STATE>
+    {
+        void execute(Ref<STATE> state, EntropySource entropySource) throws Throwable;
+    }
+
+    public interface StatePrecondition<STATE>
+    {
+        boolean test(STATE state) throws Throwable;
+    }
+
+    public interface Precondition<STATE>
+    {
+        boolean test(STATE state) throws Throwable;
+
+        static <STATE> Precondition<STATE> alwaysTrue()
+        {
+            return (a) -> true;
+        }
+    }
+
+    public interface Step<STATE>
+    {
+        STATE next(STATE t, EntropySource entropySource) throws Throwable;
+    }
+
+    public interface ThrowingConsumer<I>
+    {
+        void consume(I t) throws Throwable;
+    }
+
+    public interface ThrowingFunction<I, O>
+    {
+        O apply(I t) throws Throwable;
+    }
+
+    public interface ThrowingBiFunction<I1, I2, O>
+    {
+        O apply(I1 t1, I2 t2) throws Throwable;
+    }
+
+    public static class Ref<T>
+    {
+        public T ref;
+
+        public Ref(T init)
+        {
+            this.ref = init;
+        }
+
+        public T get()
+        {
+            return ref;
+        }
+
+        public void set(T v)
+        {
+            this.ref = v;
+        }
+
+        public void map(ThrowingFunction<T, T> fn) throws Throwable
+        {
+            this.ref = fn.apply(ref);
+        }
+    }
+
+    public static class State<MODEL, SUT>
+    {
+        public final MODEL model;
+        public final SUT sut;
+
+        public State(MODEL model, SUT sut)
+        {
+            this.model = model;
+            this.sut = sut;
+        }
+
+        public State<MODEL, SUT> next(MODEL state)
+        {
+            return new State<>(state, this.sut);
+        }
+    }
+}
diff --git a/test/harry/main/org/apache/cassandra/harry/clock/ApproximateClock.java b/test/harry/main/org/apache/cassandra/harry/clock/ApproximateClock.java
new file mode 100644
index 0000000000..5bb4aa00ef
--- /dev/null
+++ b/test/harry/main/org/apache/cassandra/harry/clock/ApproximateClock.java
@@ -0,0 +1,279 @@
+/*
+  * 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 org.apache.cassandra.harry.clock;
+
+import java.util.concurrent.CopyOnWriteArrayList;
+import java.util.concurrent.Executors;
+import java.util.concurrent.ScheduledExecutorService;
+import java.util.concurrent.TimeUnit;
+import java.util.concurrent.atomic.AtomicLong;
+import java.util.concurrent.locks.LockSupport;
+
+import com.google.common.annotations.VisibleForTesting;
+
+import org.apache.cassandra.harry.core.Configuration;
+import org.apache.cassandra.harry.model.OpSelectors;
+
+/**
+ * Monotonic clock, that guarantees that any LTS can be converted to a unique RTS, given
+ * the number of LTS does not exceed the number of RTS (e.g., we do not draw LTS more frequently
+ * than once per microsecond).
+ * <p>
+ * This conversion works as follows:
+ * <p>
+ * * every `period`, we record the current timestamp and maximum seen LTS, and keep history of up to
+ * `historySize` LTS/timestamp combinations
+ * * when queried to retrieve the LTS, we find a timestamp, relative to which we can calculate RTS.
+ * After that, we calculate a difference between the largest LTS that is still smaller than the converted
+ * one, and add this difference to the timestamp.
+ * <p>
+ * This way, later LTS can only be mapped to later RTS, and any LTS that was drawn previously, will be
+ * uniquely mapped relative to some timestamp, with the order matching the LTS order.
+ */
+public class ApproximateClock implements OpSelectors.Clock
+{
+    public static final long START_VALUE = 0;
+    public static final long DEFUNCT = Long.MIN_VALUE;
+    public static final long REBASE_IN_PROGRESS = Long.MIN_VALUE + 1;
+
+    // TODO: there's a theoretical possibility of a bug; when we have several consecutive epochs without
+    // change in LTS, current implementation will return the latest epoch instead of the earliest one.
+    // This is not a big deal in terms of monotonicity but can cause some problems when validating TTL.
+    // The simples fix would be to find the smallest matching epoch.
+    private final ScheduledExecutorService executor;
+    private final int historySize;
+    private final CopyOnWriteArrayList<Long> ltsHistory;
+    private final long startTimeMicros;
+    private volatile int idx;
+    private final AtomicLong lts;
+
+    private final long periodMicros;
+
+    private final long epoch;
+    private final TimeUnit epochTimeUnit;
+
+    public ApproximateClock(long period, TimeUnit timeUnit)
+    {
+        this(10000, period, timeUnit);
+    }
+
+    public ApproximateClock(int historySize, long epoch, TimeUnit epochTimeUnit)
+    {
+        this(TimeUnit.MILLISECONDS.toMicros(System.currentTimeMillis()),
+             historySize, new CopyOnWriteArrayList<>(), START_VALUE, 0, epoch, epochTimeUnit);
+        rebase();
+    }
+
+    ApproximateClock(long startTimeMicros,
+                     int historySize,
+                     CopyOnWriteArrayList<Long> history,
+                     long lts,
+                     int idx,
+                     long epoch,
+                     TimeUnit epochTimeUnit)
+    {
+        this.startTimeMicros = startTimeMicros;
+        this.historySize = historySize;
+        this.ltsHistory = history;
+        this.lts = new AtomicLong(lts);
+        this.idx = idx;
+        this.periodMicros = epochTimeUnit.toMicros(epoch);
+        this.executor = Executors.newSingleThreadScheduledExecutor(r -> {
+            Thread t = new Thread(r);
+            t.setName("ApproximateMonotonicClock-ScheduledTasks");
+            t.setDaemon(true);
+            return t;
+        });
+        this.executor.scheduleAtFixedRate(this::rebase, epoch, epoch, epochTimeUnit);
+        this.epoch = epoch;
+        this.epochTimeUnit = epochTimeUnit;
+    }
+
+    @VisibleForTesting
+    public static ApproximateClock forDebug(long startTimeMicros, int historySize, long lts, int idx, long period, TimeUnit timeUnit, long... values)
+    {
+        CopyOnWriteArrayList<Long> history = new CopyOnWriteArrayList<>();
+        for (int i = 0; i < values.length; i++)
+            history.set(i, values[i]);
+
+        assert values.length == idx; // sanity check
+        return new ApproximateClock(startTimeMicros, historySize, history, lts, idx, period, timeUnit);
+    }
+
+    public long get(long idx)
+    {
+        return ltsHistory.get((int) (idx % historySize));
+    }
+
+    private void rebase()
+    {
+        int arrayIdx = idx % historySize;
+        long rebaseLts = lts.get();
+        if (rebaseLts == DEFUNCT)
+            throw new IllegalStateException();
+
+        while (!lts.compareAndSet(rebaseLts, REBASE_IN_PROGRESS))
+            rebaseLts = lts.get();
+
+        ltsHistory.add(arrayIdx, rebaseLts == START_VALUE ? START_VALUE : (rebaseLts + 1));
+
+        // If we happen to exhaust counter, we just need to make operations "wider".
+        // It is unsafe to proceed, so we defunct the clock.
+        //
+        // We could make a clock implementation that would sleep on `get`, but it will
+        // be more expensive, since we'll have to check for overflow each time before
+        // returning anything.
+        if (idx > 1 && get(idx) - get(idx - 1) > periodMicros)
+        {
+            lts.set(DEFUNCT);
+            executor.shutdown();
+            throwCounterExhaustedException();
+        }
+
+        idx = idx + 1;
+        if (!lts.compareAndSet(REBASE_IN_PROGRESS, rebaseLts))
+            throw new IllegalStateException("No thread should have changed LTS during rebase. " + lts.get());
+    }
+
+    @Override
+    public long nextLts()
+    {
+        long current = lts.get();
+        while (true)
+        {
+            if (current >= 0)
+            {
+                if (lts.compareAndSet(current, current + 1))
+                    return current;
+
+                current = lts.get();
+                continue;
+            }
+
+            if (current == REBASE_IN_PROGRESS)
+            {
+                LockSupport.parkNanos(1);
+                current = lts.get();
+                continue;
+            }
+
+            if (current == DEFUNCT)
+                throwCounterExhaustedException();
+
+            throw new IllegalStateException("This should have been unreachable: " + current);
+        }
+    }
+
+    public long peek()
+    {
+        while (true)
+        {
+            long ret = lts.get();
+
+            if (ret == REBASE_IN_PROGRESS)
+            {
+                LockSupport.parkNanos(1);
+                continue;
+            }
+
+            if (ret == DEFUNCT)
+                throwCounterExhaustedException();
+
+            return ret;
+        }
+    }
+
+    public Configuration.ClockConfiguration toConfig()
+    {
+        int idx = this.idx;
+        long[] history = new long[Math.min(idx, historySize)];
+        for (int i = 0; i < history.length; i++)
+            history[i] = ltsHistory.get(i);
+        return new Configuration.DebugApproximateClockConfiguration(startTimeMicros,
+                                                                    ltsHistory.size(),
+                                                                    history,
+                                                                    lts.get(),
+                                                                    idx,
+                                                                    epoch,
+                                                                    epochTimeUnit);
+    }
+
+    public long lts(final long rts)
+    {
+        final int historyIdx = idx - 1;
+        for (int i = 0; i < historySize - 1 && historyIdx - i >= 0; i++)
+        {
+            long periodStartRts = startTimeMicros + periodMicros * (historyIdx - i);
+            if (rts >= periodStartRts)
+            {
+                long periodStartLts = get(historyIdx - i);
+                return periodStartLts + rts - periodStartRts;
+            }
+        }
+        throw new IllegalStateException("RTS is too old to convert to LTS: " + rts + "\n " + ltsHistory);
+    }
+
+    public long rts(final long lts)
+    {
+        assert lts <= peek() : String.format("Queried for LTS we haven't yet issued %d. Max is %d.", lts, peek());
+
+        final int historyIdx = idx - 1;
+        for (int i = 0; i < historySize - 1 && historyIdx - i >= 0; i++)
+        {
+            long periodStartLts = get(historyIdx - i);
+            if (lts >= periodStartLts)
+            {
+                long periodStartRts = startTimeMicros + periodMicros * (historyIdx - i);
+                return periodStartRts + lts - periodStartLts;
+            }
+        }
+
+        throw new IllegalStateException("LTS is too old to convert to RTS: " + lts + "\n " + dumpHistory());
+    }
+
+    private String dumpHistory()
+    {
+        String s = "";
+        int idx = this.idx;
+        for (int i = 0; i < Math.min(idx, historySize); i++)
+        {
+            s += ltsHistory.get(i) + ",";
+        }
... 20485 lines suppressed ...


---------------------------------------------------------------------
To unsubscribe, e-mail: commits-unsubscribe@cassandra.apache.org
For additional commands, e-mail: commits-help@cassandra.apache.org