From 439d1b122af334bf68c159b82ef4e4879c210bd5 Mon Sep 17 00:00:00 2001 From: Alex Petrov Date: Sun, 10 Dec 2023 18:30:26 +0100 Subject: [PATCH] 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 -- ...urrentQuiescentCheckerIntegrationTest.java | 123 -- .../distributed/fuzz/FuzzTestBase.java | 132 -- .../fuzz/InJVMTokenAwareVisitorExecutor.java | 111 -- .../distributed/fuzz/QueryingNoOpChecker.java | 66 - .../distributed/fuzz/SSTableGenerator.java | 363 ----- .../fuzz/SSTableLoadingVisitor.java | 113 -- .../fuzz/test/SSTableGeneratorTest.java | 131 -- .../distributed/shared/ClusterUtils.java | 7 +- .../test/log/BounceGossipTest.java | 41 +- .../distributed/test/log/CMSTestBase.java | 5 +- .../test/log/CoordinatorPathTest.java | 32 +- .../test/log/CoordinatorPathTestBase.java | 26 +- .../distributed/test/log/FailedLeaveTest.java | 38 +- .../distributed/test/log/FuzzTestBase.java | 96 +- .../log/MetadataChangeSimulationTest.java | 86 +- .../distributed/test/log/ModelState.java | 76 +- .../test/log/OperationalEquivalenceTest.java | 43 +- .../test/log/PlacementSimulator.java | 765 +--------- .../test/log/PlacementSimulatorTest.java | 33 +- .../test/log/ResumableStartupTest.java | 47 +- .../test/log/SimulatedOperation.java | 6 +- .../test/log/SystemKeyspaceStorageTest.java | 3 +- .../examples/RangeTombstoneBurnTest.java | 144 ++ .../fuzz/harry/gen/DataGeneratorsTest.java | 504 +++++++ .../fuzz/harry/gen/EntropySourceTest.java | 158 +++ .../harry/gen/ExtensionsTest.java} | 35 +- .../fuzz/harry/gen/SurjectionsTest.java | 63 + .../integration/QuickTheoriesAdapter.java | 100 ++ .../harry/integration/ddl/SchemaGenTest.java | 216 +++ .../dsl/HistoryBuilderIntegrationTest.java | 148 ++ .../DataGeneratorsIntegrationTest.java | 179 +++ ...urrentQuiescentCheckerIntegrationTest.java | 71 + .../model/InJVMTokenAwareExecutorTest.java | 98 ++ .../model/IntegrationTestBase.java | 116 ++ .../harry/integration/model/MockSchema.java | 117 ++ .../integration/model/ModelTestBase.java | 138 ++ .../model/QuerySelectorNegativeTest.java | 157 +++ .../integration/model/QuerySelectorTest.java | 163 +++ .../QuiescentCheckerIntegrationTest.java | 224 +++ ...scentLocalStateCheckerIntegrationTest.java | 79 ++ .../model/ReconcilerIntegrationTest.java | 94 ++ .../reconciler/SimpleReconcilerTest.java | 341 +++++ .../harry/integration/op/RowVisitorTest.java | 90 ++ .../harry/model/ApproximateClockTest.java | 180 +++ .../fuzz/harry/model/OpSelectorsTest.java | 418 ++++++ .../fuzz/harry/operations/RelationTest.java | 204 +++ .../harry/runner/LockingDataTrackerTest.java | 133 ++ .../cassandra/fuzz/harry/util/BitSetTest.java | 66 + .../cassandra/fuzz/harry/util/RangesTest.java | 102 ++ .../ClusterMetadataUpgradeHarryTest.java | 55 +- .../ring/ConsistentBootstrapTest.java | 142 +- .../ring}/ConsistentLeaveTest.java | 63 +- .../log => fuzz/ring}/ConsistentMoveTest.java | 67 +- test/harry/main/README.md | 647 +++++++++ .../apache/cassandra/harry}/HarryHelper.java | 52 +- .../cassandra/harry/checker/ModelChecker.java | 230 +++ .../harry/clock/ApproximateClock.java | 279 ++++ .../cassandra/harry/clock/OffsetClock.java | 94 ++ .../cassandra/harry/core/Configuration.java | 1255 +++++++++++++++++ .../cassandra/harry/core/MetricReporter.java | 51 + .../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 ++ .../corruptor/QueryResponseCorruptor.java | 108 ++ .../harry/corruptor/RowCorruptor.java | 56 + .../harry/corruptor/ShowValueCorruptor.java | 77 + .../cassandra/harry/data/ResultSetRow.java | 137 ++ .../cassandra/harry/ddl/ColumnSpec.java | 438 ++++++ .../cassandra/harry/ddl/SchemaGenerators.java | 512 +++++++ .../cassandra/harry/ddl/SchemaSpec.java | 492 +++++++ .../harry/dsl/BatchOperationBuilder.java | 32 + .../harry/dsl/BatchVisitBuilder.java | 138 ++ .../cassandra/harry/dsl/HistoryBuilder.java | 598 ++++++++ .../harry/dsl/PartitionVisitState.java | 35 + .../harry/dsl/ReplayingHistoryBuilder.java | 117 ++ .../harry/dsl/SingleOperationBuilder.java | 53 + .../dsl/SingleOperationVisitBuilder.java | 295 ++++ .../dsl/ValueDescriptorIndexGenerator.java | 78 + .../cassandra/harry/gen/Bijections.java | 457 ++++++ .../cassandra/harry/gen/BooleanGenerator.java | 29 + .../org/apache/cassandra/harry/gen/Bytes.java | 42 + .../cassandra/harry/gen/Collections.java | 249 ++++ .../cassandra/harry/gen/DataGenerators.java | 499 +++++++ .../cassandra/harry/gen/EntropySource.java | 95 ++ .../apache/cassandra/harry/gen/Generator.java | 133 ++ .../cassandra/harry/gen/Generators.java | 86 ++ .../cassandra/harry/gen/StringBijection.java | 172 +++ .../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 ++ .../cassandra/harry/gen/rng/PcgRSUFast.java | 133 ++ .../cassandra/harry/gen/rng/RngUtils.java | 172 +++ .../harry/model/AgainstSutChecker.java | 99 ++ .../model/AlwaysSamePartitionSelector.java | 92 ++ .../apache/cassandra/harry/model/Model.java | 47 + .../cassandra/harry/model/NoOpChecker.java | 40 + .../cassandra/harry/model/OpSelectors.java | 903 ++++++++++++ .../harry/model/QueryingNoOpValidator.java | 65 + .../harry/model/QuiescentChecker.java | 296 ++++ .../model/QuiescentLocalStateCheckerBase.java | 123 ++ .../model/RepairingLocalStateValidator.java | 92 ++ .../cassandra/harry/model/SelectHelper.java | 337 +++++ .../model/reconciler/PartitionState.java | 311 ++++ .../harry/model/reconciler/Reconciler.java | 331 +++++ .../harry/model/ring/PlacementSimulator.java | 1052 ++++++++++++++ .../harry/operations/CompiledStatement.java | 88 ++ .../harry/operations/DeleteHelper.java | 190 +++ .../harry/operations/FilteringQuery.java | 68 + .../cassandra/harry/operations/Query.java | 501 +++++++ .../harry/operations/QueryGenerator.java | 176 +++ .../cassandra/harry/operations/Relation.java | 299 ++++ .../harry/operations/WriteHelper.java | 166 +++ .../harry/runner/EarlyExitException.java | 27 + .../harry/runner}/FlaggedRunner.java | 19 +- .../cassandra/harry/runner/HarryRunner.java | 115 ++ .../apache/cassandra/harry/runner/Runner.java | 479 +++++++ .../cassandra/harry/runner/StagedRunner.java | 92 ++ .../harry/runner/TrivialShrinker.java | 189 +++ .../cassandra/harry/runner/UpToLtsRunner.java | 111 ++ .../cassandra/harry/sut/DoubleWritingSut.java | 60 + .../cassandra/harry/sut/PrintlnSut.java | 67 + .../harry/sut/QueryModifyingSut.java | 80 ++ .../cassandra/harry/sut/SystemUnderTest.java | 105 ++ .../harry/sut/TokenPlacementModel.java | 891 ++++++++++++ .../harry/sut/injvm}/ClusterState.java | 2 +- .../harry/sut/injvm}/ExistingClusterSUT.java | 6 +- .../injvm/InJVMTokenAwareVisitExecutor.java | 163 +++ .../cassandra/harry/sut/injvm}/InJvmSut.java | 16 +- .../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 ++++ .../apache/cassandra/harry/util/BitSet.java | 244 ++++ .../cassandra/harry/util/ByteUtils.java | 162 +++ .../harry/util/DescriptorRanges.java | 150 ++ .../cassandra/harry/util/StringUtils.java | 35 + .../cassandra/harry/util/TestRunner.java | 106 ++ .../harry/util/ThrowingRunnable.java | 33 + .../cassandra/harry/util/TokenUtil.java | 150 ++ .../visitors/AllPartitionsValidator.java | 150 ++ .../harry/visitors/CorruptingVisitor.java | 89 ++ .../visitors/DoubleWriteVisitExecutor.java | 56 + .../harry/visitors/FaultInjectingVisitor.java | 97 ++ .../harry/visitors/GeneratingVisitor.java | 232 +++ .../harry/visitors/LoggingVisitor.java | 85 ++ .../cassandra/harry/visitors/LtsVisitor.java | 83 ++ .../harry/visitors/MutatingRowVisitor.java | 115 ++ .../harry/visitors/MutatingVisitor.java | 220 +++ .../harry/visitors/OperationExecutor.java | 78 + .../visitors/ParallelRecentValidator.java | 149 ++ .../harry/visitors/ParallelValidator.java | 103 ++ .../cassandra/harry/visitors/QueryLogger.java | 97 ++ .../visitors/RandomPartitionValidator.java | 58 + .../harry/visitors/RandomValidator.java | 93 ++ .../harry/visitors/RecentValidator.java | 96 ++ .../harry/visitors/ReplayingVisitor.java | 73 + .../cassandra/harry/visitors/Sampler.java | 97 ++ .../harry/visitors/SingleValidator.java | 66 + .../harry/visitors/SkippingVisitor.java | 56 + .../harry/visitors/VisitExecutor.java | 122 ++ .../cassandra/harry/visitors/Visitor.java | 33 + .../AlwaysDeliverNetworkScheduler.java | 6 +- .../simulator/cluster/KeyspaceActions.java | 50 +- .../simulator/harry/HarryValidatingQuery.java | 18 +- .../simulator/test/HarrySimulatorTest.java | 57 +- .../test/SimulatedVisitExectuor.java | 35 +- .../cassandra/tcm/ClusterMetadataTest.java | 4 +- .../UniformRangePlacementIntegrationTest.java | 14 +- .../tcm/sequences/ProgressBarrierTest.java | 46 +- 180 files changed, 25070 insertions(+), 2578 deletions(-) delete mode 100644 test/conf/harry-generic.yaml delete mode 100644 test/distributed/org/apache/cassandra/distributed/fuzz/ConcurrentQuiescentCheckerIntegrationTest.java delete mode 100644 test/distributed/org/apache/cassandra/distributed/fuzz/FuzzTestBase.java delete mode 100644 test/distributed/org/apache/cassandra/distributed/fuzz/InJVMTokenAwareVisitorExecutor.java delete mode 100644 test/distributed/org/apache/cassandra/distributed/fuzz/QueryingNoOpChecker.java delete mode 100644 test/distributed/org/apache/cassandra/distributed/fuzz/SSTableGenerator.java delete mode 100644 test/distributed/org/apache/cassandra/distributed/fuzz/SSTableLoadingVisitor.java delete mode 100644 test/distributed/org/apache/cassandra/distributed/fuzz/test/SSTableGeneratorTest.java create mode 100644 test/distributed/org/apache/cassandra/fuzz/harry/examples/RangeTombstoneBurnTest.java create mode 100644 test/distributed/org/apache/cassandra/fuzz/harry/gen/DataGeneratorsTest.java create mode 100644 test/distributed/org/apache/cassandra/fuzz/harry/gen/EntropySourceTest.java rename test/distributed/org/apache/cassandra/{distributed/fuzz/FixedSchemaProviderConfiguration.java => fuzz/harry/gen/ExtensionsTest.java} (55%) create mode 100644 test/distributed/org/apache/cassandra/fuzz/harry/gen/SurjectionsTest.java create mode 100644 test/distributed/org/apache/cassandra/fuzz/harry/integration/QuickTheoriesAdapter.java create mode 100644 test/distributed/org/apache/cassandra/fuzz/harry/integration/ddl/SchemaGenTest.java create mode 100644 test/distributed/org/apache/cassandra/fuzz/harry/integration/dsl/HistoryBuilderIntegrationTest.java create mode 100644 test/distributed/org/apache/cassandra/fuzz/harry/integration/generators/DataGeneratorsIntegrationTest.java create mode 100644 test/distributed/org/apache/cassandra/fuzz/harry/integration/model/ConcurrentQuiescentCheckerIntegrationTest.java create mode 100644 test/distributed/org/apache/cassandra/fuzz/harry/integration/model/InJVMTokenAwareExecutorTest.java create mode 100644 test/distributed/org/apache/cassandra/fuzz/harry/integration/model/IntegrationTestBase.java create mode 100644 test/distributed/org/apache/cassandra/fuzz/harry/integration/model/MockSchema.java create mode 100644 test/distributed/org/apache/cassandra/fuzz/harry/integration/model/ModelTestBase.java create mode 100644 test/distributed/org/apache/cassandra/fuzz/harry/integration/model/QuerySelectorNegativeTest.java create mode 100644 test/distributed/org/apache/cassandra/fuzz/harry/integration/model/QuerySelectorTest.java create mode 100644 test/distributed/org/apache/cassandra/fuzz/harry/integration/model/QuiescentCheckerIntegrationTest.java create mode 100644 test/distributed/org/apache/cassandra/fuzz/harry/integration/model/QuiescentLocalStateCheckerIntegrationTest.java create mode 100644 test/distributed/org/apache/cassandra/fuzz/harry/integration/model/ReconcilerIntegrationTest.java create mode 100644 test/distributed/org/apache/cassandra/fuzz/harry/integration/model/reconciler/SimpleReconcilerTest.java create mode 100644 test/distributed/org/apache/cassandra/fuzz/harry/integration/op/RowVisitorTest.java create mode 100644 test/distributed/org/apache/cassandra/fuzz/harry/model/ApproximateClockTest.java create mode 100644 test/distributed/org/apache/cassandra/fuzz/harry/model/OpSelectorsTest.java create mode 100644 test/distributed/org/apache/cassandra/fuzz/harry/operations/RelationTest.java create mode 100644 test/distributed/org/apache/cassandra/fuzz/harry/runner/LockingDataTrackerTest.java create mode 100644 test/distributed/org/apache/cassandra/fuzz/harry/util/BitSetTest.java create mode 100644 test/distributed/org/apache/cassandra/fuzz/harry/util/RangesTest.java rename test/distributed/org/apache/cassandra/{distributed/upgrade => fuzz/ring}/ClusterMetadataUpgradeHarryTest.java (74%) rename test/distributed/org/apache/cassandra/{distributed/test => fuzz}/ring/ConsistentBootstrapTest.java (68%) rename test/distributed/org/apache/cassandra/{distributed/test/log => fuzz/ring}/ConsistentLeaveTest.java (75%) rename test/distributed/org/apache/cassandra/{distributed/test/log => fuzz/ring}/ConsistentMoveTest.java (74%) create mode 100644 test/harry/main/README.md rename test/{distributed/org/apache/cassandra/distributed/fuzz => harry/main/org/apache/cassandra/harry}/HarryHelper.java (84%) create mode 100644 test/harry/main/org/apache/cassandra/harry/checker/ModelChecker.java create mode 100644 test/harry/main/org/apache/cassandra/harry/clock/ApproximateClock.java create mode 100644 test/harry/main/org/apache/cassandra/harry/clock/OffsetClock.java create mode 100644 test/harry/main/org/apache/cassandra/harry/core/Configuration.java create mode 100644 test/harry/main/org/apache/cassandra/harry/core/MetricReporter.java create mode 100644 test/harry/main/org/apache/cassandra/harry/core/Run.java create mode 100644 test/harry/main/org/apache/cassandra/harry/corruptor/AddExtraRowCorruptor.java create mode 100644 test/harry/main/org/apache/cassandra/harry/corruptor/ChangeValueCorruptor.java create mode 100644 test/harry/main/org/apache/cassandra/harry/corruptor/HideRowCorruptor.java create mode 100644 test/harry/main/org/apache/cassandra/harry/corruptor/HideValueCorruptor.java create mode 100644 test/harry/main/org/apache/cassandra/harry/corruptor/QueryResponseCorruptor.java create mode 100644 test/harry/main/org/apache/cassandra/harry/corruptor/RowCorruptor.java create mode 100644 test/harry/main/org/apache/cassandra/harry/corruptor/ShowValueCorruptor.java create mode 100644 test/harry/main/org/apache/cassandra/harry/data/ResultSetRow.java create mode 100644 test/harry/main/org/apache/cassandra/harry/ddl/ColumnSpec.java create mode 100644 test/harry/main/org/apache/cassandra/harry/ddl/SchemaGenerators.java create mode 100644 test/harry/main/org/apache/cassandra/harry/ddl/SchemaSpec.java create mode 100644 test/harry/main/org/apache/cassandra/harry/dsl/BatchOperationBuilder.java create mode 100644 test/harry/main/org/apache/cassandra/harry/dsl/BatchVisitBuilder.java create mode 100644 test/harry/main/org/apache/cassandra/harry/dsl/HistoryBuilder.java create mode 100644 test/harry/main/org/apache/cassandra/harry/dsl/PartitionVisitState.java create mode 100644 test/harry/main/org/apache/cassandra/harry/dsl/ReplayingHistoryBuilder.java create mode 100644 test/harry/main/org/apache/cassandra/harry/dsl/SingleOperationBuilder.java create mode 100644 test/harry/main/org/apache/cassandra/harry/dsl/SingleOperationVisitBuilder.java create mode 100644 test/harry/main/org/apache/cassandra/harry/dsl/ValueDescriptorIndexGenerator.java create mode 100644 test/harry/main/org/apache/cassandra/harry/gen/Bijections.java create mode 100644 test/harry/main/org/apache/cassandra/harry/gen/BooleanGenerator.java create mode 100644 test/harry/main/org/apache/cassandra/harry/gen/Bytes.java create mode 100644 test/harry/main/org/apache/cassandra/harry/gen/Collections.java create mode 100644 test/harry/main/org/apache/cassandra/harry/gen/DataGenerators.java create mode 100644 test/harry/main/org/apache/cassandra/harry/gen/EntropySource.java create mode 100644 test/harry/main/org/apache/cassandra/harry/gen/Generator.java create mode 100644 test/harry/main/org/apache/cassandra/harry/gen/Generators.java create mode 100644 test/harry/main/org/apache/cassandra/harry/gen/StringBijection.java create mode 100644 test/harry/main/org/apache/cassandra/harry/gen/Surjections.java create mode 100644 test/harry/main/org/apache/cassandra/harry/gen/distribution/Distribution.java create mode 100644 test/harry/main/org/apache/cassandra/harry/gen/rng/JdkRandomEntropySource.java create mode 100644 test/harry/main/org/apache/cassandra/harry/gen/rng/PCGFastPure.java create mode 100644 test/harry/main/org/apache/cassandra/harry/gen/rng/PcgRSUFast.java create mode 100644 test/harry/main/org/apache/cassandra/harry/gen/rng/RngUtils.java create mode 100644 test/harry/main/org/apache/cassandra/harry/model/AgainstSutChecker.java create mode 100644 test/harry/main/org/apache/cassandra/harry/model/AlwaysSamePartitionSelector.java create mode 100644 test/harry/main/org/apache/cassandra/harry/model/Model.java create mode 100644 test/harry/main/org/apache/cassandra/harry/model/NoOpChecker.java create mode 100644 test/harry/main/org/apache/cassandra/harry/model/OpSelectors.java create mode 100644 test/harry/main/org/apache/cassandra/harry/model/QueryingNoOpValidator.java create mode 100644 test/harry/main/org/apache/cassandra/harry/model/QuiescentChecker.java create mode 100644 test/harry/main/org/apache/cassandra/harry/model/QuiescentLocalStateCheckerBase.java create mode 100644 test/harry/main/org/apache/cassandra/harry/model/RepairingLocalStateValidator.java create mode 100644 test/harry/main/org/apache/cassandra/harry/model/SelectHelper.java create mode 100644 test/harry/main/org/apache/cassandra/harry/model/reconciler/PartitionState.java create mode 100644 test/harry/main/org/apache/cassandra/harry/model/reconciler/Reconciler.java create mode 100644 test/harry/main/org/apache/cassandra/harry/model/ring/PlacementSimulator.java create mode 100644 test/harry/main/org/apache/cassandra/harry/operations/CompiledStatement.java create mode 100644 test/harry/main/org/apache/cassandra/harry/operations/DeleteHelper.java create mode 100644 test/harry/main/org/apache/cassandra/harry/operations/FilteringQuery.java create mode 100644 test/harry/main/org/apache/cassandra/harry/operations/Query.java create mode 100644 test/harry/main/org/apache/cassandra/harry/operations/QueryGenerator.java create mode 100644 test/harry/main/org/apache/cassandra/harry/operations/Relation.java create mode 100644 test/harry/main/org/apache/cassandra/harry/operations/WriteHelper.java create mode 100644 test/harry/main/org/apache/cassandra/harry/runner/EarlyExitException.java rename test/{distributed/org/apache/cassandra/distributed/harry => harry/main/org/apache/cassandra/harry/runner}/FlaggedRunner.java (82%) create mode 100644 test/harry/main/org/apache/cassandra/harry/runner/HarryRunner.java create mode 100644 test/harry/main/org/apache/cassandra/harry/runner/Runner.java create mode 100644 test/harry/main/org/apache/cassandra/harry/runner/StagedRunner.java create mode 100644 test/harry/main/org/apache/cassandra/harry/runner/TrivialShrinker.java create mode 100644 test/harry/main/org/apache/cassandra/harry/runner/UpToLtsRunner.java create mode 100644 test/harry/main/org/apache/cassandra/harry/sut/DoubleWritingSut.java create mode 100644 test/harry/main/org/apache/cassandra/harry/sut/PrintlnSut.java create mode 100644 test/harry/main/org/apache/cassandra/harry/sut/QueryModifyingSut.java create mode 100644 test/harry/main/org/apache/cassandra/harry/sut/SystemUnderTest.java create mode 100644 test/harry/main/org/apache/cassandra/harry/sut/TokenPlacementModel.java rename test/{distributed/org/apache/cassandra/distributed/harry => harry/main/org/apache/cassandra/harry/sut/injvm}/ClusterState.java (94%) rename test/{distributed/org/apache/cassandra/distributed/harry => harry/main/org/apache/cassandra/harry/sut/injvm}/ExistingClusterSUT.java (95%) create mode 100644 test/harry/main/org/apache/cassandra/harry/sut/injvm/InJVMTokenAwareVisitExecutor.java rename test/{distributed/org/apache/cassandra/distributed/fuzz => harry/main/org/apache/cassandra/harry/sut/injvm}/InJvmSut.java (91%) rename test/{distributed/org/apache/cassandra/distributed/fuzz => harry/main/org/apache/cassandra/harry/sut/injvm}/InJvmSutBase.java (87%) create mode 100644 test/harry/main/org/apache/cassandra/harry/sut/injvm/QuiescentLocalStateChecker.java create mode 100644 test/harry/main/org/apache/cassandra/harry/tracker/DataTracker.java create mode 100644 test/harry/main/org/apache/cassandra/harry/tracker/DefaultDataTracker.java create mode 100644 test/harry/main/org/apache/cassandra/harry/tracker/LockingDataTracker.java create mode 100644 test/harry/main/org/apache/cassandra/harry/util/BitSet.java create mode 100644 test/harry/main/org/apache/cassandra/harry/util/ByteUtils.java create mode 100644 test/harry/main/org/apache/cassandra/harry/util/DescriptorRanges.java create mode 100644 test/harry/main/org/apache/cassandra/harry/util/StringUtils.java create mode 100644 test/harry/main/org/apache/cassandra/harry/util/TestRunner.java create mode 100644 test/harry/main/org/apache/cassandra/harry/util/ThrowingRunnable.java create mode 100644 test/harry/main/org/apache/cassandra/harry/util/TokenUtil.java create mode 100644 test/harry/main/org/apache/cassandra/harry/visitors/AllPartitionsValidator.java create mode 100644 test/harry/main/org/apache/cassandra/harry/visitors/CorruptingVisitor.java create mode 100644 test/harry/main/org/apache/cassandra/harry/visitors/DoubleWriteVisitExecutor.java create mode 100644 test/harry/main/org/apache/cassandra/harry/visitors/FaultInjectingVisitor.java create mode 100644 test/harry/main/org/apache/cassandra/harry/visitors/GeneratingVisitor.java create mode 100644 test/harry/main/org/apache/cassandra/harry/visitors/LoggingVisitor.java create mode 100644 test/harry/main/org/apache/cassandra/harry/visitors/LtsVisitor.java create mode 100644 test/harry/main/org/apache/cassandra/harry/visitors/MutatingRowVisitor.java create mode 100644 test/harry/main/org/apache/cassandra/harry/visitors/MutatingVisitor.java create mode 100644 test/harry/main/org/apache/cassandra/harry/visitors/OperationExecutor.java create mode 100644 test/harry/main/org/apache/cassandra/harry/visitors/ParallelRecentValidator.java create mode 100644 test/harry/main/org/apache/cassandra/harry/visitors/ParallelValidator.java create mode 100644 test/harry/main/org/apache/cassandra/harry/visitors/QueryLogger.java create mode 100644 test/harry/main/org/apache/cassandra/harry/visitors/RandomPartitionValidator.java create mode 100644 test/harry/main/org/apache/cassandra/harry/visitors/RandomValidator.java create mode 100644 test/harry/main/org/apache/cassandra/harry/visitors/RecentValidator.java create mode 100644 test/harry/main/org/apache/cassandra/harry/visitors/ReplayingVisitor.java create mode 100644 test/harry/main/org/apache/cassandra/harry/visitors/Sampler.java create mode 100644 test/harry/main/org/apache/cassandra/harry/visitors/SingleValidator.java create mode 100644 test/harry/main/org/apache/cassandra/harry/visitors/SkippingVisitor.java create mode 100644 test/harry/main/org/apache/cassandra/harry/visitors/VisitExecutor.java create mode 100644 test/harry/main/org/apache/cassandra/harry/visitors/Visitor.java 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 @@ org.apache.ant ant-junit - - org.apache.cassandra - harry-core - org.junit junit-bom 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 @@ - - org.apache.cassandra - harry-core - 0.0.2 - test - org.reflections reflections diff --git a/build.xml b/build.xml index 162c137984..79fdbda8d9 100644 --- a/build.xml +++ b/build.xml @@ -87,6 +87,7 @@ + @@ -1043,6 +1044,7 @@ + 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 @@ + 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 @@ .. - ${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.dir}/build/lib/jars/asm-9.4.jar:${project.dir}/build/lib/jars/big-math-2.3.0.jar:${project.dir}/build/lib/jars/byteman-4.0.20.jar:${project.dir}/build/lib/jars/byteman-bmunit-4.0.20.jar:${project.dir}/build/lib/jars/byteman-install-4.0.20.jar:${project.dir}/build/lib/jars/byteman-submit-4.0.20.jar:${project.dir}/build/lib/jars/caffeine-2.9.2.jar:${project.dir}/build/lib/jars/cassandra-driver-core-3.11.0-shaded.jar:${project.dir}/build/lib/jars/checker-qual-3.10.0.jar:${project.dir}/build/lib/jars/chronicle-bytes-2.23.33.jar:${project.dir}/build/lib/jars/chronicle-core-2.23.36.jar:${project.dir}/build/lib/jars/chronicle-queue-5.23.37.jar:${project.dir}/build/lib/jars/chronicle-threads-2.23.25.jar:${project.dir}/build/lib/jars/chronicle-wire-2.23.39.jar:${project.dir}/build/lib/jars/commons-cli-1.5.0.jar:${project.dir}/build/lib/jars/commons-codec-1.9.jar:${project.dir}/build/lib/jars/commons-lang3-3.11.jar:${project.dir}/build/lib/jars/commons-math3-3.2.jar:${project.dir}/build/lib/jars/compile-command-annotations-1.2.0.jar:${project.dir}/build/lib/jars/compress-lzf-0.8.4.jar:${project.dir}/build/lib/jars/concurrent-trees-2.4.0.jar:${project.dir}/build/lib/jars/ecj-3.33.0.jar:${project.dir}/build/lib/jars/error_prone_annotations-2.5.1.jar:${project.dir}/build/lib/jars/failureaccess-1.0.1.jar:${project.dir}/build/lib/jars/guava-32.0.1-jre.jar:${project.dir}/build/lib/jars/high-scale-lib-1.0.6.jar:${project.dir}/build/lib/jars/hppc-0.8.1.jar:${project.dir}/build/lib/jars/ipaddress-5.3.3.jar:${project.dir}/build/lib/jars/j2objc-annotations-1.3.jar:${project.dir}/build/lib/jars/jackson-annotations-2.13.2.jar:${project.dir}/build/lib/jars/jackson-core-2.13.2.jar:${project.dir}/build/lib/jars/jackson-databind-2.13.2.2.jar:${project.dir}/build/lib/jars/jackson-datatype-jsr310-2.13.2.jar:${project.dir}/build/lib/jars/jacocoagent.jar:${project.dir}/build/lib/jars/jamm-0.4.0.jar:${project.dir}/build/lib/jars/javax.inject-1.jar:${project.dir}/build/lib/jars/jbcrypt-0.4.jar:${project.dir}/build/lib/jars/jcl-over-slf4j-1.7.25.jar:${project.dir}/build/lib/jars/jcommander-1.30.jar:${project.dir}/build/lib/jars/jctools-core-3.1.0.jar:${project.dir}/build/lib/jars/jffi-1.3.11-native.jar:${project.dir}/build/lib/jars/jffi-1.3.11.jar:${project.dir}/build/lib/jars/jna-5.13.0.jar:${project.dir}/build/lib/jars/jna-platform-5.13.0.jar:${project.dir}/build/lib/jars/jnr-a64asm-1.0.0.jar:${project.dir}/build/lib/jars/jnr-constants-0.10.4.jar:${project.dir}/build/lib/jars/jnr-ffi-2.2.13.jar:${project.dir}/build/lib/jars/jnr-x86asm-1.0.2.jar:${project.dir}/build/lib/jars/jsr305-2.0.2.jar:${project.dir}/build/lib/jars/jvm-attach-api-1.5.jar:${project.dir}/build/lib/jars/log4j-over-slf4j-1.7.25.jar:${project.dir}/build/lib/jars/logback-classic-1.2.9.jar:${project.dir}/build/lib/jars/logback-core-1.2.9.jar:${project.dir}/build/lib/jars/lucene-analysis-common-9.7.0.jar:${project.dir}/build/lib/jars/lucene-core-9.7.0.jar:${project.dir}/build/lib/jars/lz4-java-1.8.0.jar:${project.dir}/build/lib/jars/metrics-core-3.1.5.jar:${project.dir}/build/lib/jars/metrics-jvm-3.1.5.jar:${project.dir}/build/lib/jars/metrics-logback-3.1.5.jar:${project.dir}/build/lib/jars/mxdump-0.14.jar:${project.dir}/build/lib/jars/netty-all-4.1.58.Final.jar:${project.dir}/build/lib/jars/netty-tcnative-boringssl-static-2.0.36.Final.jar:${project.dir}/build/lib/jars/ohc-core-0.5.1.jar:${project.dir}/build/lib/jars/ohc-core-j8-0.5.1.jar:${project.dir}/build/lib/jars/posix-2.24ea4.jar:${project.dir}/build/lib/jars/psjava-0.1.19.jar:${project.dir}/build/lib/jars/reporter-config-base-3.0.3.jar:${project.dir}/build/lib/jars/reporter-config3-3.0.3.jar:${project.dir}/build/lib/jars/sigar-1.6.4.jar:${project.dir}/build/lib/jars/sjk-cli-0.14.jar:${project.dir}/build/lib/jars/sjk-core-0.14.jar:${project.dir}/build/lib/jars/sjk-json-0.14.jar:${project.dir}/build/lib/jars/sjk-stacktrace-0.14.jar:${project.dir}/build/lib/jars/slf4j-api-1.7.25.jar:${project.dir}/build/lib/jars/snakeyaml-1.26.jar:${project.dir}/build/lib/jars/snappy-java-1.1.10.1.jar:${project.dir}/build/lib/jars/stream-2.5.2.jar:${project.dir}/build/lib/jars/zstd-jni-1.5.5-1.jar:${project.dir}/build/test/lib/jars/AmazonCorrettoCryptoProvider-2.2.0-linux-x86_64.jar:${project.dir}/build/test/lib/jars/Saxon-HE-12.2.jar:${project.dir}/build/test/lib/jars/accessors-smart-2.4.7.jar:${project.dir}/build/test/lib/jars/ant-1.10.12.jar:${project.dir}/build/test/lib/jars/ant-junit-1.10.12.jar:${project.dir}/build/test/lib/jars/ant-launcher-1.10.12.jar:${project.dir}/build/test/lib/jars/antlr4-runtime-4.11.1.jar:${project.dir}/build/test/lib/jars/asm-9.4.jar:${project.dir}/build/test/lib/jars/asm-analysis-9.4.jar:${project.dir}/build/test/lib/jars/asm-commons-9.4.jar:${project.dir}/build/test/lib/jars/asm-tree-9.4.jar:${project.dir}/build/test/lib/jars/asm-util-9.4.jar:${project.dir}/build/test/lib/jars/asm-xml-6.0.jar:${project.dir}/build/test/lib/jars/assertj-core-3.15.0.jar:${project.dir}/build/test/lib/jars/awaitility-4.0.3.jar:${project.dir}/build/test/lib/jars/byte-buddy-1.12.13.jar:${project.dir}/build/test/lib/jars/byte-buddy-agent-1.12.13.jar:${project.dir}/build/test/lib/jars/checker-qual-3.27.0.jar:${project.dir}/build/test/lib/jars/checkstyle-10.12.1.jar:${project.dir}/build/test/lib/jars/commons-beanutils-1.9.4.jar:${project.dir}/build/test/lib/jars/commons-codec-1.15.jar:${project.dir}/build/test/lib/jars/commons-collections-3.2.2.jar:${project.dir}/build/test/lib/jars/commons-fileupload-1.4.jar:${project.dir}/build/test/lib/jars/commons-io-2.11.0.jar:${project.dir}/build/test/lib/jars/commons-lang3-3.12.0.jar:${project.dir}/build/test/lib/jars/commons-logging-1.2.jar:${project.dir}/build/test/lib/jars/commons-math3-3.2.jar:${project.dir}/build/test/lib/jars/dtest-api-0.0.15.jar:${project.dir}/build/test/lib/jars/guava-18.0.jar:${project.dir}/build/test/lib/jars/hamcrest-2.2.jar:${project.dir}/build/test/lib/jars/hamcrest-core-2.2.jar:${project.dir}/build/test/lib/jars/handlebars-4.3.1.jar:${project.dir}/build/test/lib/jars/handlebars-helpers-4.3.1.jar:${project.dir}/build/test/lib/jars/harry-core-0.0.1.jar:${project.dir}/build/test/lib/jars/http2-common-9.4.49.v20220914.jar:${project.dir}/build/test/lib/jars/http2-hpack-9.4.49.v20220914.jar:${project.dir}/build/test/lib/jars/http2-server-9.4.49.v20220914.jar:${project.dir}/build/test/lib/jars/httpclient5-5.1.3.jar:${project.dir}/build/test/lib/jars/httpcore5-5.1.3.jar:${project.dir}/build/test/lib/jars/httpcore5-h2-5.1.3.jar:${project.dir}/build/test/lib/jars/jackson-annotations-2.11.3.jar:${project.dir}/build/test/lib/jars/jackson-core-2.13.2.jar:${project.dir}/build/test/lib/jars/jackson-databind-2.11.3.jar:${project.dir}/build/test/lib/jars/jackson-dataformat-yaml-2.13.2.jar:${project.dir}/build/test/lib/jars/jakarta.activation-api-1.2.2.jar:${project.dir}/build/test/lib/jars/jakarta.xml.bind-api-2.3.3.jar:${project.dir}/build/test/lib/jars/java-allocation-instrumenter-3.1.0.jar:${project.dir}/build/test/lib/jars/java-cup-runtime-11b-20160615.jar:${project.dir}/build/test/lib/jars/javassist-3.28.0-GA.jar:${project.dir}/build/test/lib/jars/javax.servlet-api-3.1.0.jar:${project.dir}/build/test/lib/jars/jetty-alpn-client-9.4.49.v20220914.jar:${project.dir}/build/test/lib/jars/jetty-alpn-java-client-9.4.49.v20220914.jar:${project.dir}/build/test/lib/jars/jetty-alpn-java-server-9.4.49.v20220914.jar:${project.dir}/build/test/lib/jars/jetty-alpn-openjdk8-client-9.4.49.v20220914.jar:${project.dir}/build/test/lib/jars/jetty-alpn-openjdk8-server-9.4.49.v20220914.jar:${project.dir}/build/test/lib/jars/jetty-alpn-server-9.4.49.v20220914.jar:${project.dir}/build/test/lib/jars/jetty-client-9.4.49.v20220914.jar:${project.dir}/build/test/lib/jars/jetty-continuation-9.4.49.v20220914.jar:${project.dir}/build/test/lib/jars/jetty-http-9.4.49.v20220914.jar:${project.dir}/build/test/lib/jars/jetty-io-9.4.49.v20220914.jar:${project.dir}/build/test/lib/jars/jetty-proxy-9.4.49.v20220914.jar:${project.dir}/build/test/lib/jars/jetty-security-9.4.49.v20220914.jar:${project.dir}/build/test/lib/jars/jetty-server-9.4.49.v20220914.jar:${project.dir}/build/test/lib/jars/jetty-servlet-9.4.49.v20220914.jar:${project.dir}/build/test/lib/jars/jetty-servlets-9.4.49.v20220914.jar:${project.dir}/build/test/lib/jars/jetty-util-9.4.49.v20220914.jar:${project.dir}/build/test/lib/jars/jetty-util-ajax-9.4.49.v20220914.jar:${project.dir}/build/test/lib/jars/jetty-webapp-9.4.49.v20220914.jar:${project.dir}/build/test/lib/jars/jetty-xml-9.4.49.v20220914.jar:${project.dir}/build/test/lib/jars/jflex-1.8.2.jar:${project.dir}/build/test/lib/jars/jimfs-1.1.jar:${project.dir}/build/test/lib/jars/jmh-core-1.21.jar:${project.dir}/build/test/lib/jars/jmh-generator-annprocess-1.21.jar:${project.dir}/build/test/lib/jars/jopt-simple-4.6.jar:${project.dir}/build/test/lib/jars/json-path-2.7.0.jar:${project.dir}/build/test/lib/jars/json-smart-2.4.7.jar:${project.dir}/build/test/lib/jars/json-unit-core-2.36.0.jar:${project.dir}/build/test/lib/jars/jsr305-3.0.2.jar:${project.dir}/build/test/lib/jars/junit-4.12.jar:${project.dir}/build/test/lib/jars/mockito-core-4.7.0.jar:${project.dir}/build/test/lib/jars/mockito-inline-4.7.0.jar:${project.dir}/build/test/lib/jars/objenesis-3.2.jar:${project.dir}/build/test/lib/jars/org.jacoco.agent-0.8.8.jar:${project.dir}/build/test/lib/jars/org.jacoco.ant-0.8.8.jar:${project.dir}/build/test/lib/jars/org.jacoco.core-0.8.8.jar:${project.dir}/build/test/lib/jars/org.jacoco.report-0.8.8.jar:${project.dir}/build/test/lib/jars/picocli-4.7.4.jar:${project.dir}/build/test/lib/jars/quicktheories-0.26.jar:${project.dir}/build/test/lib/jars/randomizedtesting-runner-2.1.2.jar:${project.dir}/build/test/lib/jars/reflections-0.10.2.jar:${project.dir}/build/test/lib/jars/semver4j-3.1.0.jar:${project.dir}/build/test/lib/jars/simulator-asm.jar:${project.dir}/build/test/lib/jars/simulator-bootstrap.jar:${project.dir}/build/test/lib/jars/slf4j-api-1.7.32.jar:${project.dir}/build/test/lib/jars/wiremock-jre8-2.35.0.jar:${project.dir}/build/test/lib/jars/xmlresolver-5.1.2-data.jar:${project.dir}/build/test/lib/jars/xmlresolver-5.1.2.jar:${project.dir}/build/test/lib/jars/xmlunit-core-2.9.0.jar:${project.dir}/build/test/lib/jars/xmlunit-legacy-2.9.0.jar:${project.dir}/build/test/lib/jars/xmlunit-placeholders-2.9.0.jar: + ${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.dir}/build/lib/jars/asm-9.4.jar:${project.dir}/build/lib/jars/big-math-2.3.0.jar:${project.dir}/build/lib/jars/byteman-4.0.20.jar:${project.dir}/build/lib/jars/byteman-bmunit-4.0.20.jar:${project.dir}/build/lib/jars/byteman-install-4.0.20.jar:${project.dir}/build/lib/jars/byteman-submit-4.0.20.jar:${project.dir}/build/lib/jars/caffeine-2.9.2.jar:${project.dir}/build/lib/jars/cassandra-driver-core-3.11.0-shaded.jar:${project.dir}/build/lib/jars/checker-qual-3.10.0.jar:${project.dir}/build/lib/jars/chronicle-bytes-2.23.33.jar:${project.dir}/build/lib/jars/chronicle-core-2.23.36.jar:${project.dir}/build/lib/jars/chronicle-queue-5.23.37.jar:${project.dir}/build/lib/jars/chronicle-threads-2.23.25.jar:${project.dir}/build/lib/jars/chronicle-wire-2.23.39.jar:${project.dir}/build/lib/jars/commons-cli-1.5.0.jar:${project.dir}/build/lib/jars/commons-codec-1.9.jar:${project.dir}/build/lib/jars/commons-lang3-3.11.jar:${project.dir}/build/lib/jars/commons-math3-3.2.jar:${project.dir}/build/lib/jars/compile-command-annotations-1.2.0.jar:${project.dir}/build/lib/jars/compress-lzf-0.8.4.jar:${project.dir}/build/lib/jars/concurrent-trees-2.4.0.jar:${project.dir}/build/lib/jars/ecj-3.33.0.jar:${project.dir}/build/lib/jars/error_prone_annotations-2.5.1.jar:${project.dir}/build/lib/jars/failureaccess-1.0.1.jar:${project.dir}/build/lib/jars/guava-32.0.1-jre.jar:${project.dir}/build/lib/jars/high-scale-lib-1.0.6.jar:${project.dir}/build/lib/jars/hppc-0.8.1.jar:${project.dir}/build/lib/jars/ipaddress-5.3.3.jar:${project.dir}/build/lib/jars/j2objc-annotations-1.3.jar:${project.dir}/build/lib/jars/jackson-annotations-2.13.2.jar:${project.dir}/build/lib/jars/jackson-core-2.13.2.jar:${project.dir}/build/lib/jars/jackson-databind-2.13.2.2.jar:${project.dir}/build/lib/jars/jackson-datatype-jsr310-2.13.2.jar:${project.dir}/build/lib/jars/jacocoagent.jar:${project.dir}/build/lib/jars/jamm-0.4.0.jar:${project.dir}/build/lib/jars/javax.inject-1.jar:${project.dir}/build/lib/jars/jbcrypt-0.4.jar:${project.dir}/build/lib/jars/jcl-over-slf4j-1.7.25.jar:${project.dir}/build/lib/jars/jcommander-1.30.jar:${project.dir}/build/lib/jars/jctools-core-3.1.0.jar:${project.dir}/build/lib/jars/jffi-1.3.11-native.jar:${project.dir}/build/lib/jars/jffi-1.3.11.jar:${project.dir}/build/lib/jars/jna-5.13.0.jar:${project.dir}/build/lib/jars/jna-platform-5.13.0.jar:${project.dir}/build/lib/jars/jnr-a64asm-1.0.0.jar:${project.dir}/build/lib/jars/jnr-constants-0.10.4.jar:${project.dir}/build/lib/jars/jnr-ffi-2.2.13.jar:${project.dir}/build/lib/jars/jnr-x86asm-1.0.2.jar:${project.dir}/build/lib/jars/jsr305-2.0.2.jar:${project.dir}/build/lib/jars/jvm-attach-api-1.5.jar:${project.dir}/build/lib/jars/log4j-over-slf4j-1.7.25.jar:${project.dir}/build/lib/jars/logback-classic-1.2.9.jar:${project.dir}/build/lib/jars/logback-core-1.2.9.jar:${project.dir}/build/lib/jars/lucene-analysis-common-9.7.0.jar:${project.dir}/build/lib/jars/lucene-core-9.7.0.jar:${project.dir}/build/lib/jars/lz4-java-1.8.0.jar:${project.dir}/build/lib/jars/metrics-core-3.1.5.jar:${project.dir}/build/lib/jars/metrics-jvm-3.1.5.jar:${project.dir}/build/lib/jars/metrics-logback-3.1.5.jar:${project.dir}/build/lib/jars/mxdump-0.14.jar:${project.dir}/build/lib/jars/netty-all-4.1.58.Final.jar:${project.dir}/build/lib/jars/netty-tcnative-boringssl-static-2.0.36.Final.jar:${project.dir}/build/lib/jars/ohc-core-0.5.1.jar:${project.dir}/build/lib/jars/ohc-core-j8-0.5.1.jar:${project.dir}/build/lib/jars/posix-2.24ea4.jar:${project.dir}/build/lib/jars/psjava-0.1.19.jar:${project.dir}/build/lib/jars/reporter-config-base-3.0.3.jar:${project.dir}/build/lib/jars/reporter-config3-3.0.3.jar:${project.dir}/build/lib/jars/sigar-1.6.4.jar:${project.dir}/build/lib/jars/sjk-cli-0.14.jar:${project.dir}/build/lib/jars/sjk-core-0.14.jar:${project.dir}/build/lib/jars/sjk-json-0.14.jar:${project.dir}/build/lib/jars/sjk-stacktrace-0.14.jar:${project.dir}/build/lib/jars/slf4j-api-1.7.25.jar:${project.dir}/build/lib/jars/snakeyaml-1.26.jar:${project.dir}/build/lib/jars/snappy-java-1.1.10.1.jar:${project.dir}/build/lib/jars/stream-2.5.2.jar:${project.dir}/build/lib/jars/zstd-jni-1.5.5-1.jar:${project.dir}/build/test/lib/jars/AmazonCorrettoCryptoProvider-2.2.0-linux-x86_64.jar:${project.dir}/build/test/lib/jars/Saxon-HE-12.2.jar:${project.dir}/build/test/lib/jars/accessors-smart-2.4.7.jar:${project.dir}/build/test/lib/jars/ant-1.10.12.jar:${project.dir}/build/test/lib/jars/ant-junit-1.10.12.jar:${project.dir}/build/test/lib/jars/ant-launcher-1.10.12.jar:${project.dir}/build/test/lib/jars/antlr4-runtime-4.11.1.jar:${project.dir}/build/test/lib/jars/asm-9.4.jar:${project.dir}/build/test/lib/jars/asm-analysis-9.4.jar:${project.dir}/build/test/lib/jars/asm-commons-9.4.jar:${project.dir}/build/test/lib/jars/asm-tree-9.4.jar:${project.dir}/build/test/lib/jars/asm-util-9.4.jar:${project.dir}/build/test/lib/jars/asm-xml-6.0.jar:${project.dir}/build/test/lib/jars/assertj-core-3.15.0.jar:${project.dir}/build/test/lib/jars/awaitility-4.0.3.jar:${project.dir}/build/test/lib/jars/byte-buddy-1.12.13.jar:${project.dir}/build/test/lib/jars/byte-buddy-agent-1.12.13.jar:${project.dir}/build/test/lib/jars/checker-qual-3.27.0.jar:${project.dir}/build/test/lib/jars/checkstyle-10.12.1.jar:${project.dir}/build/test/lib/jars/commons-beanutils-1.9.4.jar:${project.dir}/build/test/lib/jars/commons-codec-1.15.jar:${project.dir}/build/test/lib/jars/commons-collections-3.2.2.jar:${project.dir}/build/test/lib/jars/commons-fileupload-1.4.jar:${project.dir}/build/test/lib/jars/commons-io-2.11.0.jar:${project.dir}/build/test/lib/jars/commons-lang3-3.12.0.jar:${project.dir}/build/test/lib/jars/commons-logging-1.2.jar:${project.dir}/build/test/lib/jars/commons-math3-3.2.jar:${project.dir}/build/test/lib/jars/dtest-api-0.0.15.jar:${project.dir}/build/test/lib/jars/guava-18.0.jar:${project.dir}/build/test/lib/jars/hamcrest-2.2.jar:${project.dir}/build/test/lib/jars/hamcrest-core-2.2.jar:${project.dir}/build/test/lib/jars/handlebars-4.3.1.jar:${project.dir}/build/test/lib/jars/handlebars-helpers-4.3.1.jar:${project.dir}/build/test/lib/jars/http2-common-9.4.49.v20220914.jar:${project.dir}/build/test/lib/jars/http2-hpack-9.4.49.v20220914.jar:${project.dir}/build/test/lib/jars/http2-server-9.4.49.v20220914.jar:${project.dir}/build/test/lib/jars/httpclient5-5.1.3.jar:${project.dir}/build/test/lib/jars/httpcore5-5.1.3.jar:${project.dir}/build/test/lib/jars/httpcore5-h2-5.1.3.jar:${project.dir}/build/test/lib/jars/jackson-annotations-2.11.3.jar:${project.dir}/build/test/lib/jars/jackson-core-2.13.2.jar:${project.dir}/build/test/lib/jars/jackson-databind-2.11.3.jar:${project.dir}/build/test/lib/jars/jackson-dataformat-yaml-2.13.2.jar:${project.dir}/build/test/lib/jars/jakarta.activation-api-1.2.2.jar:${project.dir}/build/test/lib/jars/jakarta.xml.bind-api-2.3.3.jar:${project.dir}/build/test/lib/jars/java-allocation-instrumenter-3.1.0.jar:${project.dir}/build/test/lib/jars/java-cup-runtime-11b-20160615.jar:${project.dir}/build/test/lib/jars/javassist-3.28.0-GA.jar:${project.dir}/build/test/lib/jars/javax.servlet-api-3.1.0.jar:${project.dir}/build/test/lib/jars/jetty-alpn-client-9.4.49.v20220914.jar:${project.dir}/build/test/lib/jars/jetty-alpn-java-client-9.4.49.v20220914.jar:${project.dir}/build/test/lib/jars/jetty-alpn-java-server-9.4.49.v20220914.jar:${project.dir}/build/test/lib/jars/jetty-alpn-openjdk8-client-9.4.49.v20220914.jar:${project.dir}/build/test/lib/jars/jetty-alpn-openjdk8-server-9.4.49.v20220914.jar:${project.dir}/build/test/lib/jars/jetty-alpn-server-9.4.49.v20220914.jar:${project.dir}/build/test/lib/jars/jetty-client-9.4.49.v20220914.jar:${project.dir}/build/test/lib/jars/jetty-continuation-9.4.49.v20220914.jar:${project.dir}/build/test/lib/jars/jetty-http-9.4.49.v20220914.jar:${project.dir}/build/test/lib/jars/jetty-io-9.4.49.v20220914.jar:${project.dir}/build/test/lib/jars/jetty-proxy-9.4.49.v20220914.jar:${project.dir}/build/test/lib/jars/jetty-security-9.4.49.v20220914.jar:${project.dir}/build/test/lib/jars/jetty-server-9.4.49.v20220914.jar:${project.dir}/build/test/lib/jars/jetty-servlet-9.4.49.v20220914.jar:${project.dir}/build/test/lib/jars/jetty-servlets-9.4.49.v20220914.jar:${project.dir}/build/test/lib/jars/jetty-util-9.4.49.v20220914.jar:${project.dir}/build/test/lib/jars/jetty-util-ajax-9.4.49.v20220914.jar:${project.dir}/build/test/lib/jars/jetty-webapp-9.4.49.v20220914.jar:${project.dir}/build/test/lib/jars/jetty-xml-9.4.49.v20220914.jar:${project.dir}/build/test/lib/jars/jflex-1.8.2.jar:${project.dir}/build/test/lib/jars/jimfs-1.1.jar:${project.dir}/build/test/lib/jars/jmh-core-1.21.jar:${project.dir}/build/test/lib/jars/jmh-generator-annprocess-1.21.jar:${project.dir}/build/test/lib/jars/jopt-simple-4.6.jar:${project.dir}/build/test/lib/jars/json-path-2.7.0.jar:${project.dir}/build/test/lib/jars/json-smart-2.4.7.jar:${project.dir}/build/test/lib/jars/json-unit-core-2.36.0.jar:${project.dir}/build/test/lib/jars/jsr305-3.0.2.jar:${project.dir}/build/test/lib/jars/junit-4.12.jar:${project.dir}/build/test/lib/jars/mockito-core-4.7.0.jar:${project.dir}/build/test/lib/jars/mockito-inline-4.7.0.jar:${project.dir}/build/test/lib/jars/objenesis-3.2.jar:${project.dir}/build/test/lib/jars/org.jacoco.agent-0.8.8.jar:${project.dir}/build/test/lib/jars/org.jacoco.ant-0.8.8.jar:${project.dir}/build/test/lib/jars/org.jacoco.core-0.8.8.jar:${project.dir}/build/test/lib/jars/org.jacoco.report-0.8.8.jar:${project.dir}/build/test/lib/jars/picocli-4.7.4.jar:${project.dir}/build/test/lib/jars/quicktheories-0.26.jar:${project.dir}/build/test/lib/jars/randomizedtesting-runner-2.1.2.jar:${project.dir}/build/test/lib/jars/reflections-0.10.2.jar:${project.dir}/build/test/lib/jars/semver4j-3.1.0.jar:${project.dir}/build/test/lib/jars/simulator-asm.jar:${project.dir}/build/test/lib/jars/simulator-bootstrap.jar:${project.dir}/build/test/lib/jars/slf4j-api-1.7.32.jar:${project.dir}/build/test/lib/jars/wiremock-jre8-2.35.0.jar:${project.dir}/build/test/lib/jars/xmlresolver-5.1.2-data.jar:${project.dir}/build/test/lib/jars/xmlresolver-5.1.2.jar:${project.dir}/build/test/lib/jars/xmlunit-core-2.9.0.jar:${project.dir}/build/test/lib/jars/xmlunit-legacy-2.9.0.jar:${project.dir}/build/test/lib/jars/xmlunit-placeholders-2.9.0.jar: @@ -112,6 +112,12 @@ ${project.dir}/test/simulator/test UTF-8 + + + java + ${project.dir}/test/harry/main + UTF-8 + ${project.dir} @@ -259,6 +265,10 @@ ${project.dir}/test/simulator/test + + + ${project.dir}/test/harry/main + ${project.dir}/build.xml @@ -300,6 +310,7 @@ ${project.dir}/test/simulator/bootstrap ${project.dir}/test/simulator/main ${project.dir}/test/simulator/test + ${project.dir}/test/harry/main ${cassandra.classpath.jars}:${project.dir}/build/classes/main:${project.dir}/build/classes/fqltool/:${project.dir}/build/classes/stress/ ${project.dir}/build/test/classes 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 schemaSupplier() - { - Surjections.Surjection schemaGen = HarryHelper.schemaSpecGen("harry", "table_"); - return new Supplier() - { - 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 cluster = builder().withNodes(3) - .withConfig(config -> config.with(GOSSIP, NETWORK, NATIVE_PROTOCOL)) - .start()) - { - Supplier 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 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 future, CompiledStatement statement) - { - executeAsyncWithRetries(lts, pd, future, statement, 0); - } - - private void executeAsyncWithRetries(long lts, long pd, CompletableFuture 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 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 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 flush() - { - Util.flush(store); - sstables.removeAll(store.getLiveSSTables()); - - Set 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 variableNames = new ArrayList<>(); - - List 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> startBounds = restrictions.getClusteringColumnsBounds(Bound.START, options); - SortedSet> 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 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 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 processorFactory, boolean addListeners, PlacementSimulator.ReplicationFactor rf) + public CMSSut(IIsolatedExecutor.SerializableFunction 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 test) throws Throwable + public void coordinatorPathTest(TokenPlacementModel.ReplicationFactor rf, TestRunner.ThrowingBiConsumer 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 test, boolean startCluster) throws Throwable + public void coordinatorPathTest(TokenPlacementModel.ReplicationFactor rf, TestRunner.ThrowingBiConsumer 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 realCluster, TokenSupplier tokenSupplier) + public SimulatedCluster(TokenPlacementModel.ReplicationFactor rf, ICluster realCluster, TokenSupplier tokenSupplier) { this.tokenSupplier = tokenSupplier; @@ -845,9 +847,9 @@ public abstract class CoordinatorPathTestBase extends FuzzTestBase { protected final SimulatedPlacementHolder state; protected final List 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 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 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 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 e : rf.asMap().entrySet()) { @@ -553,16 +553,16 @@ public class MetadataChangeSimulationTest extends CMSTestBase return sb.toString(); } - public static void match(PlacementForRange actual, Map> predicted) throws Throwable + public static void match(PlacementForRange actual, Map> predicted) throws Throwable { Map, 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> entry : predicted.entrySet()) + for (Map.Entry> entry : predicted.entrySet()) { - PlacementSimulator.Range range = entry.getKey(); + TokenPlacementModel.Range range = entry.getKey(); List predictedNodes = entry.getValue(); Range predictedRange = new Range(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 opStates, List> expectedRanges, PlacementForRange placements, boolean allowPending) + public static void validatePlacementsInternal(ReplicationFactor rf, List opStates, List> expectedRanges, PlacementForRange placements, boolean allowPending) { int overreplicated = 0; for (Range 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 currentNodes; - public final Map> nodesByDc; - public final List registeredNodes; - public final List leavingNodes; - public final List movingNodes; + public final List currentNodes; + public final Map> nodesByDc; + public final List registeredNodes; + public final List leavingNodes; + public final List movingNodes; public final List 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> groupByDc(List nodes) + public static Map> groupByDc(List nodes) { // using treemap here since it is much easier to read/debug when it comes to that - Map> grouped = new TreeMap<>(); - for (PlacementSimulator.Node node : nodes) + Map> 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 currentNodes, - List registeredNodes, - List leavingNodes, - List movingNodes, + List currentNodes, + List registeredNodes, + List leavingNodes, + List movingNodes, List 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 e : rfs.asMap().entrySet()) { String dc = e.getKey(); int rf = e.getValue(); - List nodes = nodesByDc.get(dc); - Set nodesInDc = nodes == null ? new HashSet<>() : new HashSet<>(nodes); + List nodes = nodesByDc.get(dc); + Set 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 currentNodes; - private List registeredNodes; - private List leavingNodes; - private List movingNodes; + private List currentNodes; + private List registeredNodes; + private List leavingNodes; + private List movingNodes; private List 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 tmp = currentNodes; + List 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 nodes) + public TokenPlacementModel.ReplicatedRanges replicate(TokenPlacementModel.Range[] ranges, List 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 equivalentNodes = new ArrayList<>(); + List 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 nodes) throws Exception + private static ClusterMetadata simulateAndCompare(TokenPlacementModel.ReplicationFactor rf, List 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 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>> orig) - { - - Range[] ranges = new Range[orig.size()]; - int idx = 0; - NavigableMap> flattened = new TreeMap<>(); - for (Map.Entry>> e : orig.entrySet()) - { - List placementsForRange = new ArrayList<>(); - for (List 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> placementsForRange; - - public ReplicatedRanges(Range[] ranges, NavigableMap> placementsForRange) - { - this.ranges = ranges; - this.placementsForRange = placementsForRange; - } - - public List 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> asMap() - { - return placementsForRange; - } - - private static int indexedBinarySearch(T[] arr, CompareTo 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 - { - int compareTo(V v); - } - - private static , T1, T2> Map mapValues(Map allDCs, Function map) - { - NavigableMap res = new TreeMap<>(); - for (Map.Entry e : allDCs.entrySet()) - { - res.put(e.getKey(), map.apply(e.getValue())); - } - return res; - } - - public static Map> nodesByDC(List nodes) - { - Map> nodesByDC = new HashMap<>(); - for (Node node : nodes) - nodesByDC.computeIfAbsent(node.dc(), (k) -> new ArrayList<>()).add(node); - - return nodesByDC; - } - - public static Map> racksByDC(List nodes) - { - Map> 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 nodes = new ArrayList<>(); - private final Set 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 nodes, Set 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 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 - { - 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 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 tokens(int i) - { - return Collections.singletonList(Long.toString(lookup.token(i))); - } - } - - public static class Node implements Comparable - { - 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> 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 asMap(); - - public ReplicatedRanges replicate(List nodes) - { - return replicate(toRanges(nodes), nodes); - } - public abstract ReplicatedRanges replicate(Range[] ranges, List nodes); - } - - public static class NtsReplicationFactor extends ReplicationFactor - { - private final Lookup lookup = new DefaultLookup(); - public final int[] nodesPerDc; - private KeyspaceParams keyspaceParams; - private Map 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 asMap() - { - if (this.map == null) - this.map = toMap(lookup); - return this.map; - } - - public ReplicatedRanges replicate(Range[] ranges, List nodes) - { - return replicate(ranges, nodes, asMap()); - } - - private static > void assertStrictlySorted(Collection coll) - { - if (coll.size() <= 1) return; - - Iterator 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 nodes, Map rfs) - { - assertStrictlySorted(nodes); - Map template = new HashMap<>(); - - Map> nodesByDC = nodesByDC(nodes); - Map> racksByDC = racksByDC(nodes); - - for (Map.Entry entry : rfs.entrySet()) - { - String dc = entry.getKey(); - int rf = entry.getValue(); - List nodesInThisDC = nodesByDC.get(dc); - Set 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>> replication = new TreeMap<>(); - - for (Range range : ranges) - { - final int idx = primaryReplica(nodes, range); - int cnt = 0; - if (idx >= 0) - { - int dcsToFill = template.size(); - - Map nodesInDCs = new HashMap<>(); - for (Map.Entry 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 toMap(Lookup lookup) - { - Map 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 asMap() - { - return Collections.singletonMap(lookup.dc(1), total()); - } - - public ReplicatedRanges replicate(Range[] ranges, List nodes) - { - return replicate(ranges, nodes, total()); - } - - public static ReplicatedRanges replicate(Range[] ranges, List nodes, int rf) - { - NavigableMap> replication = new TreeMap<>(); - for (Range range : ranges) - { - Set names = new HashSet<>(); - List 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 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 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 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 orig = Collections.singletonList(factory.make(1, 1, 1)); ModelChecker 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 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 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 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 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 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> 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 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> 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 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 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> 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 void testInverse(Bijections.Bijection gen) + { + test(gen, + (v) -> Assert.assertEquals(gen.adjustEntropyDomain(v.descriptor), gen.deflate(v.value))); + } + + public static void testOrderPreserving(Bijections.Bijection gen) + { + testOrderPreserving(gen, false); + } + + public static void testOrderPreserving(Bijections.Bijection 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 void testOrderPreserving(Bijections.Bijection gen, Comparator 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 void test(Bijections.Bijection gen1, + Consumer> validate) + { + + for (int i = 0; i < RUNS; i++) + { + long descriptor1 = rand.next(); + validate.accept(new Generator.Value(descriptor1, gen1.inflate(gen1.adjustEntropyDomain(descriptor1)))); + } + } + + public static void test(Bijections.Bijection gen1, + Bijections.Bijection gen2, + BiConsumer, Generator.Value> validate) + { + + for (int i = 0; i < RUNS; i++) + { + long descriptor1 = rand.next(); + long descriptor2 = rand.next(); + validate.accept(new Generator.Value(descriptor1, gen1.inflate(gen1.adjustEntropyDomain(descriptor1))), + new Generator.Value(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> 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 Iterator permutations(int size, Class klass, T... values) + { + int[] cursors = new int[size]; + return new Iterator() + { + 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 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 gen = Surjections.weighted(Surjections.weights(weights), + "a", "b", "c"); + + Map 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 Gen convert(Generator generator) + { + return new Gen() + { + private final RandomnessSourceAdapter adapter = new RandomnessSourceAdapter<>(); + + public T generate(RandomnessSource randomnessSource) + { + return adapter.generate(randomnessSource, generator); + } + }; + } + + public static class RandomnessSourceAdapter 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 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 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 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> ckCounts = integers().between(0, 4).zip(integers().between(0, 6), Pair::create); + Gen> regCounts = integers().between(0, 4).zip(integers().between(0, 6), Pair::create); +// Gen> staticCounts = integers().between(0, 4).zip(integers().between(0, 6), Pair::create); + Gen> pkCounts = integers().between(1, 4).zip(integers().between(0, 6), Pair::create); + + Gen 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> schemaAndInputs = inputs.flatMap(input -> { + Generator 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> expectedColl, Collection actualColl) + { + List> expectedSorted = new ArrayList<>(expectedColl); + expectedSorted.sort(Comparator.comparing(Object::toString)); + List actualSorted = new ArrayList<>(actualColl); + actualSorted.sort(Comparator.comparing(Object::toString)); + + Iterator> expectedIter = expectedSorted.iterator(); + Iterator 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 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 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 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 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 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 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 sharedConfiguration() + { + Supplier 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 columnMaskSelector1; + public static final Surjections.Surjection columnMaskSelector2; + public static final Surjections.Surjection 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 corrupt, BiConsumer validate) throws Throwable + { + Supplier 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 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 corrupt, BiConsumer 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 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 stats = new HashMap<>(); + Supplier 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 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 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 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 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 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 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> 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> 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> subset(List> superset, BitSet bitSet) + { + Set> 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> randomSubset(List> superset, Random e) + { + Set> 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 BitSet subsetToBitset(List superset, Set 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 state; + + public ModelState(Map 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 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 m = new ConcurrentHashMap<>(); + ConcurrentHashMap 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 = 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> iter = m.entrySet().iterator(); + + Map.Entry previous = iter.next(); + while (iter.hasNext()) + { + if (previous == null) + { + previous = iter.next(); + continue; + } + + Map.Entry 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 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 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 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> 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 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 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 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 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 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> partitionMap = new HashMap<>(); + CompiledStatement compiledStatement = new CompiledStatement(""); + BiConsumer 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 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 ck1 = new TreeSet<>(); + Set ck2 = new TreeSet<>(); + Set 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 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 m = new EnumMap(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 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> 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 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 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 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 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 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 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 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 actual, Query query) { + // find out up the highest completed logical timestamp + long maxCompleteLts = tracker.maxComplete(); + + // get the expected state from reconciler + Iterator 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 actual_, Query query) { + // get a list of operations for each cd + NavigableMap> operations = inflatePartitionState(query); + + for (Map.Entry> entry : operations.entrySet()) { + long cd = entry.getKey(); + List 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 +{ + private final List> steps; + private final List> invariants; + private Precondition exitCondition; + private Consumer beforeAll; + private Consumer 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 = 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 invariant : invariants) + invariant.test(state.get()); + } + + if (afterAll != null) + afterAll.accept(state.get()); + } + public ModelChecker init(STATE state) + { + this.init = state; + return this; + } + + public ModelChecker beforeAll(Consumer precondition) + { + this.beforeAll = precondition; + return this; + } + + public ModelChecker afterAll(Consumer postcondition) + { + this.afterAll = postcondition; + return this; + } + + public ModelChecker exitCondition(Precondition precondition) + { + this.exitCondition = precondition; + return this; + } + + public ModelChecker step(Precondition precondition, Step step) + { + steps.add((ref, entropySource) -> { + ref.map(state -> { + if (!precondition.test(state)) + return state; + + return step.next(state, entropySource); + }); + }); + + return this; + } + + public ModelChecker invariant(Precondition invariant) + { + invariants.add(invariant); + return this; + } + + public ModelChecker step(Step step) + { + return step(Precondition.alwaysTrue(), step); + } + + public ModelChecker step(StatePrecondition precondition, ThrowingFunction step) + { + steps.add((ref, entropySource) -> { + ref.map(state -> { + if (!precondition.test(state)) + return state; + + return step.apply(state); + }); + }); + + return this; + } + + public ModelChecker step(ThrowingConsumer step) + { + return step((t, entropySource) -> { + step.consume(t); + return t; + }); + } + public ModelChecker step(ThrowingFunction step) + { + return step((t, entropySource) -> { + return step.apply(t); + }); + } + + interface StepExecutor + { + void execute(Ref state, EntropySource entropySource) throws Throwable; + } + + public interface StatePrecondition + { + boolean test(STATE state) throws Throwable; + } + + public interface Precondition + { + boolean test(STATE state) throws Throwable; + + static Precondition alwaysTrue() + { + return (a) -> true; + } + } + + public interface Step + { + STATE next(STATE t, EntropySource entropySource) throws Throwable; + } + + public interface ThrowingConsumer + { + void consume(I t) throws Throwable; + } + + public interface ThrowingFunction + { + O apply(I t) throws Throwable; + } + + public interface ThrowingBiFunction + { + O apply(I1 t1, I2 t2) throws Throwable; + } + + public static class Ref + { + 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 fn) throws Throwable + { + this.ref = fn.apply(ref); + } + } + + public static class State + { + public final MODEL model; + public final SUT sut; + + public State(MODEL model, SUT sut) + { + this.model = model; + this.sut = sut; + } + + public State 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). + *

+ * This conversion works as follows: + *

+ * * 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. + *

+ * 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 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 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 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) + ","; + } + return s.substring(0, Math.max(0, s.length() - 1)); + } + + public String toString() + { + return String.format("withDebugClock(%dL,\n\t%d,\n\t%d,\n\t%d,\n\t%d,\n\t%s,\n\t%s)", + startTimeMicros, + historySize, + lts.get(), + idx, + epoch, + epochTimeUnit, + dumpHistory()); + } + + private void throwCounterExhaustedException() + { + long diff = get(idx) - get(idx - 1); + throw new RuntimeException(String.format("Counter was exhausted. Drawn %d out of %d lts during the period.", + diff, periodMicros)); + } +} diff --git a/test/harry/main/org/apache/cassandra/harry/clock/OffsetClock.java b/test/harry/main/org/apache/cassandra/harry/clock/OffsetClock.java new file mode 100644 index 0000000000..d12948cf1e --- /dev/null +++ b/test/harry/main/org/apache/cassandra/harry/clock/OffsetClock.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.harry.clock; + +import java.util.concurrent.atomic.AtomicLong; + +import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.annotation.JsonProperty; +import com.fasterxml.jackson.annotation.JsonTypeName; +import org.apache.cassandra.harry.core.Configuration; +import org.apache.cassandra.harry.model.OpSelectors; + +/** + * A trivial implemementation of the clock, one that does not attempt to follow the wall clock. + * This clock simply offsets LTS by a preset value. + */ +public class OffsetClock implements OpSelectors.Clock +{ + final AtomicLong lts; + + private final long base; + + public OffsetClock(long base) + { + this(ApproximateClock.START_VALUE, base); + } + + public OffsetClock(long startValue, long base) + { + this.lts = new AtomicLong(startValue); + this.base = base; + } + + public long rts(long lts) + { + return base + lts; + } + + public long lts(long rts) + { + return rts - base; + } + + public long nextLts() + { + return lts.getAndIncrement(); + } + + public long peek() + { + return lts.get(); + } + + public Configuration.ClockConfiguration toConfig() + { + return new OffsetClockConfiguration(lts.get(), base); + } + + @JsonTypeName("offset") + public static class OffsetClockConfiguration implements Configuration.ClockConfiguration + { + public final long offset; + public final long base; + + @JsonCreator + public OffsetClockConfiguration(@JsonProperty("offset") long offset, + @JsonProperty(value = "base", defaultValue = "0") long base) + { + this.offset = offset; + this.base = base; + } + + public OpSelectors.Clock make() + { + return new OffsetClock(base, offset); + } + } +} diff --git a/test/harry/main/org/apache/cassandra/harry/core/Configuration.java b/test/harry/main/org/apache/cassandra/harry/core/Configuration.java new file mode 100644 index 0000000000..47e1d3bb5d --- /dev/null +++ b/test/harry/main/org/apache/cassandra/harry/core/Configuration.java @@ -0,0 +1,1255 @@ +/* + * 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.core; + +import java.io.BufferedWriter; +import java.io.File; +import java.io.FileOutputStream; +import java.io.IOException; +import java.io.OutputStreamWriter; +import java.lang.annotation.Annotation; +import java.util.*; +import java.util.concurrent.TimeUnit; +import java.util.function.Consumer; + +import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.annotation.JsonProperty; +import com.fasterxml.jackson.annotation.JsonTypeInfo; +import com.fasterxml.jackson.annotation.JsonTypeName; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.dataformat.yaml.YAMLFactory; +import com.fasterxml.jackson.dataformat.yaml.YAMLGenerator; +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.model.Model; +import org.apache.cassandra.harry.model.OpSelectors; +import org.apache.cassandra.harry.model.NoOpChecker; +import org.apache.cassandra.harry.model.QuiescentChecker; +import org.apache.cassandra.harry.clock.ApproximateClock; +import org.apache.cassandra.harry.sut.SystemUnderTest; +import org.apache.cassandra.harry.tracker.LockingDataTracker; +import org.apache.cassandra.harry.tracker.DataTracker; +import org.apache.cassandra.harry.tracker.DefaultDataTracker; +import org.apache.cassandra.harry.runner.Runner; +import org.apache.cassandra.harry.util.BitSet; +import org.apache.cassandra.harry.visitors.AllPartitionsValidator; +import org.apache.cassandra.harry.visitors.CorruptingVisitor; +import org.apache.cassandra.harry.visitors.LoggingVisitor; +import org.apache.cassandra.harry.visitors.MutatingRowVisitor; +import org.apache.cassandra.harry.visitors.MutatingVisitor; +import org.apache.cassandra.harry.visitors.OperationExecutor; +import org.apache.cassandra.harry.visitors.QueryLogger; +import org.apache.cassandra.harry.visitors.RandomValidator; +import org.apache.cassandra.harry.visitors.RecentValidator; +import org.apache.cassandra.harry.visitors.Visitor; +import org.reflections.Reflections; +import org.reflections.scanners.Scanners; +import org.reflections.util.NameHelper; + +public class Configuration +{ + private enum NameUtils implements NameHelper + { + INSTANCE; + } + + private static final ObjectMapper mapper; + + private static Set> findClassesMarkedWith(Class annotation) + { + Reflections reflections = new Reflections(org.reflections.util.ConfigurationBuilder.build("harry").setExpandSuperTypes(false)); + Collection> classes = NameUtils.INSTANCE.forNames(reflections.get(Scanners.TypesAnnotated.get(annotation.getName())), + reflections.getConfiguration().getClassLoaders()); + return new HashSet<>(classes); + } + + static + { + mapper = new ObjectMapper(new YAMLFactory() // checkstyle: permit this instantiation + .disable(YAMLGenerator.Feature.USE_NATIVE_TYPE_ID) + .disable(YAMLGenerator.Feature.WRITE_DOC_START_MARKER) + .disable(YAMLGenerator.Feature.CANONICAL_OUTPUT) + .enable(YAMLGenerator.Feature.INDENT_ARRAYS)); + + findClassesMarkedWith(JsonTypeName.class) + .forEach(mapper::registerSubtypes); + } + + public final long seed; + public final SchemaProviderConfiguration schema_provider; + + public final boolean drop_schema; + public final String keyspace_ddl; + public final boolean create_schema; + public final boolean truncate_table; + + public final MetricReporterConfiguration metric_reporter; + public final ClockConfiguration clock; + public final SutConfiguration system_under_test; + public final DataTrackerConfiguration data_tracker; + public final RunnerConfiguration runner; + public final PDSelectorConfiguration partition_descriptor_selector; + public final CDSelectorConfiguration clustering_descriptor_selector; + + @JsonCreator + public Configuration(@JsonProperty("seed") long seed, + @JsonProperty("schema_provider") SchemaProviderConfiguration schema_provider, + @JsonProperty("drop_schema") boolean drop_schema, + @JsonProperty("create_keyspace") String keyspace_ddl, + @JsonProperty("create_schema") boolean create_schema, + @JsonProperty("truncate_schema") boolean truncate_table, + @JsonProperty("metric_reporter") MetricReporterConfiguration metric_reporter, + @JsonProperty("clock") ClockConfiguration clock, + @JsonProperty("runner") RunnerConfiguration runner, + @JsonProperty("system_under_test") SutConfiguration system_under_test, + @JsonProperty("data_tracker") DataTrackerConfiguration data_tracker, + @JsonProperty("partition_descriptor_selector") PDSelectorConfiguration partition_descriptor_selector, + @JsonProperty("clustering_descriptor_selector") CDSelectorConfiguration clustering_descriptor_selector) + { + this.seed = seed; + this.schema_provider = schema_provider; + this.keyspace_ddl = keyspace_ddl; + this.drop_schema = drop_schema; + this.create_schema = create_schema; + this.truncate_table = truncate_table; + this.metric_reporter = metric_reporter; + this.clock = clock; + this.system_under_test = system_under_test; + this.data_tracker = data_tracker; + this.partition_descriptor_selector = partition_descriptor_selector; + this.clustering_descriptor_selector = clustering_descriptor_selector; + this.runner = runner; + } + + public static void registerSubtypes(Class... classes) + { + mapper.registerSubtypes(classes); + } + + public static void toFile(File file, Configuration config) + { + try (BufferedWriter bw = new BufferedWriter(new OutputStreamWriter(new FileOutputStream(file)))) + { + bw.write(Configuration.toYamlString(config)); + bw.flush(); + } + catch (IOException e) + { + throw new RuntimeException(e); + } + } + public static String toYamlString(Configuration config) + { + try + { + return mapper.writeValueAsString(config); + } + catch (Throwable t) + { + throw new RuntimeException(t); + } + } + + public static Configuration fromYamlString(String config) + { + try + { + return mapper.readValue(config, Configuration.class); + } + catch (Throwable t) + { + throw new RuntimeException(t); + } + } + + public static Configuration fromFile(String path) + { + return fromFile(new File(path)); + } + + public static Configuration fromFile(File file) + { + try + { + return mapper.readValue(file, Configuration.class); + } + catch (Throwable t) + { + throw new RuntimeException(t); + } + } + + public static void validate(Configuration config) + { + Objects.requireNonNull(config.schema_provider, "Schema provider should not be null"); + Objects.requireNonNull(config.metric_reporter, "Metric reporter should not be null"); + Objects.requireNonNull(config.clock, "Clock should not be null"); + Objects.requireNonNull(config.system_under_test, "System under test should not be null"); + Objects.requireNonNull(config.partition_descriptor_selector, "Partition descriptor selector should not be null"); + Objects.requireNonNull(config.clustering_descriptor_selector, "Clustering descriptor selector should not be null"); + } + + public Runner createRunner() + { + return createRunner(this); + } + + public Run createRun() + { + return createRun(this); + } + + public static Run createRun(Configuration snapshot) + { + SystemUnderTest sut = null; + try + { + validate(snapshot); + + long seed = snapshot.seed; + + OpSelectors.PureRng rng = new OpSelectors.PCGFast(seed); + + // TODO: validate that operation kind is compatible with schema, due to statics etc + sut = snapshot.system_under_test.make(); + SchemaSpec schemaSpec = snapshot.schema_provider.make(seed, sut); + schemaSpec.validate(); + + OpSelectors.PdSelector pdSelector = snapshot.partition_descriptor_selector.make(rng); + DataTrackerConfiguration dataTrackerConfiguration = snapshot.data_tracker == null ? new DefaultDataTrackerConfiguration() : snapshot.data_tracker; + DataTracker tracker = dataTrackerConfiguration.make(pdSelector, schemaSpec); + + OpSelectors.DescriptorSelector descriptorSelector = snapshot.clustering_descriptor_selector.make(rng, schemaSpec); + OpSelectors.Clock clock = snapshot.clock.make(); + + MetricReporter metricReporter = snapshot.metric_reporter.make(); + + return new Run(rng, + clock, + pdSelector, + descriptorSelector, + schemaSpec, + tracker, + sut, + metricReporter); + } + catch (Throwable t) + { + // Make sure to shut down all SUT threads if it has been started + if (sut != null) + { + sut.shutdown(); + } + throw t; + } + } + + public static Runner createRunner(Configuration config) + { + Run run = createRun(config); + return config.runner.make(run, config); + } + + public static class ConfigurationBuilder + { + long seed; + SchemaProviderConfiguration schema_provider = new DefaultSchemaProviderConfiguration(); + + String keyspace_ddl; + boolean drop_schema; + boolean create_schema; + boolean truncate_table; + + ClockConfiguration clock; + MetricReporterConfiguration metric_reporter = new NoOpMetricReporterConfiguration(); + DataTrackerConfiguration data_tracker = new DefaultDataTrackerConfiguration(); + RunnerConfiguration runner; + SutConfiguration system_under_test; + PDSelectorConfiguration partition_descriptor_selector = new Configuration.DefaultPDSelectorConfiguration(10, 100); + CDSelectorConfiguration clustering_descriptor_selector; // TODO: sensible default value + + public ConfigurationBuilder setSeed(long seed) + { + this.seed = seed; + return this; + } + + public ConfigurationBuilder setSchemaProvider(SchemaProviderConfiguration schema_provider) + { + this.schema_provider = schema_provider; + return this; + } + + public ConfigurationBuilder setDataTracker(DataTrackerConfiguration tracker) + { + this.data_tracker = tracker; + return this; + } + + public ConfigurationBuilder setKeyspaceDdl(String keyspace_ddl) + { + this.keyspace_ddl = keyspace_ddl; + return this; + } + + + public ConfigurationBuilder setClock(ClockConfiguration clock) + { + this.clock = clock; + return this; + } + + public ConfigurationBuilder setSUT(SutConfiguration system_under_test) + { + this.system_under_test = system_under_test; + return this; + } + + public ConfigurationBuilder setDropSchema(boolean drop_schema) + { + this.drop_schema = drop_schema; + return this; + } + + public ConfigurationBuilder setCreateSchema(boolean create_schema) + { + this.create_schema = create_schema; + return this; + } + + public ConfigurationBuilder setTruncateTable(boolean truncate_table) + { + this.truncate_table = truncate_table; + return this; + } + + public ConfigurationBuilder setRunner(RunnerConfiguration runner) + { + this.runner = runner; + return this; + } + + public ConfigurationBuilder setPartitionDescriptorSelector(PDSelectorConfiguration partition_descriptor_selector) + { + this.partition_descriptor_selector = partition_descriptor_selector; + return this; + } + + public ConfigurationBuilder setClusteringDescriptorSelector(CDSelectorConfiguration builder) + { + this.clustering_descriptor_selector = builder; + return this; + } + + public ConfigurationBuilder setClusteringDescriptorSelector(Consumer build) + { + CDSelectorConfigurationBuilder builder = new CDSelectorConfigurationBuilder(); + build.accept(builder); + return setClusteringDescriptorSelector(builder.build()); + } + + public ConfigurationBuilder setMetricReporter(MetricReporterConfiguration metric_reporter) + { + this.metric_reporter = metric_reporter; + return this; + } + + public Configuration build() + { + return new Configuration(seed, + schema_provider, + drop_schema, + keyspace_ddl, + create_schema, + truncate_table, + metric_reporter, + clock, + runner, + system_under_test, + data_tracker, + partition_descriptor_selector, + clustering_descriptor_selector); + } + } + + public ConfigurationBuilder unbuild() + { + ConfigurationBuilder builder = new ConfigurationBuilder(); + builder.seed = seed; + + builder.schema_provider = schema_provider; + builder.keyspace_ddl = keyspace_ddl; + builder.drop_schema = drop_schema; + builder.create_schema = create_schema; + builder.truncate_table = truncate_table; + + builder.data_tracker = data_tracker; + builder.clock = clock; + builder.runner = runner; + builder.system_under_test = system_under_test; + builder.metric_reporter = metric_reporter; + + builder.partition_descriptor_selector = partition_descriptor_selector; + builder.clustering_descriptor_selector = clustering_descriptor_selector; + + return builder; + } + + + @JsonTypeInfo(use = JsonTypeInfo.Id.NAME, include = JsonTypeInfo.As.WRAPPER_OBJECT) + public interface DataTrackerConfiguration extends DataTracker.DataTrackerFactory + { + + } + + @JsonTypeName("no_op") + public static class NoOpDataTrackerConfiguration implements DataTrackerConfiguration + { + @JsonCreator + public NoOpDataTrackerConfiguration() + { + } + + public DataTracker make(OpSelectors.PdSelector pdSelector, SchemaSpec schemaSpec) + { + return DataTracker.NO_OP; + } + } + + @JsonTypeName("default") + public static class DefaultDataTrackerConfiguration implements DataTrackerConfiguration + { + public final long max_seen_lts; + public final long max_complete_lts; + public final List reorder_buffer; + + public DefaultDataTrackerConfiguration() + { + this(-1, -1, null); + } + + @JsonCreator + public DefaultDataTrackerConfiguration(@JsonProperty(value = "max_seen_lts", defaultValue = "-1") long max_seen_lts, + @JsonProperty(value = "max_complete_lts", defaultValue = "-1") long max_complete_lts, + @JsonProperty(value = "reorder_buffer", defaultValue = "null") List reorder_buffer) + { + this.max_seen_lts = max_seen_lts; + this.max_complete_lts = max_complete_lts; + this.reorder_buffer = reorder_buffer; + } + + @Override + public DataTracker make(OpSelectors.PdSelector pdSelector, SchemaSpec schemaSpec) + { + DefaultDataTracker tracker = new DefaultDataTracker(); + tracker.forceLts(max_seen_lts, max_complete_lts, reorder_buffer); + return tracker; + } + } + + @JsonTypeName("locking") + public static class LockingDataTrackerConfiguration implements DataTrackerConfiguration + { + public final long max_seen_lts; + public final long max_complete_lts; + public final List reorder_buffer; + + @JsonCreator + public LockingDataTrackerConfiguration(@JsonProperty(value = "max_seen_lts", defaultValue = "-1") long max_seen_lts, + @JsonProperty(value = "max_complete_lts", defaultValue = "-1") long max_complete_lts, + @JsonProperty(value = "reorder_buffer", defaultValue = "null") List reorder_buffer) + { + this.max_seen_lts = max_seen_lts; + this.max_complete_lts = max_complete_lts; + this.reorder_buffer = reorder_buffer; + } + + @Override + public DataTracker make(OpSelectors.PdSelector pdSelector, SchemaSpec schemaSpec) + { + LockingDataTracker tracker = new LockingDataTracker(pdSelector, schemaSpec); + tracker.forceLts(max_seen_lts, max_complete_lts, reorder_buffer); + return tracker; + } + } + + public static class DefaultLockingDataTrackerConfiguration implements DataTrackerConfiguration + { + @Override + public DataTracker make(OpSelectors.PdSelector pdSelector, SchemaSpec schemaSpec) + { + LockingDataTracker tracker = new LockingDataTracker(pdSelector, schemaSpec); + tracker.forceLts(-1, -1, null); + return tracker; + } + } + + @JsonTypeInfo(use = JsonTypeInfo.Id.NAME, include = JsonTypeInfo.As.WRAPPER_OBJECT) + public interface ClockConfiguration extends OpSelectors.ClockFactory + { + } + + @JsonTypeName("approximate_monotonic") + public static class ApproximateClockConfiguration implements ClockConfiguration + { + public final int history_size; + public final int epoch_length; + public final TimeUnit epoch_time_unit; + + @JsonCreator + public ApproximateClockConfiguration(@JsonProperty("history_size") int history_size, + @JsonProperty("epoch_length") int epoch_length, + @JsonProperty("epoch_time_unit") TimeUnit epoch_time_unit) + { + this.history_size = history_size; + this.epoch_length = epoch_length; + this.epoch_time_unit = epoch_time_unit; + } + + public OpSelectors.Clock make() + { + return new ApproximateClock(history_size, + epoch_length, + epoch_time_unit); + } + } + + @JsonTypeName("debug_approximate_monotonic") + public static class DebugApproximateClockConfiguration implements ClockConfiguration + { + public final long start_time_micros; + public final int history_size; + public final long[] history; + public final long lts; + public final int idx; + public final long epoch_period; + public final TimeUnit epoch_time_unit; + + @JsonCreator + public DebugApproximateClockConfiguration(@JsonProperty("start_time_micros") long start_time_micros, + @JsonProperty("history_size") int history_size, + @JsonProperty("history") long[] history, + @JsonProperty("lts") long lts, + @JsonProperty("idx") int idx, + @JsonProperty("epoch_period") long epoch_period, + @JsonProperty("epoch_time_unit") TimeUnit epoch_time_unit) + { + this.start_time_micros = start_time_micros; + this.history_size = history_size; + this.history = history; + this.lts = lts; + this.idx = idx; + this.epoch_period = epoch_period; + this.epoch_time_unit = epoch_time_unit; + } + + public OpSelectors.Clock make() + { + return ApproximateClock.forDebug(start_time_micros, + history_size, + lts, + idx, + epoch_period, + epoch_time_unit, + history); + } + } + + @JsonTypeInfo(use = JsonTypeInfo.Id.NAME, include = JsonTypeInfo.As.WRAPPER_OBJECT) + public interface RunnerConfiguration extends Runner.RunnerFactory + { + } + + @JsonTypeName("concurrent") + public static class ConcurrentRunnerConfig implements RunnerConfiguration + { + @JsonProperty(value = "visitor_pools") + public final List visitor_pools; + + public final long run_time; + public final TimeUnit run_time_unit; + + @JsonCreator + public ConcurrentRunnerConfig(@JsonProperty(value = "visitor_pools") List visitor_pools, + @JsonProperty(value = "run_time", defaultValue = "2") long runtime, + @JsonProperty(value = "run_time_unit", defaultValue = "HOURS") TimeUnit runtimeUnit) + { + this.visitor_pools = visitor_pools; + this.run_time = runtime; + this.run_time_unit = runtimeUnit; + } + + @Override + public Runner make(Run run, Configuration config) + { + return new Runner.ConcurrentRunner(run, config, visitor_pools, run_time, run_time_unit); + } + } + + public static class VisitorPoolConfiguration + { + public final String prefix; + public final int concurrency; + public final VisitorConfiguration visitor; + + @JsonCreator + public VisitorPoolConfiguration(@JsonProperty(value = "prefix") String prefix, + @JsonProperty(value = "concurrency") int concurrency, + @JsonProperty(value = "visitor") VisitorConfiguration visitor) + { + this.prefix = prefix; + this.concurrency = concurrency; + this.visitor = visitor; + } + + public static VisitorPoolConfiguration pool(String prefix, int concurrency, VisitorConfiguration visitor) + { + return new VisitorPoolConfiguration(prefix, concurrency, visitor); + } + } + + @JsonTypeName("sequential") + public static class SequentialRunnerConfig implements RunnerConfiguration + { + @JsonProperty(value = "visitors") + public final List visitorFactories; + + public final long run_time; + public final TimeUnit run_time_unit; + + @JsonCreator + public SequentialRunnerConfig(@JsonProperty(value = "visitors") List visitors, + @JsonProperty(value = "run_time", defaultValue = "2") long runtime, + @JsonProperty(value = "run_time_unit", defaultValue = "HOURS") TimeUnit runtimeUnit) + { + this.visitorFactories = visitors; + this.run_time = runtime; + this.run_time_unit = runtimeUnit; + } + + @Override + public Runner make(Run run, Configuration config) + { + return new Runner.SequentialRunner(run, config, visitorFactories, run_time, run_time_unit); + } + } + + @JsonTypeName("single") + public static class SingleVisitRunnerConfig implements RunnerConfiguration + { + @JsonProperty(value = "visitors") + public final List visitorFactories; + + @JsonCreator + public SingleVisitRunnerConfig(@JsonProperty(value = "visitors") List visitors) + { + this.visitorFactories = visitors; + } + + @Override + public Runner make(Run run, Configuration config) + { + return new Runner.SingleVisitRunner(run, config, visitorFactories); + } + } + + @JsonTypeInfo(use = JsonTypeInfo.Id.NAME, include = JsonTypeInfo.As.WRAPPER_OBJECT) + public interface SutConfiguration extends SystemUnderTest.SUTFactory + { + } + + @JsonTypeInfo(use = JsonTypeInfo.Id.NAME, include = JsonTypeInfo.As.WRAPPER_OBJECT) + public interface ModelConfiguration extends Model.ModelFactory + { + } + + @JsonTypeName("quiescent_checker") + public static class QuiescentCheckerConfig implements ModelConfiguration + { + @JsonCreator + public QuiescentCheckerConfig() + { + } + + public Model make(Run run) + { + return new QuiescentChecker(run); + } + } + + @JsonTypeName("no_op") + public static class NoOpCheckerConfig implements ModelConfiguration + { + @JsonCreator + public NoOpCheckerConfig() + { + } + + public Model make(Run run) + { + return new NoOpChecker(run); + } + } + + + @JsonTypeInfo(use = JsonTypeInfo.Id.NAME, include = JsonTypeInfo.As.WRAPPER_OBJECT) + public interface PDSelectorConfiguration extends OpSelectors.PdSelectorFactory + { + } + + @JsonTypeName("default") + public static class DefaultPDSelectorConfiguration implements PDSelectorConfiguration + { + public final int window_size; + public final int slide_after_repeats; + public final long position_offset; + public final long position_window_size; + + public DefaultPDSelectorConfiguration(int window_size, + int slide_after_repeats) + { + this.window_size = window_size; + this.slide_after_repeats = slide_after_repeats; + this.position_offset = 0L; + this.position_window_size = Long.MAX_VALUE; + } + + @JsonCreator + public DefaultPDSelectorConfiguration(@JsonProperty(value = "window_size", defaultValue = "10") int window_size, + @JsonProperty(value = "slide_after_repeats", defaultValue = "100") int slide_after_repeats, + @JsonProperty(value = "runner_index") Long runner_index, + @JsonProperty(value = "total_runners") Long total_runners, + @JsonProperty(value = "position_offset") Long position_offset, + @JsonProperty(value = "position_window_size") Long position_window_size) + { + this.window_size = window_size; + this.slide_after_repeats = slide_after_repeats; + if (runner_index != null || total_runners != null) + { + assert runner_index != null && total_runners != null : "Both runner_index and total_runners are required"; + assert position_offset == null && position_window_size == null : "Please use either runner_index/total_runners or position_offset/position_window_size combinations."; + this.position_window_size = Long.MAX_VALUE / total_runners; + this.position_offset = this.position_window_size * runner_index; + } + else + { + assert runner_index == null && total_runners == null : "Please use either runner_index/total_runners or position_offset/position_window_size combinations."; + this.position_offset = position_offset == null ? 0 : position_offset; + if (position_window_size == null) + this.position_window_size = Long.MAX_VALUE - this.position_offset; + else + this.position_window_size = position_window_size; + } + } + + public OpSelectors.PdSelector make(OpSelectors.PureRng rng) + { + return new OpSelectors.DefaultPdSelector(rng, window_size, slide_after_repeats, position_offset, position_window_size); + } + } + + @JsonTypeInfo(use = JsonTypeInfo.Id.NAME, include = JsonTypeInfo.As.WRAPPER_OBJECT) + public interface CDSelectorConfiguration extends OpSelectors.DescriptorSelectorFactory + { + } + + public static class WeightedSelectorBuilder + { + private final Map operation_kind_weights; + + public WeightedSelectorBuilder() + { + operation_kind_weights = new HashMap<>(); + } + + public WeightedSelectorBuilder addWeight(T v, int weight) + { + operation_kind_weights.put(v, weight); + return this; + } + + public Map build() + { + return operation_kind_weights; + } + } + + public static class OperationKindSelectorBuilder extends WeightedSelectorBuilder + { + } + + // TODO: configure fractions/fractional builder + public static class CDSelectorConfigurationBuilder + { + private DistributionConfig operations_per_lts = new ConstantDistributionConfig(10); + private int max_partition_size = 100; + private Map operation_kind_weights = new OperationKindSelectorBuilder() + .addWeight(OpSelectors.OperationKind.DELETE_ROW, 1) + .addWeight(OpSelectors.OperationKind.DELETE_COLUMN, 1) + .addWeight(OpSelectors.OperationKind.INSERT, 98) + .build(); + private Map column_mask_bitsets; + private int[] fractions; + + public CDSelectorConfigurationBuilder setOperationsPerLtsDistribution(DistributionConfig operations_per_lts) + { + this.operations_per_lts = operations_per_lts; + return this; + } + + public CDSelectorConfigurationBuilder setMaxPartitionSize(int max_partition_size) + { + if (max_partition_size <= 0) + throw new IllegalArgumentException("Max partition size should be positive"); + this.max_partition_size = max_partition_size; + return this; + } + + public CDSelectorConfigurationBuilder setOperationKindWeights(Map operation_kind_weights) + { + this.operation_kind_weights = operation_kind_weights; + return this; + } + + public CDSelectorConfigurationBuilder setColumnMasks(Map column_mask_bitsets) + { + this.column_mask_bitsets = column_mask_bitsets; + return this; + } + + public CDSelectorConfigurationBuilder setFractions(int[] fractions) + { + this.fractions = fractions; + return this; + } + + public DefaultCDSelectorConfiguration build() + { + if (fractions == null) + { + return new DefaultCDSelectorConfiguration(operations_per_lts, + max_partition_size, + operation_kind_weights, + column_mask_bitsets); + } + else + { + return new HierarchicalCDSelectorConfiguration(operations_per_lts, + max_partition_size, + operation_kind_weights, + column_mask_bitsets, + fractions); + } + } + } + + @JsonTypeName("default") + public static class DefaultCDSelectorConfiguration implements CDSelectorConfiguration + { + public final DistributionConfig operations_per_lts; + public final int max_partition_size; + public final Map operation_kind_weights; + public final Map column_mask_bitsets; + + @JsonCreator + public DefaultCDSelectorConfiguration(@JsonProperty("operations_per_lts") DistributionConfig operations_per_lts, + @JsonProperty(value = "window_size", defaultValue = "100") int max_partition_size, + @JsonProperty("operation_kind_weights") Map operation_kind_weights, + @JsonProperty("column_mask_bitsets") Map column_mask_bitsets) + { + this.operations_per_lts = operations_per_lts; + this.max_partition_size = max_partition_size; + this.operation_kind_weights = operation_kind_weights; + this.column_mask_bitsets = column_mask_bitsets; + } + + protected OpSelectors.ColumnSelector columnSelector(SchemaSpec schemaSpec) + { + OpSelectors.ColumnSelector columnSelector; + if (column_mask_bitsets == null) + { + columnSelector = OpSelectors.columnSelectorBuilder().forAll(schemaSpec).build(); + } + else + { + Map> m = new EnumMap<>(OpSelectors.OperationKind.class); + for (Map.Entry entry : column_mask_bitsets.entrySet()) + { + List bitSets = new ArrayList<>(entry.getValue().length); + for (long raw_bitset : entry.getValue()) + { + bitSets.add(BitSet.create(raw_bitset, schemaSpec.allColumns.size())); + } + Surjections.Surjection selector = Surjections.pick(bitSets); + m.put(entry.getKey(), selector); + } + columnSelector = (opKind, descr) -> m.get(opKind).inflate(descr); + } + + return columnSelector; + } + + public OpSelectors.DescriptorSelector make(OpSelectors.PureRng rng, SchemaSpec schemaSpec) + { + return new OpSelectors.DefaultDescriptorSelector(rng, + columnSelector(schemaSpec), + OpSelectors.OperationSelector.weighted(operation_kind_weights), + operations_per_lts.make(), + max_partition_size); + } + } + + public static class HierarchicalCDSelectorConfiguration extends DefaultCDSelectorConfiguration + { + private final int[] fractions; + + public HierarchicalCDSelectorConfiguration(DistributionConfig operations_per_lts, + int max_partition_size, + Map operation_kind_weights, + Map column_mask_bitsets, + int[] fractions) + { + super(operations_per_lts, max_partition_size, operation_kind_weights, column_mask_bitsets); + this.fractions = fractions; + } + + public OpSelectors.DescriptorSelector make(OpSelectors.PureRng rng, SchemaSpec schemaSpec) + { + return new OpSelectors.HierarchicalDescriptorSelector(rng, + fractions, + columnSelector(schemaSpec), + OpSelectors.OperationSelector.weighted(operation_kind_weights), + operations_per_lts.make(), + max_partition_size); + } + } + + @JsonTypeInfo(use = JsonTypeInfo.Id.NAME, property = "type") + public interface DistributionConfig extends Distribution.DistributionFactory + { + } + + @JsonTypeName("identity") + public static class IdentityDistributionConfig implements DistributionConfig + { + @JsonCreator + public IdentityDistributionConfig() + { + } + + public Distribution make() + { + return new Distribution.IdentityDistribution(); + } + } + + @JsonTypeName("normal") + public static class NormalDistributionConfig implements DistributionConfig + { + @JsonCreator + public NormalDistributionConfig() + { + } + + public Distribution make() + { + return new Distribution.NormalDistribution(); + } + } + + @JsonTypeName("constant") + public static class ConstantDistributionConfig implements DistributionConfig + { + public final long constant; + + @JsonCreator + public ConstantDistributionConfig(@JsonProperty("constant") long constant) + { + this.constant = constant; + } + + public Distribution make() + { + return new Distribution.ConstantDistribution(constant); + } + } + + @JsonTypeName("scaled") + public static class ScaledDistributionConfig implements DistributionConfig + { + private final long min; + private final long max; + + @JsonCreator + public ScaledDistributionConfig(long min, long max) + { + this.min = min; + this.max = max; + } + + public Distribution make() + { + return new Distribution.ScaledDistribution(min, max); + } + } + + + @JsonTypeInfo(use = JsonTypeInfo.Id.NAME, include = JsonTypeInfo.As.WRAPPER_OBJECT) + public interface VisitorConfiguration extends Visitor.VisitorFactory + { + } + + + @JsonTypeName("mutating") + public static class MutatingVisitorConfiguation implements VisitorConfiguration + { + public final RowVisitorConfiguration row_visitor; + + @JsonCreator + public MutatingVisitorConfiguation(@JsonProperty("row_visitor") RowVisitorConfiguration row_visitor) + { + this.row_visitor = row_visitor; + } + + @Override + public Visitor make(Run run) + { + return new MutatingVisitor(run, row_visitor::make); + } + } + + @JsonTypeName("logging") + public static class LoggingVisitorConfiguration implements VisitorConfiguration + { + protected final RowVisitorConfiguration row_visitor; + + @JsonCreator + public LoggingVisitorConfiguration(@JsonProperty("row_visitor") RowVisitorConfiguration row_visitor) + { + this.row_visitor = row_visitor; + } + + @Override + public Visitor make(Run run) + { + return new LoggingVisitor(run, row_visitor::make); + } + } + + @JsonTypeName("validate_all_partitions") + public static class AllPartitionsValidatorConfiguration implements VisitorConfiguration + { + public final int concurrency; + public final QueryLoggerConfiguration query_logger; + public final Configuration.ModelConfiguration model; + + @JsonCreator + public AllPartitionsValidatorConfiguration(@JsonProperty("concurrency") int concurrency, + @JsonProperty("model") Configuration.ModelConfiguration model, + @JsonProperty("query_logger") QueryLoggerConfiguration query_logger) + { + this.concurrency = concurrency; + this.model = model; + this.query_logger = QueryLogger.thisOrDefault(query_logger); + } + + public Visitor make(Run run) + { + return new AllPartitionsValidator(run, concurrency, model, query_logger.make()); + } + } + + @JsonTypeName("corrupt") + public static class CorruptingVisitorConfiguration implements VisitorConfiguration + { + public final int trigger_after; + + @JsonCreator + public CorruptingVisitorConfiguration(@JsonProperty("trigger_after") int trigger_after) + { + this.trigger_after = trigger_after; + } + + public Visitor make(Run run) + { + return new CorruptingVisitor(trigger_after, run); + } + } + + @JsonTypeName("validate_recent_partitions") + public static class RecentPartitionsValidatorConfiguration implements VisitorConfiguration + { + public final int partition_count; + public final int queries; + public final Configuration.ModelConfiguration modelConfiguration; + public final QueryLoggerConfiguration query_logger; + + // TODO: make query selector configurable + @JsonCreator + public RecentPartitionsValidatorConfiguration(@JsonProperty("partition_count") int partition_count, + @JsonProperty("queries_per_partition") int queries, + @JsonProperty("model") Configuration.ModelConfiguration model, + @JsonProperty("logger") QueryLoggerConfiguration query_logger) + { + this.partition_count = partition_count; + this.queries = queries; + this.modelConfiguration = model; + this.query_logger = QueryLogger.thisOrDefault(query_logger); + } + + @Override + public Visitor make(Run run) + { + return new RecentValidator(partition_count, queries, run, modelConfiguration, query_logger.make()); + } + } + + @JsonTypeName("validate_random_partitions") + public static class RandomPartitionValidatorConfiguration implements VisitorConfiguration + { + public final int partition_count; + public final int queries; + public final Configuration.ModelConfiguration model_configuration; + public final QueryLoggerConfiguration query_logger; + + // TODO: make query selector configurable + @JsonCreator + public RandomPartitionValidatorConfiguration(@JsonProperty("partition_count") int partition_count, + @JsonProperty("queries_per_partition") int queries, + @JsonProperty("model") Configuration.ModelConfiguration model, + @JsonProperty("logger") QueryLoggerConfiguration query_logger) + { + this.partition_count = partition_count; + this.queries = queries; + this.model_configuration = model; + this.query_logger = QueryLogger.thisOrDefault(query_logger); + } + + @Override + public Visitor make(Run run) + { + return new RandomValidator(partition_count, queries, run, model_configuration, query_logger.make()); + } + } + + @JsonTypeInfo(use = JsonTypeInfo.Id.NAME, include = JsonTypeInfo.As.WRAPPER_OBJECT) + public interface QueryLoggerConfiguration extends QueryLogger.QueryLoggerFactory + { + } + + @JsonTypeName("no_op") + public static class NoOpQueryLoggerConfiguration implements QueryLoggerConfiguration + { + public QueryLogger make() + { + return QueryLogger.NO_OP; + } + } + + @JsonTypeName("file") + public static class FileQueryLoggerConfiguration implements QueryLoggerConfiguration + { + public final String filename; + + @JsonCreator + public FileQueryLoggerConfiguration(@JsonProperty("filename") String filename) + { + this.filename = filename; + } + + public QueryLogger make() + { + return new QueryLogger.FileQueryLogger(filename); + } + } + + + @JsonTypeInfo(use = JsonTypeInfo.Id.NAME, include = JsonTypeInfo.As.WRAPPER_OBJECT) + public interface RowVisitorConfiguration extends OperationExecutor.RowVisitorFactory + { + } + + @JsonTypeName("mutating") + public static class MutatingRowVisitorConfiguration implements RowVisitorConfiguration + { + @Override + public OperationExecutor make(Run run) + { + return new MutatingRowVisitor(run); + } + } + + @JsonTypeInfo(use = JsonTypeInfo.Id.NAME, include = JsonTypeInfo.As.WRAPPER_OBJECT) + public interface SchemaProviderConfiguration extends SchemaSpec.SchemaSpecFactory + { + } + + @JsonTypeName("default") + public static class DefaultSchemaProviderConfiguration implements SchemaProviderConfiguration + { + public SchemaSpec make(long seed, SystemUnderTest sut) + { + return SchemaGenerators.defaultSchemaSpecGen("table0") + .inflate(seed); + } + } + + @JsonTypeName("fixed") + public static class FixedSchemaProviderConfiguration implements SchemaProviderConfiguration + { + private final SchemaSpec schemaSpec; + + @JsonCreator + public FixedSchemaProviderConfiguration(@JsonProperty("keyspace") String keyspace, + @JsonProperty("table") String table, + @JsonProperty("partition_keys") Map pks, + @JsonProperty("clustering_keys") Map cks, + @JsonProperty("regular_columns") Map regulars, + @JsonProperty("static_columns") Map statics) + { + this(SchemaGenerators.parse(keyspace, table, + pks, cks, regulars, statics)); + } + + public FixedSchemaProviderConfiguration(SchemaSpec schemaSpec) + { + this.schemaSpec = schemaSpec; + } + public SchemaSpec make(long seed, SystemUnderTest sut) + { + return schemaSpec; + } + } + + @JsonTypeInfo(use = JsonTypeInfo.Id.NAME, include = JsonTypeInfo.As.WRAPPER_OBJECT) + public interface MetricReporterConfiguration extends MetricReporter.MetricReporterFactory + { + } + + @JsonTypeName("no_op") + public static class NoOpMetricReporterConfiguration implements MetricReporterConfiguration + { + public MetricReporter make() + { + return MetricReporter.NO_OP; + } + } +} diff --git a/test/harry/main/org/apache/cassandra/harry/core/MetricReporter.java b/test/harry/main/org/apache/cassandra/harry/core/MetricReporter.java new file mode 100644 index 0000000000..4034eb97c5 --- /dev/null +++ b/test/harry/main/org/apache/cassandra/harry/core/MetricReporter.java @@ -0,0 +1,51 @@ +/* + * 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.core; + +public interface MetricReporter +{ + void columnDelete(); + void rowDelete(); + void partitionDelete(); + void insert(); + void rangeDelete(); + + void validatePartition(); + void validateRandomQuery(); + + interface MetricReporterFactory + { + MetricReporter make(); + } + + MetricReporter NO_OP = new NoOpMetricReporter(); + + class NoOpMetricReporter implements MetricReporter + { + private NoOpMetricReporter() {} + + public void columnDelete(){} + public void rowDelete(){} + public void partitionDelete(){} + public void insert(){} + public void rangeDelete(){} + public void validatePartition(){} + public void validateRandomQuery(){} + } +} diff --git a/test/harry/main/org/apache/cassandra/harry/core/Run.java b/test/harry/main/org/apache/cassandra/harry/core/Run.java new file mode 100644 index 0000000000..0399a34072 --- /dev/null +++ b/test/harry/main/org/apache/cassandra/harry/core/Run.java @@ -0,0 +1,75 @@ +/* + * 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.core; + +import org.apache.cassandra.harry.ddl.SchemaSpec; +import org.apache.cassandra.harry.model.OpSelectors; +import org.apache.cassandra.harry.sut.SystemUnderTest; +import org.apache.cassandra.harry.tracker.DataTracker; +import org.apache.cassandra.harry.operations.QueryGenerator; + +public class Run +{ + public final OpSelectors.PureRng rng; + public final OpSelectors.Clock clock; + public final OpSelectors.PdSelector pdSelector; + public final OpSelectors.DescriptorSelector descriptorSelector; + public final QueryGenerator rangeSelector; + + public final SchemaSpec schemaSpec; + public final DataTracker tracker; + public final SystemUnderTest sut; + + public final MetricReporter metricReporter; + + public Run(OpSelectors.PureRng rng, + OpSelectors.Clock clock, + OpSelectors.PdSelector pdSelector, + OpSelectors.DescriptorSelector descriptorSelector, + SchemaSpec schemaSpec, + DataTracker tracker, + SystemUnderTest sut, + MetricReporter metricReporter) + { + this(rng, clock, pdSelector, descriptorSelector, + new QueryGenerator(schemaSpec, pdSelector, descriptorSelector, rng), + schemaSpec, tracker, sut, metricReporter); + } + + private Run(OpSelectors.PureRng rng, + OpSelectors.Clock clock, + OpSelectors.PdSelector pdSelector, + OpSelectors.DescriptorSelector descriptorSelector, + QueryGenerator rangeSelector, + SchemaSpec schemaSpec, + DataTracker tracker, + SystemUnderTest sut, + MetricReporter metricReporter) + { + this.rng = rng; + this.clock = clock; + this.pdSelector = pdSelector; + this.descriptorSelector = descriptorSelector; + this.rangeSelector = rangeSelector; + this.schemaSpec = schemaSpec; + this.tracker = tracker; + this.sut = sut; + this.metricReporter = metricReporter; + } +} \ No newline at end of file diff --git a/test/harry/main/org/apache/cassandra/harry/corruptor/AddExtraRowCorruptor.java b/test/harry/main/org/apache/cassandra/harry/corruptor/AddExtraRowCorruptor.java new file mode 100644 index 0000000000..11de57ba4a --- /dev/null +++ b/test/harry/main/org/apache/cassandra/harry/corruptor/AddExtraRowCorruptor.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.harry.corruptor; + +import java.util.HashSet; +import java.util.Set; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import org.apache.cassandra.harry.data.ResultSetRow; +import org.apache.cassandra.harry.ddl.SchemaSpec; +import org.apache.cassandra.harry.model.OpSelectors; +import org.apache.cassandra.harry.model.SelectHelper; +import org.apache.cassandra.harry.sut.SystemUnderTest; +import org.apache.cassandra.harry.operations.WriteHelper; +import org.apache.cassandra.harry.operations.Query; +import org.apache.cassandra.harry.tracker.DataTracker; + +public class AddExtraRowCorruptor implements QueryResponseCorruptor +{ + private static final Logger logger = LoggerFactory.getLogger(AddExtraRowCorruptor.class); + + private final SchemaSpec schema; + private final OpSelectors.Clock clock; + private final DataTracker tracker; + private final OpSelectors.DescriptorSelector descriptorSelector; + + public AddExtraRowCorruptor(SchemaSpec schema, + OpSelectors.Clock clock, + DataTracker tracker, + OpSelectors.DescriptorSelector descriptorSelector) + { + this.schema = schema; + this.clock = clock; + this.tracker = tracker; + this.descriptorSelector = descriptorSelector; + } + + public boolean maybeCorrupt(Query query, SystemUnderTest sut) + { + Set cds = new HashSet<>(); + long maxLts = tracker.maxStarted(); + for (Object[] obj : sut.execute(query.toSelectStatement(), SystemUnderTest.ConsistencyLevel.ALL)) + { + ResultSetRow row = SelectHelper.resultSetToRow(schema, clock, obj); + cds.add(row.cd); + } + boolean partitionIsFull = cds.size() >= descriptorSelector.maxPartitionSize(); + + long attempt = 0; + long cd = descriptorSelector.randomCd(query.pd, attempt, schema); + while (!query.matchCd(cd) || cds.contains(cd)) + { + if (partitionIsFull) + // We can't pick from the existing CDs, so let's try to come up with a new one that would match the query + cd += descriptorSelector.randomCd(query.pd, attempt, schema); + else + cd = descriptorSelector.randomCd(query.pd, attempt, schema); + if (attempt++ == 1000) + return false; + } + + long[] vds = descriptorSelector.vds(query.pd, cd, maxLts, 0, OpSelectors.OperationKind.INSERT, schema); + + // We do not know if the row was deleted. We could try inferring it, but that + // still won't help since we can't use it anyways, since collisions between a + // written value and tombstone are resolved in favour of tombstone, so we're + // just going to take the next lts. + logger.info("Corrupting the resultset by writing a row with cd {}", cd); + sut.execute(WriteHelper.inflateInsert(schema, query.pd, cd, vds, null, clock.rts(maxLts) + 1), SystemUnderTest.ConsistencyLevel.ALL); + return true; + } +} \ No newline at end of file diff --git a/test/harry/main/org/apache/cassandra/harry/corruptor/ChangeValueCorruptor.java b/test/harry/main/org/apache/cassandra/harry/corruptor/ChangeValueCorruptor.java new file mode 100644 index 0000000000..1e9189a93b --- /dev/null +++ b/test/harry/main/org/apache/cassandra/harry/corruptor/ChangeValueCorruptor.java @@ -0,0 +1,85 @@ +/* + * 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.corruptor; + +import java.util.Arrays; + +import org.apache.cassandra.harry.data.ResultSetRow; +import org.apache.cassandra.harry.ddl.SchemaSpec; +import org.apache.cassandra.harry.gen.DataGenerators; +import org.apache.cassandra.harry.gen.rng.PcgRSUFast; +import org.apache.cassandra.harry.gen.EntropySource; +import org.apache.cassandra.harry.model.Model; +import org.apache.cassandra.harry.model.OpSelectors; +import org.apache.cassandra.harry.operations.CompiledStatement; +import org.apache.cassandra.harry.operations.WriteHelper; + +/** + * Corrupts a single value written value in the row by writing a valid, invertible value with an incorrect + * descriptor, if row has any values written. + */ +public class ChangeValueCorruptor implements RowCorruptor +{ + private final SchemaSpec schema; + private final OpSelectors.Clock clock; + private final EntropySource rng; + + public ChangeValueCorruptor(SchemaSpec schemaSpec, + OpSelectors.Clock clock) + { + this.schema = schemaSpec; + this.clock = clock; + this.rng = new PcgRSUFast(1, 1); + } + + // Can corrupt any row that has at least one written non-null value + public boolean canCorrupt(ResultSetRow row) + { + for (int idx = 0; idx < row.lts.length; idx++) + { + // TODO: in addition to this, we should check if the value equals to the largest possible + // value, since otherwise it won't sort correctly. + if (row.lts[idx] != Model.NO_TIMESTAMP) + return true; + } + return false; + } + + public CompiledStatement corrupt(ResultSetRow row) + { + long[] corruptedVds = new long[row.vds.length]; + Arrays.fill(corruptedVds, DataGenerators.UNSET_DESCR); + + int idx; + do + { + idx = rng.nextInt(corruptedVds.length - 1); + } while (row.lts[idx] == Model.NO_TIMESTAMP); + + final long oldV = row.vds[idx]; + do + { + corruptedVds[idx] = schema.regularColumns.get(idx).type.generator().adjustEntropyDomain(rng.next()); + } + // we need to find a value that sorts strictly greater than the current one + while (schema.regularColumns.get(idx).type.compareLexicographically(corruptedVds[idx], oldV) <= 0); + + return WriteHelper.inflateInsert(schema, row.pd, row.cd, corruptedVds, null, clock.rts(row.lts[idx])); + } +} diff --git a/test/harry/main/org/apache/cassandra/harry/corruptor/HideRowCorruptor.java b/test/harry/main/org/apache/cassandra/harry/corruptor/HideRowCorruptor.java new file mode 100644 index 0000000000..d8b8ed971c --- /dev/null +++ b/test/harry/main/org/apache/cassandra/harry/corruptor/HideRowCorruptor.java @@ -0,0 +1,49 @@ +/* + * 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.corruptor; + +import org.apache.cassandra.harry.data.ResultSetRow; +import org.apache.cassandra.harry.ddl.SchemaSpec; +import org.apache.cassandra.harry.model.OpSelectors; +import org.apache.cassandra.harry.operations.CompiledStatement; +import org.apache.cassandra.harry.operations.DeleteHelper; + +public class HideRowCorruptor implements RowCorruptor +{ + private final SchemaSpec schema; + private final OpSelectors.Clock clock; + + public HideRowCorruptor(SchemaSpec schemaSpec, + OpSelectors.Clock clock) + { + this.schema = schemaSpec; + this.clock = clock; + } + + // Can corrupt any row that has at least one written non-null value + public boolean canCorrupt(ResultSetRow row) + { + return row != null; + } + + public CompiledStatement corrupt(ResultSetRow row) + { + return DeleteHelper.deleteRow(schema, row.pd, row.cd, clock.rts(clock.peek())); + } +} diff --git a/test/harry/main/org/apache/cassandra/harry/corruptor/HideValueCorruptor.java b/test/harry/main/org/apache/cassandra/harry/corruptor/HideValueCorruptor.java new file mode 100644 index 0000000000..7b2d1fbfe7 --- /dev/null +++ b/test/harry/main/org/apache/cassandra/harry/corruptor/HideValueCorruptor.java @@ -0,0 +1,109 @@ +/* + * 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.corruptor; + +import java.util.HashSet; +import java.util.Set; + +import org.apache.cassandra.harry.data.ResultSetRow; +import org.apache.cassandra.harry.ddl.SchemaSpec; +import org.apache.cassandra.harry.gen.rng.JdkRandomEntropySource; +import org.apache.cassandra.harry.gen.EntropySource; +import org.apache.cassandra.harry.model.Model; +import org.apache.cassandra.harry.model.OpSelectors; +import org.apache.cassandra.harry.operations.CompiledStatement; +import org.apache.cassandra.harry.operations.DeleteHelper; +import org.apache.cassandra.harry.util.BitSet; + +// removes/hides the value of one of the columns that was previously set +public class HideValueCorruptor implements RowCorruptor +{ + private final SchemaSpec schema; + private final OpSelectors.Clock clock; + private final EntropySource rng; + + public HideValueCorruptor(SchemaSpec schemaSpec, + OpSelectors.Clock clock) + { + this.schema = schemaSpec; + this.clock = clock; + this.rng = new JdkRandomEntropySource(1L); + } + + // Can corrupt any row that has at least one written non-null value + public boolean canCorrupt(ResultSetRow row) + { + for (int idx = 0; idx < row.lts.length; idx++) + { + if (row.lts[idx] != Model.NO_TIMESTAMP) + return true; + } + return false; + } + + public CompiledStatement corrupt(ResultSetRow row) + { + BitSet mask; + // Corrupt a static row, if it is available and if RNG says so + if (row.hasStaticColumns() && rng.nextBoolean()) + { + int cnt = 0; + int idx; + do + { + idx = rng.nextInt(row.slts.length); + cnt++; + } + while (row.slts[idx] == Model.NO_TIMESTAMP && cnt < 10); + + if (row.slts[idx] != Model.NO_TIMESTAMP) + { + mask = BitSet.allUnset(schema.allColumns.size()); + mask.set(schema.staticColumnsOffset + idx); + + return DeleteHelper.deleteColumn(schema, + row.pd, + mask, + schema.regularAndStaticColumnsMask(), + clock.rts(clock.peek())); + } + } + + Set tried = new HashSet<>(); + int idx; + do + { + if (tried.size() == row.lts.length) + throw new IllegalStateException(String.format("Could not corrupt after trying all %s indexes", tried)); + idx = rng.nextInt(row.lts.length); + tried.add(idx); + } + while (row.lts[idx] == Model.NO_TIMESTAMP); + + mask = BitSet.allUnset(schema.allColumns.size()); + mask.set(schema.regularColumnsOffset + idx); + + return DeleteHelper.deleteColumn(schema, + row.pd, + row.cd, + mask, + schema.regularAndStaticColumnsMask(), + clock.rts(clock.peek())); + } +} diff --git a/test/harry/main/org/apache/cassandra/harry/corruptor/QueryResponseCorruptor.java b/test/harry/main/org/apache/cassandra/harry/corruptor/QueryResponseCorruptor.java new file mode 100644 index 0000000000..6495bd4d11 --- /dev/null +++ b/test/harry/main/org/apache/cassandra/harry/corruptor/QueryResponseCorruptor.java @@ -0,0 +1,108 @@ +/* + * 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.corruptor; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import org.apache.cassandra.harry.data.ResultSetRow; +import org.apache.cassandra.harry.ddl.SchemaSpec; +import org.apache.cassandra.harry.model.OpSelectors; +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.operations.Query; + +public interface QueryResponseCorruptor +{ + Logger logger = LoggerFactory.getLogger(QueryResponseCorruptor.class); + + boolean maybeCorrupt(Query query, SystemUnderTest sut); + + class SimpleQueryResponseCorruptor implements QueryResponseCorruptor + { + private final RowCorruptor rowCorruptor; + private final SchemaSpec schema; + private final OpSelectors.Clock clock; + + public SimpleQueryResponseCorruptor(SchemaSpec schema, + OpSelectors.Clock clock, + RowCorruptor.RowCorruptorFactory factory) + { + this.rowCorruptor = factory.create(schema, clock); + this.schema = schema; + this.clock = clock; + } + + public boolean maybeCorrupt(Query query, SystemUnderTest sut) + { + List result = new ArrayList<>(); + CompiledStatement statement = query.toSelectStatement(); + Object[][] before = sut.execute(statement.cql(), SystemUnderTest.ConsistencyLevel.ALL, statement.bindings()); + for (Object[] obj : before) + result.add(SelectHelper.resultSetToRow(schema, clock, obj)); + + // Technically, we can do this just depends on corruption strategy, + // we just need to corrupt results of the current query. + if (result.isEmpty()) + return false; + + for (ResultSetRow row : result) + { + if (rowCorruptor.maybeCorrupt(row, sut)) + { + Object[][] after = sut.execute(statement.cql(), SystemUnderTest.ConsistencyLevel.ALL, statement.bindings()); + boolean mismatch = false; + for (int i = 0; i < before.length && i < after.length; i++) + { + if (!Arrays.equals(before[i], after[i])) + { + logger.info("Corrupted: \nBefore: {}\n" + + "After: {}\n", + Arrays.toString(before[i]), + Arrays.toString(after[i])); + mismatch = true; + } + } + assert mismatch || before.length != after.length : String.format("Could not corrupt.\n" + + "Before\n%s\n" + + "After\n%s\nkma", + toString(before), + toString(after)); + return true; + } + } + return false; + } + + private static String toString(Object[][] obj) + { + StringBuilder sb = new StringBuilder(); + for (Object[] objects : obj) + { + sb.append(Arrays.toString(objects)).append("\n"); + } + return sb.toString(); + } + } +} diff --git a/test/harry/main/org/apache/cassandra/harry/corruptor/RowCorruptor.java b/test/harry/main/org/apache/cassandra/harry/corruptor/RowCorruptor.java new file mode 100644 index 0000000000..969f49ce46 --- /dev/null +++ b/test/harry/main/org/apache/cassandra/harry/corruptor/RowCorruptor.java @@ -0,0 +1,56 @@ +/* + * 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.corruptor; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import org.apache.cassandra.harry.data.ResultSetRow; +import org.apache.cassandra.harry.ddl.SchemaSpec; +import org.apache.cassandra.harry.model.OpSelectors; +import org.apache.cassandra.harry.sut.SystemUnderTest; +import org.apache.cassandra.harry.operations.CompiledStatement; + +public interface RowCorruptor +{ + final Logger logger = LoggerFactory.getLogger(QueryResponseCorruptor.class); + + boolean canCorrupt(ResultSetRow row); + + CompiledStatement corrupt(ResultSetRow row); + + // Returns true if it could corrupt a row, false otherwise + default boolean maybeCorrupt(ResultSetRow row, SystemUnderTest sut) + { + if (canCorrupt(row)) + { + CompiledStatement statement = corrupt(row); + sut.execute(statement.cql(), SystemUnderTest.ConsistencyLevel.ALL, statement.bindings()); + logger.info("Corrupting with: {} ({})", statement.cql(), CompiledStatement.bindingsToString(statement.bindings())); + return true; + } + return false; + } + + interface RowCorruptorFactory + { + RowCorruptor create(SchemaSpec schemaSpec, + OpSelectors.Clock clock); + } +} \ No newline at end of file diff --git a/test/harry/main/org/apache/cassandra/harry/corruptor/ShowValueCorruptor.java b/test/harry/main/org/apache/cassandra/harry/corruptor/ShowValueCorruptor.java new file mode 100644 index 0000000000..79c6c737e7 --- /dev/null +++ b/test/harry/main/org/apache/cassandra/harry/corruptor/ShowValueCorruptor.java @@ -0,0 +1,77 @@ +/* + * 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.corruptor; + +import java.util.Arrays; + +import org.apache.cassandra.harry.data.ResultSetRow; +import org.apache.cassandra.harry.ddl.SchemaSpec; +import org.apache.cassandra.harry.gen.DataGenerators; +import org.apache.cassandra.harry.gen.rng.PcgRSUFast; +import org.apache.cassandra.harry.gen.EntropySource; +import org.apache.cassandra.harry.model.Model; +import org.apache.cassandra.harry.model.OpSelectors; +import org.apache.cassandra.harry.operations.CompiledStatement; +import org.apache.cassandra.harry.operations.WriteHelper; + +public class ShowValueCorruptor implements RowCorruptor +{ + private final SchemaSpec schema; + private final OpSelectors.Clock clock; + private final EntropySource rng; + + public ShowValueCorruptor(SchemaSpec schemaSpec, + OpSelectors.Clock clock) + { + this.schema = schemaSpec; + this.clock = clock; + this.rng = new PcgRSUFast(1, 1); + } + + // Can corrupt any row that has at least one written non-null value + public boolean canCorrupt(ResultSetRow row) + { + for (int idx = 0; idx < row.lts.length; idx++) + { + if (row.lts[idx] == Model.NO_TIMESTAMP) + return true; + } + return false; + } + + public CompiledStatement corrupt(ResultSetRow row) + { + long[] corruptedVds = new long[row.lts.length]; + Arrays.fill(corruptedVds, DataGenerators.UNSET_DESCR); + + int idx; + do + { + idx = rng.nextInt(corruptedVds.length - 1); + } + while (row.lts[idx] != Model.NO_TIMESTAMP); + + corruptedVds[idx] = rng.next(); + + // We do not know LTS of the deleted row. We could try inferring it, but that + // still won't help since we can't use it anyways, since collisions between a + // written value and tombstone are resolved in favour of tombstone. + return WriteHelper.inflateInsert(schema, row.pd, row.cd, corruptedVds, null, clock.rts(clock.peek())); + } +} diff --git a/test/harry/main/org/apache/cassandra/harry/data/ResultSetRow.java b/test/harry/main/org/apache/cassandra/harry/data/ResultSetRow.java new file mode 100644 index 0000000000..7ee1e06e0c --- /dev/null +++ b/test/harry/main/org/apache/cassandra/harry/data/ResultSetRow.java @@ -0,0 +1,137 @@ +/* + * 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.data; + +import java.util.Arrays; +import java.util.Collections; +import java.util.List; +import java.util.Objects; + +import org.apache.cassandra.harry.ddl.SchemaSpec; +import org.apache.cassandra.harry.util.StringUtils; + +public class ResultSetRow +{ + public final long pd; + public final long cd; + public final long[] vds; + public final long[] lts; + + public final long[] sds; + public final long[] slts; + public final List visited_lts; + + private ResultSetRow(long pd, + long cd, + long[] sds, + long[] slts, + long[] vds, + long[] lts) + { + this(pd, cd, sds, slts, vds, lts, Collections.emptyList()); + } + + public ResultSetRow(long pd, + long cd, + long[] sds, + long[] slts, + long[] vds, + long[] lts, + List visited_lts) + { + // There should be as many timestamps as value descriptors, both for regular and static columns + assert slts.length == sds.length : String.format("Descriptors: %s, timestamps: %s", Arrays.toString(sds), Arrays.toString(slts)); + assert lts.length == vds.length : String.format("Descriptors: %s, timestamps: %s", Arrays.toString(vds), Arrays.toString(lts)); + this.pd = pd; + this.cd = cd; + this.vds = vds; + this.lts = lts; + this.sds = sds; + this.slts = slts; + this.visited_lts = visited_lts; + } + + public boolean hasStaticColumns() + { + return slts.length > 0; + } + + @Override + public ResultSetRow clone() + { + return new ResultSetRow(pd, cd, + Arrays.copyOf(sds, sds.length), Arrays.copyOf(slts, slts.length), + Arrays.copyOf(vds, vds.length), Arrays.copyOf(lts, lts.length), + visited_lts); + } + + @Override + public int hashCode() + { + int result = Objects.hash(pd, cd, visited_lts); + result = 31 * result + Arrays.hashCode(vds); + result = 31 * result + Arrays.hashCode(lts); + result = 31 * result + Arrays.hashCode(sds); + result = 31 * result + Arrays.hashCode(slts); + return result; + } + + @Override + public boolean equals(Object o) + { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + ResultSetRow that = (ResultSetRow) o; + return pd == that.pd && + cd == that.cd && + Arrays.equals(vds, that.vds) && + Arrays.equals(lts, that.lts) && + Arrays.equals(sds, that.sds) && + Arrays.equals(slts, that.slts) && + Objects.equals(visited_lts, that.visited_lts); + } + + @Override + public String toString() + { + return "resultSetRow(" + + pd + + "L, " + cd + + (sds == null ? "" : "L, statics(" + StringUtils.toString(sds) + ")") + + (slts == null ? "" : ", lts(" + StringUtils.toString(slts) + ")") + + ", values(" + StringUtils.toString(vds) + ")" + + ", lts(" + StringUtils.toString(lts) + ")" + + ")"; + } + + public String toString(SchemaSpec schema) + { + return "resultSetRow(" + + pd + + "L, " + cd + + (sds == null ? "" : "L, staticValues(" + StringUtils.toString(sds) + ")") + + (slts == null ? "" : ", slts(" + StringUtils.toString(slts) + ")") + + ", values(" + StringUtils.toString(vds) + ")" + + ", lts(" + StringUtils.toString(lts) + ")" + + ", clustering=" + Arrays.toString(schema.inflateClusteringKey(cd)) + + ", values=" + Arrays.toString(schema.inflateRegularColumns(vds)) + + (sds == null ? "" : ", statics=" + Arrays.toString(schema.inflateStaticColumns(sds))) + + ")"; + } +} diff --git a/test/harry/main/org/apache/cassandra/harry/ddl/ColumnSpec.java b/test/harry/main/org/apache/cassandra/harry/ddl/ColumnSpec.java new file mode 100644 index 0000000000..5d1109335b --- /dev/null +++ b/test/harry/main/org/apache/cassandra/harry/ddl/ColumnSpec.java @@ -0,0 +1,438 @@ +/* + * 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.ddl; + +import java.util.Arrays; +import java.util.Collection; +import java.util.Collections; +import java.util.Date; +import java.util.HashMap; +import java.util.Map; +import java.util.Objects; +import java.util.UUID; + +import org.apache.cassandra.harry.gen.Bijections; +import org.apache.cassandra.harry.gen.StringBijection; + +public class ColumnSpec +{ + public final String name; + public final DataType type; + public final Kind kind; + int columnIndex; + + public ColumnSpec(String name, + DataType type, + Kind kind) + { + this.name = name; + this.type = type; + this.kind = kind; + } + + void setColumnIndex(int idx) + { + this.columnIndex = idx; + } + + public int getColumnIndex() + { + return columnIndex; + } + + public String toCQL() + { + return String.format("%s %s%s", + name, + type.toString(), + kind == Kind.STATIC ? " static" : ""); + } + + @Override + public boolean equals(Object o) + { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + ColumnSpec that = (ColumnSpec) o; + return Objects.equals(name, that.name) && + Objects.equals(type, that.type) && + kind == that.kind; + } + + @Override + public int hashCode() + { + return Objects.hash(name, type, kind); + } + + public String name() + { + return name; + } + + public boolean isReversed() + { + return type.isReversed(); + } + + public String toString() + { + return name + '(' + type.toString() + ")"; + } + + public Bijections.Bijection generator() + { + return type.generator(); + } + + public T inflate(long current) + { + return type.generator().inflate(current); + } + + public long deflate(T value) + { + return type.generator().deflate(value); + } + + public static ColumnSpec pk(String name, DataType type) + { + return new ColumnSpec<>(name, type, Kind.PARTITION_KEY); + } + + @SuppressWarnings("unchecked") + public static ColumnSpec ck(String name, DataType type, boolean isReversed) + { + return new ColumnSpec(name, isReversed ? ReversedType.getInstance(type) : type, Kind.CLUSTERING); + } + + @SuppressWarnings("unchecked") + public static ColumnSpec ck(String name, DataType type) + { + return new ColumnSpec(name, type, Kind.CLUSTERING); + } + + public static ColumnSpec regularColumn(String name, DataType type) + { + return new ColumnSpec<>(name, type, Kind.REGULAR); + } + + public static ColumnSpec staticColumn(String name, DataType type) + { + return new ColumnSpec<>(name, type, Kind.STATIC); + } + + public enum Kind + { + CLUSTERING, REGULAR, STATIC, PARTITION_KEY + } + + public static abstract class DataType + { + protected final String cqlName; + + protected DataType(String cqlName) + { + this.cqlName = cqlName; + } + + public boolean isReversed() + { + return false; + } + + /** + * Cassandra uses lexicographical oder for resolving timestamp ties + */ + public int compareLexicographically(long l, long r) + { + for (int i = Long.BYTES - 1; i >= 0; i--) + { + int cmp = Integer.compare((int) ((l >> (i * 8)) & 0xffL), + (int) ((r >> (i * 8)) & 0xffL)); + if (cmp != 0) + return cmp; + } + return 0; + } + + public abstract Bijections.Bijection generator(); + + public int maxSize() + { + return generator().byteSize(); + } + + public String toString() + { + return cqlName; + } + + public String nameForParser() + { + return cqlName; + } + + public boolean equals(Object o) + { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + DataType dataType = (DataType) o; + return Objects.equals(cqlName, dataType.cqlName); + } + + public int hashCode() + { + return Objects.hash(cqlName); + } + } + + public static final DataType int8Type = new DataType("tinyint") + { + public Bijections.Bijection generator() + { + return Bijections.INT8_GENERATOR; + } + }; + + public static final DataType int16Type = new DataType("smallint") + { + public Bijections.Bijection generator() + { + return Bijections.INT16_GENERATOR; + } + }; + + public static final DataType int32Type = new DataType("int") + { + public Bijections.Bijection generator() + { + return Bijections.INT32_GENERATOR; + } + }; + + public static final DataType int64Type = new DataType("bigint") + { + public Bijections.Bijection generator() + { + return Bijections.INT64_GENERATOR; + } + }; + + public static final DataType booleanType = new DataType("boolean") + { + public Bijections.Bijection generator() + { + return Bijections.BOOLEAN_GENERATOR; + } + + public int compareLexicographically(long l, long r) + { + throw new RuntimeException("Boolean does not support custom comparators"); + } + }; + + public static final DataType floatType = new DataType("float") + { + public Bijections.Bijection generator() + { + return Bijections.FLOAT_GENERATOR; + } + }; + + public static final DataType doubleType = new DataType("double") + { + public Bijections.Bijection generator() + { + return Bijections.DOUBLE_GENERATOR; + } + }; + + public static final DataType asciiType = new DataType("ascii") + { + private final Bijections.Bijection gen = new StringBijection(); + + public Bijections.Bijection generator() + { + return gen; + } + + public int compareLexicographically(long l, long r) + { + return Long.compare(l, r); + } + }; + + public static final DataType textType = new DataType("text") + { + private final Bijections.Bijection gen = new StringBijection(); + + public Bijections.Bijection generator() + { + return gen; + } + + public int compareLexicographically(long l, long r) + { + return Long.compare(l, r); + } + }; + + public static DataType asciiType(int nibbleSize, int maxRandomBytes) + { + Bijections.Bijection gen = new StringBijection(nibbleSize, maxRandomBytes); + + return new DataType("ascii") + { + public Bijections.Bijection generator() + { + return gen; + } + + public int compareLexicographically(long l, long r) + { + return Long.compare(l, r); + } + + public String nameForParser() + { + return String.format("%s(%d,%d)", + super.nameForParser(), + nibbleSize, + maxRandomBytes); + } + }; + } + + public static final DataType uuidType = new DataType("uuid") + { + public Bijections.Bijection generator() + { + return Bijections.UUID_GENERATOR; + } + + public int compareLexicographically(long l, long r) + { + throw new RuntimeException("UUID does not support custom comparators"); + } + }; + + public static final DataType timeUuidType = new DataType("timeuuid") + { + public Bijections.Bijection generator() + { + return Bijections.TIME_UUID_GENERATOR; + } + + public int compareLexicographically(long l, long r) + { + throw new RuntimeException("UUID does not support custom comparators"); + } + }; + + public static final DataType timestampType = new DataType("timestamp") + { + public Bijections.Bijection generator() + { + return Bijections.TIMESTAMP_GENERATOR; + } + + public int compareLexicographically(long l, long r) + { + throw new RuntimeException("Date does not support custom comparators"); + } + }; + + public static final Collection> DATA_TYPES = Collections.unmodifiableList( + Arrays.asList(ColumnSpec.int8Type, + ColumnSpec.int16Type, + ColumnSpec.int32Type, + ColumnSpec.int64Type, + ColumnSpec.booleanType, + ColumnSpec.floatType, + ColumnSpec.doubleType, + ColumnSpec.asciiType, + ColumnSpec.textType, + ColumnSpec.uuidType, + ColumnSpec.timeUuidType, + ColumnSpec.timestampType)); + + public static class ReversedType extends DataType + { + public static final Map, ReversedType> cache = new HashMap() + {{ + put(int8Type, new ReversedType<>(int8Type)); + put(int16Type, new ReversedType<>(int16Type)); + put(int32Type, new ReversedType<>(int32Type)); + put(int64Type, new ReversedType<>(int64Type)); + put(booleanType, new ReversedType<>(booleanType)); + put(floatType, new ReversedType<>(floatType, new Bijections.ReverseFloatGenerator())); + put(doubleType, new ReversedType<>(doubleType, new Bijections.ReverseDoubleGenerator())); + put(asciiType, new ReversedType<>(asciiType)); + put(uuidType, new ReversedType<>(uuidType)); + put(timeUuidType, new ReversedType<>(timeUuidType)); + }}; + + private final DataType baseType; + private final Bijections.Bijection generator; + + public ReversedType(DataType baseType) + { + super(baseType.cqlName); + this.baseType = baseType; + this.generator = new Bijections.ReverseBijection<>(baseType.generator()); + } + + public ReversedType(DataType baseType, Bijections.Bijection generator) + { + super(baseType.cqlName); + this.baseType = baseType; + this.generator = generator; + } + + public boolean isReversed() + { + return true; + } + + public Bijections.Bijection generator() + { + return generator; + } + + public int maxSize() + { + return baseType.maxSize(); + } + + public DataType baseType() + { + return baseType; + } + + public static DataType getInstance(DataType type) + { + ReversedType t = (ReversedType) cache.get(type); + if (t == null) + t = new ReversedType<>(type); + assert t.baseType == type : "Type mismatch"; + return t; + } + } +} \ No newline at end of file diff --git a/test/harry/main/org/apache/cassandra/harry/ddl/SchemaGenerators.java b/test/harry/main/org/apache/cassandra/harry/ddl/SchemaGenerators.java new file mode 100644 index 0000000000..1e5846a579 --- /dev/null +++ b/test/harry/main/org/apache/cassandra/harry/ddl/SchemaGenerators.java @@ -0,0 +1,512 @@ +/* + * 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.ddl; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collection; +import java.util.Collections; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.concurrent.atomic.AtomicInteger; +import java.util.function.Function; +import java.util.function.Supplier; + +import org.apache.cassandra.config.CassandraRelevantProperties; +import org.apache.cassandra.harry.gen.Generator; +import org.apache.cassandra.harry.gen.Surjections; + +public class SchemaGenerators +{ + private final static long SCHEMAGEN_STREAM_ID = 0x6264593273L; + + public static Builder schema(String ks) + { + return new Builder(ks); + } + + public static final Map> nameToTypeMap; + public static final Collection> columnTypes; + public static final Collection> partitionKeyTypes; + public static final Collection> clusteringKeyTypes; + + static + { + partitionKeyTypes = Collections.unmodifiableList(Arrays.asList(ColumnSpec.int8Type, + ColumnSpec.int16Type, + ColumnSpec.int32Type, + ColumnSpec.int64Type, + ColumnSpec.floatType, + ColumnSpec.doubleType, + ColumnSpec.asciiType, + ColumnSpec.textType)); + + columnTypes = Collections.unmodifiableList(Arrays.asList(ColumnSpec.int8Type, + ColumnSpec.int16Type, + ColumnSpec.int32Type, + ColumnSpec.int64Type, + ColumnSpec.floatType, + ColumnSpec.doubleType, + ColumnSpec.asciiType, + ColumnSpec.textType)); + + + List> builder = new ArrayList<>(partitionKeyTypes); + Map> mapBuilder = new HashMap<>(); + + for (ColumnSpec.DataType columnType : partitionKeyTypes) + { + ColumnSpec.DataType reversedType = ColumnSpec.ReversedType.getInstance(columnType); + builder.add(reversedType); + + mapBuilder.put(columnType.nameForParser(), columnType); + mapBuilder.put(String.format("desc(%s)", columnType.nameForParser()), columnType); + } + + builder.add(ColumnSpec.floatType); + builder.add(ColumnSpec.doubleType); + + clusteringKeyTypes = Collections.unmodifiableList(builder); + nameToTypeMap = Collections.unmodifiableMap(mapBuilder); + } + + @SuppressWarnings("unchecked") + public static Generator fromValues(Collection allValues) + { + return fromValues((T[]) allValues.toArray()); + } + + public static Generator fromValues(T[] allValues) + { + return (rng) -> { + return allValues[rng.nextInt(allValues.length - 1)]; + }; + } + + @SuppressWarnings("unchecked") + public static Generator> columnSpecGenerator(String prefix, ColumnSpec.Kind kind) + { + return fromValues(columnTypes) + .map(new Function, ColumnSpec>() + { + private int counter = 0; + + public ColumnSpec apply(ColumnSpec.DataType type) + { + return new ColumnSpec<>(prefix + (counter++), + type, + kind); + } + }); + } + + @SuppressWarnings("unchecked") + public static Generator> columnSpecGenerator(Collection> columnTypes, String prefix, ColumnSpec.Kind kind) + { + return fromValues(columnTypes) + .map(new Function, ColumnSpec>() + { + private int counter = 0; + + public ColumnSpec apply(ColumnSpec.DataType type) + { + return new ColumnSpec<>(String.format("%s%04d", prefix, counter++), + type, + kind); + } + }); + } + + @SuppressWarnings("unchecked") + public static Generator> clusteringColumnSpecGenerator(String prefix) + { + return fromValues(clusteringKeyTypes) + .map(new Function, ColumnSpec>() + { + private int counter = 0; + + public ColumnSpec apply(ColumnSpec.DataType type) + { + return ColumnSpec.ck(String.format("%s%04d", prefix, counter++), type); + } + }); + } + + @SuppressWarnings("unchecked") + public static Generator> partitionColumnSpecGenerator(String prefix) + { + return fromValues(partitionKeyTypes) + .map(new Function, ColumnSpec>() + { + private int counter = 0; + + public ColumnSpec apply(ColumnSpec.DataType type) + { + + return ColumnSpec.pk(String.format("%s%04d", prefix, counter++), + type); + } + }); + } + + private static AtomicInteger tableCounter = new AtomicInteger(1); + + public static class Builder + { + private final String keyspace; + private final Supplier tableNameSupplier; + + private Generator> pkGenerator = partitionColumnSpecGenerator("pk"); + private Generator> ckGenerator = clusteringColumnSpecGenerator("ck"); + private Generator> regularGenerator = columnSpecGenerator("regular", ColumnSpec.Kind.REGULAR); + private Generator> staticGenerator = columnSpecGenerator("static", ColumnSpec.Kind.STATIC); + + private int minPks = 1; + private int maxPks = 1; + private int minCks = 0; + private int maxCks = 0; + private int minRegular = 0; + private int maxRegular = 0; + private int minStatic = 0; + private int maxStatic = 0; + + public Builder(String keyspace) + { + this(keyspace, () -> "table_" + tableCounter.getAndIncrement()); + } + + public Builder(String keyspace, Supplier tableNameSupplier) + { + this.keyspace = keyspace; + this.tableNameSupplier = tableNameSupplier; + } + + public Builder partitionKeyColumnCount(int numCols) + { + return partitionKeyColumnCount(numCols, numCols); + } + + public Builder partitionKeyColumnCount(int minCols, int maxCols) + { + this.minPks = minCols; + this.maxPks = maxCols; + return this; + } + + public Builder partitionKeySpec(int minCols, int maxCols, ColumnSpec.DataType... columnTypes) + { + return partitionKeySpec(minCols, maxCols, Arrays.asList(columnTypes)); + } + + public Builder partitionKeySpec(int minCols, int maxCols, Collection> columnTypes) + { + this.minPks = minCols; + this.maxPks = maxCols; + this.pkGenerator = columnSpecGenerator(columnTypes, "pk", ColumnSpec.Kind.PARTITION_KEY); + return this; + } + + public Builder clusteringColumnCount(int numCols) + { + return clusteringColumnCount(numCols, numCols); + } + + public Builder clusteringColumnCount(int minCols, int maxCols) + { + this.minCks = minCols; + this.maxCks = maxCols; + return this; + } + + public Builder clusteringKeySpec(int minCols, int maxCols, ColumnSpec.DataType... columnTypes) + { + return clusteringKeySpec(minCols, maxCols, Arrays.asList(columnTypes)); + } + + public Builder clusteringKeySpec(int minCols, int maxCols, Collection> columnTypes) + { + this.minCks = minCols; + this.maxCks = maxCols; + this.ckGenerator = columnSpecGenerator(columnTypes, "ck", ColumnSpec.Kind.CLUSTERING); + return this; + } + + public Builder regularColumnCount(int minCols, int maxCols) + { + this.minRegular = minCols; + this.maxRegular = maxCols; + return this; + } + + public Builder regularColumnCount(int numCols) + { + return regularColumnCount(numCols, numCols); + } + + public Builder regularColumnSpec(int minCols, int maxCols, ColumnSpec.DataType... columnTypes) + { + return this.regularColumnSpec(minCols, maxCols, Arrays.asList(columnTypes)); + } + + public Builder regularColumnSpec(int minCols, int maxCols, Collection> columnTypes) + { + this.minRegular = minCols; + this.maxRegular = maxCols; + this.regularGenerator = columnSpecGenerator(columnTypes, "regular", ColumnSpec.Kind.REGULAR); + return this; + } + + public Builder staticColumnCount(int minCols, int maxCols) + { + this.minStatic = minCols; + this.maxStatic = maxCols; + return this; + } + + public Builder staticColumnCount(int numCols) + { + return staticColumnCount(numCols, numCols); + } + + public Builder staticColumnSpec(int minCols, int maxCols, ColumnSpec.DataType... columnTypes) + { + return this.staticColumnSpec(minCols, maxCols, Arrays.asList(columnTypes)); + } + + public Builder staticColumnSpec(int minCols, int maxCols, Collection> columnTypes) + { + this.minStatic = minCols; + this.maxStatic = maxCols; + this.staticGenerator = columnSpecGenerator(columnTypes, "static", ColumnSpec.Kind.STATIC); + return this; + } + + private static class ColumnCounts + { + private final int pks; + private final int cks; + private final int regulars; + private final int statics; + + private ColumnCounts(int pks, int cks, int regulars, int statics) + { + this.pks = pks; + this.cks = cks; + this.regulars = regulars; + this.statics = statics; + } + } + + public Generator columnCountsGenerator() + { + return (rand) -> { + int pks = rand.nextInt(minPks, maxPks); + int cks = rand.nextInt(minCks, maxCks); + int regulars = rand.nextInt(minRegular, maxRegular); + int statics = rand.nextInt(minStatic, maxStatic); + + return new ColumnCounts(pks, cks, regulars, statics); + }; + } + + public Generator generator() + { + Generator columnCountsGenerator = columnCountsGenerator(); + + return columnCountsGenerator.flatMap(counts -> { + return rand -> { + List> pk = pkGenerator.generate(rand, counts.pks); + List> ck = ckGenerator.generate(rand, counts.cks); + return new SchemaSpec(keyspace, + tableNameSupplier.get(), + pk, + ck, + regularGenerator.generate(rand, counts.regulars), + staticGenerator.generate(rand, counts.statics)); + }; + }); + } + + public Surjections.Surjection surjection() + { + return generator().toSurjection(SCHEMAGEN_STREAM_ID); + } + } + + public static Surjections.Surjection defaultSchemaSpecGen(String table) + { + return new SchemaGenerators.Builder(DEFAULT_KEYSPACE_NAME, () -> table) + .partitionKeySpec(1, 3, + partitionKeyTypes) + .clusteringKeySpec(1, 3, + clusteringKeyTypes) + .regularColumnSpec(3, 5, + ColumnSpec.int8Type, + ColumnSpec.int16Type, + ColumnSpec.int32Type, + ColumnSpec.int64Type, + ColumnSpec.floatType, + ColumnSpec.doubleType, + ColumnSpec.asciiType(5, 256)) + .staticColumnSpec(3, 5, + ColumnSpec.int8Type, + ColumnSpec.int16Type, + ColumnSpec.int32Type, + ColumnSpec.int64Type, + ColumnSpec.floatType, + ColumnSpec.doubleType, + ColumnSpec.asciiType(4, 512), + ColumnSpec.asciiType(4, 2048)) + .surjection(); + } + + public static String DEFAULT_KEYSPACE_NAME = "harry"; + + private static final String DEFAULT_PREFIX = "table_"; + private static final AtomicInteger counter = new AtomicInteger(); + private static final Supplier tableNameSupplier = () -> DEFAULT_PREFIX + counter.getAndIncrement(); + + // simplest schema gen, nothing can go wrong with it + public static final Surjections.Surjection longOnlySpecBuilder = new Builder(DEFAULT_KEYSPACE_NAME, tableNameSupplier) + .partitionKeySpec(1, 1, ColumnSpec.int64Type) + .clusteringKeySpec(1, 1, ColumnSpec.int64Type) + .regularColumnSpec(1, 10, ColumnSpec.int64Type) + .staticColumnSpec(1, 10, ColumnSpec.int64Type) + .surjection(); + + private static final ColumnSpec.DataType simpleStringType = ColumnSpec.asciiType(4, 10); + private static final Surjections.Surjection longAndStringSpecBuilder = new SchemaGenerators.Builder(DEFAULT_KEYSPACE_NAME, tableNameSupplier) + .partitionKeySpec(2, 2, ColumnSpec.int64Type, simpleStringType) + .clusteringKeySpec(2, 2, ColumnSpec.int64Type, simpleStringType) + .regularColumnSpec(1, 10, ColumnSpec.int64Type, simpleStringType) + .staticColumnSpec(1, 10, ColumnSpec.int64Type) + .surjection(); + + public static final Surjections.Surjection longOnlyWithReverseSpecBuilder = new SchemaGenerators.Builder(DEFAULT_KEYSPACE_NAME, tableNameSupplier) + .partitionKeySpec(1, 1, ColumnSpec.int64Type) + .clusteringKeySpec(1, 1, ColumnSpec.ReversedType.getInstance(ColumnSpec.int64Type)) + .regularColumnSpec(1, 10, ColumnSpec.int64Type) + .staticColumnSpec(1, 10, ColumnSpec.int64Type) + .surjection(); + + public static final Surjections.Surjection longAndStringSpecWithReversedLongBuilder = new SchemaGenerators.Builder(DEFAULT_KEYSPACE_NAME, tableNameSupplier) + .partitionKeySpec(2, 2, ColumnSpec.int64Type, simpleStringType) + .clusteringKeySpec(2, 2, ColumnSpec.ReversedType.getInstance(ColumnSpec.int64Type), simpleStringType) + .regularColumnSpec(1, 10, ColumnSpec.int64Type, simpleStringType) + .staticColumnSpec(1, 10, ColumnSpec.int64Type) + .surjection(); + + public static final Surjections.Surjection longAndStringSpecWithReversedStringBuilder = new SchemaGenerators.Builder(DEFAULT_KEYSPACE_NAME, tableNameSupplier) + .partitionKeySpec(2, 2, ColumnSpec.int64Type, simpleStringType) + .clusteringKeySpec(2, 2, ColumnSpec.int64Type, ColumnSpec.ReversedType.getInstance(simpleStringType)) + .regularColumnSpec(1, 10, ColumnSpec.int64Type, simpleStringType) + .staticColumnSpec(1, 10, ColumnSpec.int64Type) + .surjection(); + + public static final Surjections.Surjection longAndStringSpecWithReversedBothBuilder = new SchemaGenerators.Builder(DEFAULT_KEYSPACE_NAME, tableNameSupplier) + .partitionKeySpec(2, 2, ColumnSpec.int64Type, simpleStringType) + .clusteringKeySpec(2, 2, ColumnSpec.ReversedType.getInstance(ColumnSpec.int64Type), ColumnSpec.ReversedType.getInstance(simpleStringType)) + .regularColumnSpec(1, 10, ColumnSpec.int64Type, simpleStringType) + .staticColumnSpec(1, 10, ColumnSpec.int64Type) + .surjection(); + + public static final Surjections.Surjection withAllFeaturesEnabled = new SchemaGenerators.Builder(DEFAULT_KEYSPACE_NAME, tableNameSupplier) + .partitionKeySpec(1, 4, columnTypes) + .clusteringKeySpec(1, 4, clusteringKeyTypes) + .regularColumnSpec(1, 10, columnTypes) + .surjection(); + + public static final Surjections.Surjection[] PROGRESSIVE_GENERATORS = new Surjections.Surjection[]{ + longOnlySpecBuilder, + longAndStringSpecBuilder, + longOnlyWithReverseSpecBuilder, + longAndStringSpecWithReversedLongBuilder, + longAndStringSpecWithReversedStringBuilder, + longAndStringSpecWithReversedBothBuilder, + withAllFeaturesEnabled + }; + + // Create schema generators that would produce tables starting with just a few features, progressing to use more + public static Supplier progression(int switchAfter) + { + Supplier[] generators = new Supplier[PROGRESSIVE_GENERATORS.length]; + for (int i = 0; i < generators.length; i++) + generators[i] = PROGRESSIVE_GENERATORS[i].toSupplier(); + + return new Supplier() + { + private int counter = 0; + public SchemaSpec get() + { + int idx = (counter / switchAfter) % generators.length; + counter++; + SchemaSpec spec = generators[idx].get(); + int tries = 100; + while ((spec.pkGenerator.byteSize() != Long.BYTES) && tries > 0) + { + System.out.println("Skipping schema, since it doesn't have enough entropy bits available: " + spec.compile().cql()); + spec = generators[idx].get(); + tries--; + } + + spec.validate(); + + assert tries > 0 : String.format("Max number of tries exceeded on generator %d, can't generate a needed schema", idx); + return spec; + } + }; + } + + public static List> toColumns(Map config, ColumnSpec.Kind kind, boolean allowReverse) + { + if (config == null) + return Collections.EMPTY_LIST; + + List> columns = new ArrayList<>(config.size()); + + for (Map.Entry e : config.entrySet()) + { + ColumnSpec.DataType type = nameToTypeMap.get(e.getValue()); + assert type != null : "Can't parse the type"; + assert allowReverse || !type.isReversed() : String.format("%s columns aren't allowed to be reversed"); + columns.add(new ColumnSpec<>(e.getKey(), type, kind)); + } + + return columns; + } + + public static SchemaSpec parse(String keyspace, + String table, + Map pks, + Map cks, + Map regulars, + Map statics) + { + return new SchemaSpec(keyspace, table, + toColumns(pks, ColumnSpec.Kind.PARTITION_KEY, false), + toColumns(cks, ColumnSpec.Kind.CLUSTERING, false), + toColumns(regulars, ColumnSpec.Kind.REGULAR, false), + toColumns(statics, ColumnSpec.Kind.STATIC, false)); + } + + public static int DEFAULT_SWITCH_AFTER = CassandraRelevantProperties.TEST_HARRY_SWITCH_AFTER.getInt(); + public static int GENERATORS_COUNT = PROGRESSIVE_GENERATORS.length; + public static int DEFAULT_RUNS = DEFAULT_SWITCH_AFTER * GENERATORS_COUNT; +} diff --git a/test/harry/main/org/apache/cassandra/harry/ddl/SchemaSpec.java b/test/harry/main/org/apache/cassandra/harry/ddl/SchemaSpec.java new file mode 100644 index 0000000000..2ee0c50add --- /dev/null +++ b/test/harry/main/org/apache/cassandra/harry/ddl/SchemaSpec.java @@ -0,0 +1,492 @@ +/* + * 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.ddl; + +import java.util.*; +import java.util.function.Consumer; + +import org.apache.cassandra.harry.gen.DataGenerators; +import org.apache.cassandra.harry.sut.SystemUnderTest; +import org.apache.cassandra.harry.operations.CompiledStatement; +import org.apache.cassandra.harry.operations.Relation; +import org.apache.cassandra.harry.util.BitSet; + +public class SchemaSpec +{ + public interface SchemaSpecFactory + { + public SchemaSpec make(long seed, SystemUnderTest sut); + } + + public final DataGenerators.KeyGenerator pkGenerator; + public final DataGenerators.KeyGenerator ckGenerator; + + private final boolean isCompactStorage; + public final boolean trackLts; + + // These fields are immutable, and are safe as public + public final String keyspace; + public final String table; + + public final List> partitionKeys; + public final List> clusteringKeys; + public final List> regularColumns; + public final List> staticColumns; + public final List> allColumns; + public final Set> allColumnsSet; + + public final BitSet ALL_COLUMNS_BITSET; + public final int regularColumnsOffset; + public final int staticColumnsOffset; + public final BitSet regularColumnsMask; + public final BitSet regularAndStaticColumnsMask; + public final BitSet staticColumnsMask; + + public SchemaSpec(String keyspace, + String table, + List> partitionKeys, + List> clusteringKeys, + List> regularColumns, + List> staticColumns) + { + this(keyspace, table, partitionKeys, clusteringKeys, regularColumns, staticColumns, false, false); + } + + public SchemaSpec cloneWithName(String ks, + String table) + { + return new SchemaSpec(ks, table, partitionKeys, clusteringKeys, regularColumns, staticColumns, isCompactStorage, trackLts); + } + + public SchemaSpec trackLts() + { + return new SchemaSpec(keyspace, table, partitionKeys, clusteringKeys, regularColumns, staticColumns, isCompactStorage, true); + } + + public SchemaSpec withCompactStorage() + { + return new SchemaSpec(keyspace, table, partitionKeys, clusteringKeys, regularColumns, staticColumns, true, trackLts); + } + + public SchemaSpec(String keyspace, + String table, + List> partitionKeys, + List> clusteringKeys, + List> regularColumns, + List> staticColumns, + boolean isCompactStorage, + boolean trackLts) + { + assert !isCompactStorage || clusteringKeys.size() == 0 || regularColumns.size() <= 1; + + this.keyspace = keyspace; + this.table = table; + this.isCompactStorage = isCompactStorage; + + this.partitionKeys = Collections.unmodifiableList(new ArrayList<>(partitionKeys)); + for (int i = 0; i < partitionKeys.size(); i++) + partitionKeys.get(i).setColumnIndex(i); + this.clusteringKeys = Collections.unmodifiableList(new ArrayList<>(clusteringKeys)); + for (int i = 0; i < clusteringKeys.size(); i++) + clusteringKeys.get(i).setColumnIndex(i); + this.staticColumns = Collections.unmodifiableList(new ArrayList<>(staticColumns)); + for (int i = 0; i < staticColumns.size(); i++) + staticColumns.get(i).setColumnIndex(i); + this.regularColumns = Collections.unmodifiableList(new ArrayList<>(regularColumns)); + for (int i = 0; i < regularColumns.size(); i++) + regularColumns.get(i).setColumnIndex(i); + + List> all = new ArrayList<>(); + for (ColumnSpec columnSpec : concat(partitionKeys, + clusteringKeys, + staticColumns, + regularColumns)) + { + all.add(columnSpec); + } + this.allColumns = Collections.unmodifiableList(all); + this.allColumnsSet = Collections.unmodifiableSet(new LinkedHashSet<>(all)); + + this.pkGenerator = DataGenerators.createKeyGenerator(partitionKeys); + this.ckGenerator = DataGenerators.createKeyGenerator(clusteringKeys); + + this.ALL_COLUMNS_BITSET = BitSet.allSet(regularColumns.size()); + + this.staticColumnsOffset = partitionKeys.size() + clusteringKeys.size(); + this.regularColumnsOffset = staticColumnsOffset + staticColumns.size(); + + this.regularColumnsMask = regularColumnsMask(this); + this.regularAndStaticColumnsMask = regularAndStaticColumnsMask(this); + this.staticColumnsMask = staticColumnsMask(this); + this.trackLts = trackLts; + } + + public static BitSet allColumnsMask(SchemaSpec schema) + { + return BitSet.allSet(schema.allColumns.size()); + } + + public BitSet regularColumnsMask() + { + return this.regularColumnsMask; + } + + public BitSet regularAndStaticColumnsMask() + { + return this.regularAndStaticColumnsMask; + } + + public BitSet staticColumnsMask() + { + return this.staticColumnsMask; + } + + private static BitSet regularColumnsMask(SchemaSpec schema) + { + BitSet mask = BitSet.allUnset(schema.allColumns.size()); + for (int i = 0; i < schema.regularColumns.size(); i++) + mask.set(schema.regularColumnsOffset + i); + return mask; + } + + private static BitSet regularAndStaticColumnsMask(SchemaSpec schema) + { + BitSet mask = BitSet.allUnset(schema.allColumns.size()); + for (int i = 0; i < schema.staticColumns.size() + schema.regularColumns.size(); i++) + mask.set(schema.staticColumnsOffset + i); + return mask; + } + + private static BitSet staticColumnsMask(SchemaSpec schema) + { + BitSet mask = BitSet.allUnset(schema.allColumns.size()); + for (int i = 0; i < schema.staticColumns.size(); i++) + mask.set(schema.staticColumnsOffset + i); + return mask; + } + + public void validate() + { + assert pkGenerator.byteSize() == Long.BYTES : partitionKeys.toString(); + } + + public interface AddRelationCallback + { + void accept(ColumnSpec spec, Relation.RelationKind kind, Object value); + } + + public void inflateRelations(long pd, + List clusteringRelations, + AddRelationCallback consumer) + { + Object[] pk = inflatePartitionKey(pd); + for (int i = 0; i < pk.length; i++) + consumer.accept(partitionKeys.get(i), Relation.RelationKind.EQ, pk[i]); + + inflateRelations(clusteringRelations, consumer); + } + + public void inflateRelations(List clusteringRelations, + AddRelationCallback consumer) + { + for (Relation r : clusteringRelations) + consumer.accept(r.columnSpec, r.kind, r.value()); + } + + public Object[] inflatePartitionKey(long pd) + { + return pkGenerator.inflate(pd); + } + + public Object[] inflateClusteringKey(long cd) + { + return ckGenerator.inflate(cd); + } + + public Object[] inflateRegularColumns(long[] vds) + { + return DataGenerators.inflateData(regularColumns, vds); + } + + public Object[] inflateStaticColumns(long[] sds) + { + return DataGenerators.inflateData(staticColumns, sds); + } + + public long adjustPdEntropy(long descriptor) + { + return pkGenerator.adjustEntropyDomain(descriptor); + } + + public long adjustCdEntropy(long descriptor) + { + return ckGenerator.adjustEntropyDomain(descriptor); + } + + public long deflatePartitionKey(Object[] pk) + { + return pkGenerator.deflate(pk); + } + + public long deflateClusteringKey(Object[] ck) + { + return ckGenerator.deflate(ck); + } + + public long[] deflateStaticColumns(Object[] statics) + { + return DataGenerators.deflateData(staticColumns, statics); + } + + public long[] deflateRegularColumns(Object[] regulars) + { + return DataGenerators.deflateData(regularColumns, regulars); + } + + public CompiledStatement compile() + { + StringBuilder sb = new StringBuilder(); + + sb.append("CREATE TABLE IF NOT EXISTS "); + sb.append(keyspace) + .append(".") + .append(table) + .append(" ("); + + SeparatorAppender commaAppender = new SeparatorAppender(); + for (ColumnSpec cd : partitionKeys) + { + commaAppender.accept(sb); + sb.append(cd.toCQL()); + if (partitionKeys.size() == 1 && clusteringKeys.size() == 0) + sb.append(" PRIMARY KEY"); + } + + for (ColumnSpec cd : concat(clusteringKeys, + staticColumns, + regularColumns)) + { + commaAppender.accept(sb); + sb.append(cd.toCQL()); + } + + if (clusteringKeys.size() > 0 || partitionKeys.size() > 1) + { + sb.append(", ").append(getPrimaryKeyCql()); + } + + if (trackLts) + sb.append(", ").append("visited_lts list static"); + + sb.append(')'); + + Runnable appendWith = doOnce(() -> sb.append(" WITH ")); + + if (isCompactStorage) + { + appendWith.run(); + sb.append("COMPACT STORAGE AND"); + } + + if (clusteringKeys.size() > 0) + { + appendWith.run(); + sb.append(getClusteringOrderCql()) + .append(';'); + } + + return new CompiledStatement(sb.toString()); + } + + private String getClusteringOrderCql() + { + StringBuilder sb = new StringBuilder(); + if (clusteringKeys.size() > 0) + { + sb.append(" CLUSTERING ORDER BY ("); + + SeparatorAppender commaAppender = new SeparatorAppender(); + for (ColumnSpec column : clusteringKeys) + { + commaAppender.accept(sb); + sb.append(column.name).append(' ').append(column.isReversed() ? "DESC" : "ASC"); + } + + sb.append(")"); + } + + return sb.toString(); + } + + private String getPrimaryKeyCql() + { + StringBuilder sb = new StringBuilder(); + sb.append("PRIMARY KEY ("); + if (partitionKeys.size() > 1) + { + sb.append('('); + SeparatorAppender commaAppender = new SeparatorAppender(); + for (ColumnSpec cd : partitionKeys) + { + commaAppender.accept(sb); + sb.append(cd.name); + } + sb.append(')'); + } + else + { + sb.append(partitionKeys.get(0).name); + } + + for (ColumnSpec cd : clusteringKeys) + sb.append(", ").append(cd.name); + + return sb.append(')').toString(); + } + + public String toString() + { + return String.format("schema {cql=%s, columns=%s}", compile().toString(), allColumns); + } + + private static Runnable doOnce(Runnable r) + { + return new Runnable() + { + boolean executed = false; + + public void run() + { + if (executed) + return; + + executed = true; + r.run(); + } + }; + } + + public static class SeparatorAppender implements Consumer + { + boolean isFirst = true; + private final String separator; + + public SeparatorAppender() + { + this(","); + } + + public SeparatorAppender(String separator) + { + this.separator = separator; + } + + public void accept(StringBuilder stringBuilder) + { + if (isFirst) + isFirst = false; + else + stringBuilder.append(separator); + } + + public void accept(StringBuilder stringBuilder, String s) + { + accept(stringBuilder); + stringBuilder.append(s); + } + + + public void reset() + { + isFirst = true; + } + } + + public boolean equals(Object o) + { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + SchemaSpec that = (SchemaSpec) o; + return Objects.equals(keyspace, that.keyspace) && + Objects.equals(table, that.table) && + Objects.equals(partitionKeys, that.partitionKeys) && + Objects.equals(clusteringKeys, that.clusteringKeys) && + Objects.equals(regularColumns, that.regularColumns); + } + + public int hashCode() + { + return Objects.hash(keyspace, table, partitionKeys, clusteringKeys, regularColumns); + } + + public static Iterable concat(Iterable... iterables) + { + assert iterables != null && iterables.length > 0; + if (iterables.length == 1) + return iterables[0]; + + return () -> { + return new Iterator() + { + int idx; + Iterator current; + boolean hasNext; + + { + idx = 0; + prepareNext(); + } + + private void prepareNext() + { + if (current != null && current.hasNext()) + { + hasNext = true; + return; + } + + while (idx < iterables.length) + { + current = iterables[idx].iterator(); + idx++; + if (current.hasNext()) + { + hasNext = true; + return; + } + } + + hasNext = false; + } + + public boolean hasNext() + { + return hasNext; + } + + public T next() + { + T next = current.next(); + prepareNext(); + return next; + } + }; + }; + } +} diff --git a/test/harry/main/org/apache/cassandra/harry/dsl/BatchOperationBuilder.java b/test/harry/main/org/apache/cassandra/harry/dsl/BatchOperationBuilder.java new file mode 100644 index 0000000000..50321fb398 --- /dev/null +++ b/test/harry/main/org/apache/cassandra/harry/dsl/BatchOperationBuilder.java @@ -0,0 +1,32 @@ +/* + * 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.dsl; + +public interface BatchOperationBuilder +{ + SingleOperationBuilder beginBatch(); + + /** + * Begin batch for a partition descriptor at a specific index. + * + * Imagine all partition descriptors were longs in an array. Index of a descriptor + * is a sequential number of the descriptor in this imaginary array. + */ + SingleOperationBuilder beginBatch(long pdIdx); +} diff --git a/test/harry/main/org/apache/cassandra/harry/dsl/BatchVisitBuilder.java b/test/harry/main/org/apache/cassandra/harry/dsl/BatchVisitBuilder.java new file mode 100644 index 0000000000..f85e09e721 --- /dev/null +++ b/test/harry/main/org/apache/cassandra/harry/dsl/BatchVisitBuilder.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.harry.dsl; + +import java.io.Closeable; +import java.util.function.Consumer; + +import org.apache.cassandra.harry.ddl.SchemaSpec; +import org.apache.cassandra.harry.model.OpSelectors; +import org.apache.cassandra.harry.visitors.ReplayingVisitor; + +public class BatchVisitBuilder extends SingleOperationVisitBuilder implements Closeable +{ + private final HistoryBuilder historyBuilder; + + public BatchVisitBuilder(HistoryBuilder historyBuilder, + PartitionVisitState partitionState, + long lts, + OpSelectors.PureRng rng, + OpSelectors.DescriptorSelector descriptorSelector, + SchemaSpec schemaSpec, + Consumer appendToLog) + { + super(partitionState, lts, rng, descriptorSelector, schemaSpec, appendToLog); + this.historyBuilder = historyBuilder; + } + + @Override + public int size() + { + return super.size(); + } + + @Override + public BatchVisitBuilder insert() + { + super.insert(); + return this; + } + + public BatchVisitBuilder inserts(int n) + { + assert n > 0; + for (int i = 0; i < n; i++) + insert(); + return this; + } + + @Override + public BatchVisitBuilder insert(int rowIdx) + { + super.insert(rowIdx); + return this; + } + + @Override + public BatchVisitBuilder insert(int rowIdx, long[] vds) + { + super.insert(rowIdx, vds); + return this; + } + + @Override + public BatchVisitBuilder deletePartition() + { + super.deletePartition(); + return this; + } + + @Override + public BatchVisitBuilder deleteRow() + { + super.deleteRow(); + return this; + } + + @Override + public BatchVisitBuilder deleteColumns() + { + super.deleteColumns(); + return this; + } + + @Override + public BatchVisitBuilder deleteRowRange() + { + super.deleteRowRange(); + return this; + } + + @Override + public BatchVisitBuilder deleteRowRange(int lowBoundRowIdx, int highBoundRowIdx, boolean isMinEq, boolean isMaxEq) + { + super.deleteRowRange(lowBoundRowIdx, highBoundRowIdx, isMinEq, isMaxEq); + return this; + } + + @Override + public BatchVisitBuilder deleteRowSlice() + { + super.deleteRowSlice(); + return this; + } + + // TODO: prevent from closing more than once + public HistoryBuilder endBatch() + { + super.end(); + return this.historyBuilder; + } + + /** + * Implements closeable to instruct users to end batch before using. + * + * Non-finished batches are _not_ appended to the history and will appear as gaps in history. + */ + @Override + public void close() + { + endBatch(); + } +} \ No newline at end of file diff --git a/test/harry/main/org/apache/cassandra/harry/dsl/HistoryBuilder.java b/test/harry/main/org/apache/cassandra/harry/dsl/HistoryBuilder.java new file mode 100644 index 0000000000..a119ac63ea --- /dev/null +++ b/test/harry/main/org/apache/cassandra/harry/dsl/HistoryBuilder.java @@ -0,0 +1,598 @@ +/* + * 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.dsl; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collection; +import java.util.HashMap; +import java.util.HashSet; +import java.util.Iterator; +import java.util.List; +import java.util.Map; +import java.util.NavigableSet; +import java.util.Set; +import java.util.TreeSet; +import java.util.function.LongSupplier; + +import org.apache.cassandra.harry.clock.ApproximateClock; +import org.apache.cassandra.harry.core.Configuration; +import org.apache.cassandra.harry.core.MetricReporter; +import org.apache.cassandra.harry.ddl.SchemaSpec; +import org.apache.cassandra.harry.gen.EntropySource; +import org.apache.cassandra.harry.gen.rng.JdkRandomEntropySource; +import org.apache.cassandra.harry.model.Model; +import org.apache.cassandra.harry.model.OpSelectors; +import org.apache.cassandra.harry.model.QuiescentChecker; +import org.apache.cassandra.harry.model.reconciler.Reconciler; +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.sut.injvm.QuiescentLocalStateChecker; +import org.apache.cassandra.harry.tracker.DataTracker; +import org.apache.cassandra.harry.visitors.MutatingRowVisitor; +import org.apache.cassandra.harry.visitors.MutatingVisitor; +import org.apache.cassandra.harry.visitors.ReplayingVisitor; +import org.apache.cassandra.harry.visitors.VisitExecutor; + +/** + * History builder is a component for a simple yet flexible generation of arbitrary data. You can write queries + * _as if_ you were writing them with primitive values (such as 0,1, and using for loops, and alike). + * + * You can create a history builder like: + * + * HistoryBuilder historyBuilder = new HistoryBuilder(seed, maxPartitionSize, 10, schema); + * + * The core idea is that you use simple-to-remember numbers as placeholders for your values. For partition key, + * the value (such as 0,1,2,...) would signify a distinct partition key for a given schema. You can not know in + * advance the relative order of two generated partition keys (i.e. how they'd sort). + * + * For clustering keys, the value 0 signifies the smallest possible-to-generate clustering _for this partition_ + * (i.e. there may be other values that would sort LT relative to it, but they will never be generated in this + * context. Similarly, `maxPartitionSize - 1` is going to be the largest possible-to-generate clustering _for this + * partition_). All other values (i.e. between 0 and maxPartitionSize - 1) that will be generated are ordered in + * the same way as the numbers you used to generate them. This is done for your convenience and being able to create + * complex/interesting RT queries. + * + * You can also go arbitrarily deep into specifying details of your query. For example, calling + * + * historyBuilder.insert(); + * + * Will generate an INSERT query, according to the given schema, for a random partition, random clustering, with + * random values. At the same time, calling: + * + * historyBuilder.visitPartition(1).insert(); + * + * Will generate an insert for a partition whose partition key is under index 1 (generating other writes prefixed + * with `visitPartition(1)` will ensure operations are executed against the same partition). Clustering and + * values are still going to be random. Calling: + * + * historyBuilder.visitPartition(1).insert(2); + * + * Will generate an insert for a partition whose partition key is under index 1, and the clustering will be third- + * largest possible clustering for this partition (remember, 0 is smallest, so 0,1,2 - third). Values inserted into + * this row are still going to be random. + * + * Lastly, calling + * + * historyBuilder.visitPartition(1).insert(2, new long[] { 1, 2 }); + * + * Will generate an insert to 1st partition, 2nd row, and the values are going to be taken from the random + * streams for the values for corresponding columns. + * + * Other possible operations are deleteRow, deleteColumns, deleteRowRange, deleteRowSlide, and deletePartition. + */ +public class HistoryBuilder implements Iterable, SingleOperationBuilder, BatchOperationBuilder +{ + protected final SchemaSpec schema; + protected final TokenPlacementModel.ReplicationFactor rf; + + protected final OpSelectors.PureRng pureRng; + protected final OpSelectors.DescriptorSelector descriptorSelector; + + // TODO: would be great to have a very simple B-Tree here + protected final Map log; + + // TODO: primitive array with a custom/noncopying growth strat + protected final Map partitionStates = new HashMap<>(); + + /** + * A selector that is going to be used by the model checker. + */ + public final PresetPdSelector presetSelector; + + /** + * Default selector will select every partition exactly once. + */ + protected final OpSelectors.DefaultPdSelector defaultSelector; + protected final OffsetClock clock; + protected final int maxPartitionSize; + + public HistoryBuilder(long seed, + int maxPartitionSize, + int interleaveWindowSize, + SchemaSpec schema, + TokenPlacementModel.ReplicationFactor rf) + { + this.maxPartitionSize = maxPartitionSize; + this.log = new HashMap<>(); + this.pureRng = new OpSelectors.PCGFast(seed); + + this.schema = schema; + this.rf = rf; + + // TODO: make clock pluggable + this.clock = new OffsetClock(ApproximateClock.START_VALUE, + interleaveWindowSize, + new JdkRandomEntropySource(seed)); + + this.presetSelector = new PresetPdSelector(); + this.defaultSelector = new OpSelectors.DefaultPdSelector(pureRng, 1, 1); + + this.descriptorSelector = new Configuration.CDSelectorConfigurationBuilder() + .setOperationsPerLtsDistribution(new Configuration.ConstantDistributionConfig(Integer.MAX_VALUE)) + .setMaxPartitionSize(maxPartitionSize) + .build() + .make(pureRng, schema); + } + + public SchemaSpec schema() + { + return schema; + } + + public int size() + { + return log.size(); + } + + public OpSelectors.Clock clock() + { + return clock; + } + /** + * Visited partition descriptors _not_ in the order they were visited + */ + public List visitedPds() + { + return new ArrayList<>(partitionStates.keySet()); + } + + @Override + public Iterator iterator() + { + return log.values().iterator(); + } + + protected SingleOperationVisitBuilder singleOpVisitBuilder() + { + long visitLts = clock.nextLts(); + return singleOpVisitBuilder(defaultSelector.pd(visitLts, schema), visitLts); + } + + protected SingleOperationVisitBuilder singleOpVisitBuilder(long pd, long lts) + { + PartitionVisitState partitionState = presetSelector.register(lts, pd); + return new SingleOperationVisitBuilder(partitionState, lts, pureRng, descriptorSelector, schema, (visit) -> { + log.put(visit.lts, visit); + }); + } + + @Override + public HistoryBuilder insert() + { + singleOpVisitBuilder().insert(); + return this; + } + + @Override + public HistoryBuilder insert(int rowId) + { + singleOpVisitBuilder().insert(rowId); + return this; + } + + @Override + public HistoryBuilder insert(int rowId, long[] vds) + { + singleOpVisitBuilder().insert(rowId, vds); + return this; + } + + public SingleOperationBuilder insert(int rowIdx, long[] vds, long[] sds) + { + singleOpVisitBuilder().insert(rowIdx, vds, sds); + return this; + } + + @Override + public HistoryBuilder deletePartition() + { + singleOpVisitBuilder().deletePartition(); + return this; + } + + @Override + public HistoryBuilder deleteRow() + { + singleOpVisitBuilder().deleteRow(); + return this; + } + + @Override + public HistoryBuilder deleteRow(int rowIdx) + { + singleOpVisitBuilder().deleteRow(rowIdx); + return this; + } + + @Override + public HistoryBuilder deleteColumns() + { + singleOpVisitBuilder().deleteColumns(); + return this; + } + + @Override + public HistoryBuilder deleteRowRange() + { + singleOpVisitBuilder().deleteRowRange(); + return this; + } + + @Override + public HistoryBuilder deleteRowRange(int lowBoundRowIdx, int highBoundRowIdx, boolean isMinEq, boolean isMaxEq) + { + singleOpVisitBuilder().deleteRowRange(lowBoundRowIdx, highBoundRowIdx, isMinEq, isMaxEq); + return this; + } + + @Override + public HistoryBuilder deleteRowSlice() + { + singleOpVisitBuilder().deleteRowSlice(); + return this; + } + + @Override + public BatchVisitBuilder beginBatch() + { + long visitLts = clock.nextLts(); + return batchVisitBuilder(defaultSelector.pd(visitLts, schema), visitLts); + } + + /** + * Begin batch for a partition descriptor at a specific index. + * + * Imagine all partition descriptors were longs in an array. Index of a descriptor + * is a sequential number of the descriptor in this imaginary array. + */ + @Override + public BatchVisitBuilder beginBatch(long pdIdx) + { + long visitLts = clock.nextLts(); + return batchVisitBuilder(presetSelector.pdAtPosition(pdIdx), visitLts); + } + + protected BatchVisitBuilder batchVisitBuilder(long pd, long lts) + { + PartitionVisitState partitionState = presetSelector.register(lts, pd); + return new BatchVisitBuilder(this, partitionState, lts, pureRng, descriptorSelector, schema, (visit) -> { + log.put(visit.lts, visit); + }); + } + + public SingleOperationBuilder visitPartition(long pdIdx) + { + long visitLts = clock.nextLts(); + long pd = presetSelector.pdAtPosition(pdIdx); + return singleOpVisitBuilder(pd, visitLts); + } + + /** + * This is an adapter HistoryBuilder is using to reproduce state for the reconciler. + * + * This class is inherently not thread-safe. The thinking behind this is that you should generate + * operations in advance, and only after you have generated them, should you start execution. + * If you would like to generate on the fly, you should use default Harry machinery and pure generators, + * that walk LTS space without intermediate state. This set of primitives is intended to be used for much + * smaller scale testing. + */ + public class PresetPdSelector extends OpSelectors.PdSelector + { + // TODO: implement a primitive long map? + private final Map ltsToPd = new HashMap<>(); + + public PartitionVisitState register(long lts, long pd) + { + Long prev = ltsToPd.put(lts, pd); + if (prev != null) + throw new IllegalStateException(String.format("LTS %d. Was registered twice, first with %d, and then with %d", lts, prev, pd)); + + long[] possibleCds = new long[maxPartitionSize]; + for (int i = 0; i < possibleCds.length; i++) + possibleCds[i] = descriptorSelector.cd(pd, 0, i, schema); + Arrays.sort(possibleCds); + + // TODO: can we have something more efficient than a tree set here? + PartitionVisitState partitionState = partitionStates.computeIfAbsent(pd, (pd_) -> new PartitionVisitState(pd, possibleCds, new TreeSet<>())); + partitionState.visitedLts.add(lts); + return partitionState; + } + + protected long pd(long lts) + { + return ltsToPd.get(lts); + } + + public long nextLts(long lts) + { + long pd = pd(lts); + PartitionVisitState partitionState = partitionStates.get(pd); + NavigableSet visitedLts = partitionState.visitedLts.subSet(lts, false, Long.MAX_VALUE, false); + if (visitedLts.isEmpty()) + return -1; + else + return visitedLts.first(); + } + + public long prevLts(long lts) + { + long pd = pd(lts); + PartitionVisitState partitionState = partitionStates.get(pd); + NavigableSet visitedLts = partitionState.visitedLts.descendingSet().subSet(lts, false, 0L, false); + if (visitedLts.isEmpty()) + return -1; + else + return visitedLts.first(); + } + + public long maxLtsFor(long pd) + { + PartitionVisitState partitionState = partitionStates.get(pd); + if (partitionState == null) + return -1; + return partitionState.visitedLts.last(); + } + + public long minLtsFor(long pd) + { + PartitionVisitState partitionState = partitionStates.get(pd); + if (partitionState == null) + return -1; + return partitionState.visitedLts.first(); + } + + public long pdAtPosition(long pdIdx) + { + return defaultSelector.pdAtPosition(pdIdx, schema); + } + + public Collection pds() + { + return partitionStates.keySet(); + } + + public long minLtsAt(long position) + { + throw new IllegalArgumentException("not implemented"); + } + + public long maxPosition(long maxLts) + { + // since, unlike other PdSelectors, this one is not computational, we can answer which position is the largest just + // by tracking the largest position + return 0; + } + } + + public ReplayingVisitor visitor(DataTracker tracker, SystemUnderTest sut, SystemUnderTest.ConsistencyLevel cl) + { + if (schema.trackLts) + { + return visitor(new MutatingVisitor.LtsTrackingVisitExecutor(descriptorSelector, + tracker, + sut, + schema, + new MutatingRowVisitor(schema, clock, MetricReporter.NO_OP), + cl)); + } + else + { + return visitor(new MutatingVisitor.MutatingVisitExecutor(descriptorSelector, + tracker, + sut, + schema, + new MutatingRowVisitor(schema, clock, MetricReporter.NO_OP), + cl)); + } + } + + public Model quiescentChecker(DataTracker tracker, SystemUnderTest sut) + { + // TODO: CL for quiescent checker + return new QuiescentChecker(clock, sut, tracker, schema, + new Reconciler(presetSelector, + schema, + this::visitor)); + } + + public Model quiescentLocalChecker(DataTracker tracker, SystemUnderTest sut) + { + return new QuiescentLocalStateChecker(clock, presetSelector, sut, tracker, schema, + new Reconciler(presetSelector, + schema, + this::visitor), + rf); + } + + public void validate(DataTracker tracker, SystemUnderTest sut, int... partitionIdxs) + { + validate(quiescentChecker(tracker, sut), partitionIdxs); + } + + public void validate(Model model, int... partitionIdxs) + { + for (int partitionIdx : partitionIdxs) + { + long pd = presetSelector.pdAtPosition(partitionIdx); + if (presetSelector.minLtsFor(pd) < 0) + continue; + model.validate(Query.selectPartition(schema, pd, false)); + model.validate(Query.selectPartition(schema, pd, true)); + } + } + + public void validateAll(DataTracker tracker, SystemUnderTest sut) + { + validateAll(quiescentChecker(tracker, sut)); + } + + public void validateAll(Model model) + { + for (Long pd : partitionStates.keySet()) + { + model.validate(Query.selectPartition(schema, pd, false)); + model.validate(Query.selectPartition(schema, pd, true)); + } + } + + public ReplayingVisitor visitor(VisitExecutor executor) + { + LongIterator replay = clock.replayAll(); + return new ReplayingVisitor(executor, replay) + { + public Visit getVisit(long lts) + { + long idx = lts - clock.base; + Visit visit = log.get(idx); + assert visit != null : String.format("Could not find a visit for LTS %d", lts); + return visit; + } + + public void replayAll() + { + while (replay.hasNext()) + visit(); + } + }; + } + + public interface LongIterator extends LongSupplier + { + boolean hasNext(); + long getAsLong(); + } + + /** + * Non-monotonic version of OffsetClock. + */ + public class OffsetClock implements OpSelectors.Clock + { + private long lowerBound; + private long current; + + private final long base; + private final long batchSize; + private final Set returned; + private final EntropySource entropySource; + + private final List visitOrder; + + public OffsetClock(long base, long batchSize, EntropySource entropySource) + { + this.lowerBound = base; + this.base = base; + this.batchSize = batchSize; + this.returned = new HashSet<>(); + this.entropySource = entropySource; + this.visitOrder = new ArrayList<>(); + this.current = computeNext(); + } + + /** + * Visit Order - related methods + */ + public LongIterator replayAll() + { + return new LongIterator() + { + private int visitedUpTo; + + public boolean hasNext() + { + return visitedUpTo < visitOrder.size(); + } + + public long getAsLong() + { + return visitOrder.get(visitedUpTo++); + } + }; + } + + private long computeNext() + { + if (returned.size() == batchSize) + { + returned.clear(); + lowerBound += batchSize; + } + + long generated = entropySource.nextLong(lowerBound, lowerBound + batchSize); + while (returned.contains(generated)) + generated = entropySource.nextLong(lowerBound, lowerBound + batchSize); + + returned.add(generated); + return generated; + } + + @Override + public long rts(long lts) + { + return base + lts; + } + + @Override + public long lts(long rts) + { + return rts - base; + } + + + @Override + public long nextLts() + { + long ret = current; + current = computeNext(); + visitOrder.add(ret); + return ret; + } + + public long peek() + { + return current; + } + + public Configuration.ClockConfiguration toConfig() + { + throw new RuntimeException("Not implemented"); + } + } +} diff --git a/test/harry/main/org/apache/cassandra/harry/dsl/PartitionVisitState.java b/test/harry/main/org/apache/cassandra/harry/dsl/PartitionVisitState.java new file mode 100644 index 0000000000..6944c42cc4 --- /dev/null +++ b/test/harry/main/org/apache/cassandra/harry/dsl/PartitionVisitState.java @@ -0,0 +1,35 @@ +/* + * 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.dsl; + +import java.util.NavigableSet; + +public class PartitionVisitState +{ + final long pd; + final long[] possibleCds; + final NavigableSet visitedLts; + + PartitionVisitState(long pd, long[] possibleCds, NavigableSet visitedLts) + { + this.pd = pd; + this.possibleCds = possibleCds; + this.visitedLts = visitedLts; + } +} diff --git a/test/harry/main/org/apache/cassandra/harry/dsl/ReplayingHistoryBuilder.java b/test/harry/main/org/apache/cassandra/harry/dsl/ReplayingHistoryBuilder.java new file mode 100644 index 0000000000..312da15949 --- /dev/null +++ b/test/harry/main/org/apache/cassandra/harry/dsl/ReplayingHistoryBuilder.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.harry.dsl; + +import org.apache.cassandra.harry.ddl.SchemaSpec; +import org.apache.cassandra.harry.model.Model; +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.visitors.ReplayingVisitor; + +public class ReplayingHistoryBuilder extends HistoryBuilder +{ + private final ReplayingVisitor visitor; + private final SystemUnderTest sut; + public final DataTracker tracker; + + public ReplayingHistoryBuilder(long seed, + int maxPartitionSize, + int interleaveWindowSize, + DataTracker tracker, + SystemUnderTest sut, + SchemaSpec schema, + TokenPlacementModel.ReplicationFactor rf, + SystemUnderTest.ConsistencyLevel writeCl) + { + super(seed, maxPartitionSize, interleaveWindowSize, schema, rf); + this.visitor = visitor(tracker, sut, writeCl); + this.tracker = tracker; + this.sut = sut; + } + + protected SingleOperationVisitBuilder singleOpVisitBuilder(long pd, long lts) + { + PartitionVisitState partitionState = presetSelector.register(lts, pd); + return new SingleOperationVisitBuilder(partitionState, lts, pureRng, descriptorSelector, schema, (visit) -> { + log.put(lts, visit); + }) { + @Override + void end() + { + super.end(); + visitor.replayAll(); + } + }; + } + + @Override + public BatchVisitBuilder beginBatch() + { + long visitLts = clock.nextLts(); + return batchVisitBuilder(defaultSelector.pd(visitLts, schema), visitLts); + } + + /** + * Begin batch for a partition descriptor at a specific index. + * + * Imagine all partition descriptors were longs in an array. Index of a descriptor + * is a sequential number of the descriptor in this imaginary array. + */ + @Override + public BatchVisitBuilder beginBatch(long pdIdx) + { + long visitLts = clock.nextLts(); + return batchVisitBuilder(presetSelector.pdAtPosition(pdIdx), visitLts); + } + + @Override + protected BatchVisitBuilder batchVisitBuilder(long pd, long lts) + { + PartitionVisitState partitionState = presetSelector.register(lts, pd); + return new BatchVisitBuilder(this, partitionState, lts, pureRng, descriptorSelector, schema, (visit) -> { + log.put(lts, visit); + }) { + @Override + public HistoryBuilder endBatch() + { + super.endBatch(); + visitor.replayAll(); + return ReplayingHistoryBuilder.this; + } + }; + } + + public ReplayingHistoryBuilder validate(int... partitions) + { + validate(tracker, sut, partitions); + return this; + } + + public Model quiescentChecker() + { + return quiescentChecker(tracker, sut); + } + + public Model quiescentLocalChecker() + { + return quiescentLocalChecker(tracker, sut); + } + +} diff --git a/test/harry/main/org/apache/cassandra/harry/dsl/SingleOperationBuilder.java b/test/harry/main/org/apache/cassandra/harry/dsl/SingleOperationBuilder.java new file mode 100644 index 0000000000..10f2026a7d --- /dev/null +++ b/test/harry/main/org/apache/cassandra/harry/dsl/SingleOperationBuilder.java @@ -0,0 +1,53 @@ +/* + * 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.dsl; + +public interface SingleOperationBuilder +{ + + /** + * Perform an insert operation to _some_ row + */ + SingleOperationBuilder insert(); + + /** + * Perform an insert operation to a _specific_ row. Rows are ordered by clustering key and + * numbered from 0 to maxRows + */ + SingleOperationBuilder insert(int rowIdx); + + /** + * Insert _specific values_ into _specific_ row. Rows are ordered by clustering key and + * numbered from 0 to maxRows + */ + SingleOperationBuilder insert(int rowIdx, long[] vds); + SingleOperationBuilder insert(int rowIdx, long[] vds, long[] sds); + + SingleOperationBuilder deletePartition(); + + SingleOperationBuilder deleteRow(); + SingleOperationBuilder deleteRow(int rowIdx); + + SingleOperationBuilder deleteColumns(); + + SingleOperationBuilder deleteRowRange(); + SingleOperationBuilder deleteRowRange(int lowBoundRowIdx, int highBoundRowIdx, boolean isMinEq, boolean isMaxEq); + + SingleOperationBuilder deleteRowSlice(); +} diff --git a/test/harry/main/org/apache/cassandra/harry/dsl/SingleOperationVisitBuilder.java b/test/harry/main/org/apache/cassandra/harry/dsl/SingleOperationVisitBuilder.java new file mode 100644 index 0000000000..05652f13d0 --- /dev/null +++ b/test/harry/main/org/apache/cassandra/harry/dsl/SingleOperationVisitBuilder.java @@ -0,0 +1,295 @@ +/* + * 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.dsl; + +import java.util.ArrayList; +import java.util.List; +import java.util.function.Consumer; + +import org.apache.cassandra.harry.ddl.SchemaSpec; +import org.apache.cassandra.harry.gen.EntropySource; +import org.apache.cassandra.harry.gen.rng.JdkRandomEntropySource; +import org.apache.cassandra.harry.model.OpSelectors; +import org.apache.cassandra.harry.operations.Query; +import org.apache.cassandra.harry.util.BitSet; +import org.apache.cassandra.harry.visitors.GeneratingVisitor; +import org.apache.cassandra.harry.visitors.ReplayingVisitor; +import org.apache.cassandra.harry.visitors.VisitExecutor; + +class SingleOperationVisitBuilder implements SingleOperationBuilder +{ + // TODO: singleton collection for this op class + private final List operations; + private final PartitionVisitState partitionState; + + private final long lts; + private final long pd; + + private final OpSelectors.PureRng rng; + private final OpSelectors.DescriptorSelector descriptorSelector; + private final SchemaSpec schemaSpec; + + private final Consumer appendToLog; + private final WithEntropySource rngSupplier = new WithEntropySource(); + + private int opIdCounter; + + public SingleOperationVisitBuilder(PartitionVisitState partitionState, + long lts, + OpSelectors.PureRng rng, + OpSelectors.DescriptorSelector descriptorSelector, + SchemaSpec schemaSpec, + Consumer appendToLog) + { + this.lts = lts; + this.partitionState = partitionState; + this.pd = partitionState.pd; + + this.appendToLog = appendToLog; + this.operations = new ArrayList<>(); + this.opIdCounter = 0; + + this.rng = rng; + this.descriptorSelector = descriptorSelector; + this.schemaSpec = schemaSpec; + } + + @Override + public SingleOperationVisitBuilder insert() + { + int clusteringOffset = rngSupplier.withSeed(lts).nextInt(0, partitionState.possibleCds.length - 1); + return insert(clusteringOffset); + } + + @Override + public SingleOperationVisitBuilder insert(int rowIdx) + { + int opId = opIdCounter++; + long cd = partitionState.possibleCds[rowIdx]; + operations.add(new GeneratingVisitor.GeneratedWriteOp(lts, pd, cd, opId, + OpSelectors.OperationKind.INSERT) + { + public long[] vds() + { + return descriptorSelector.vds(pd, cd, lts, opId, kind(), schemaSpec); + } + }); + end(); + return this; + } + + @Override + public SingleOperationVisitBuilder insert(int rowIdx, long[] vds) + { + int opId = opIdCounter++; + long cd = partitionState.possibleCds[rowIdx]; + operations.add(new GeneratingVisitor.GeneratedWriteOp(lts, pd, cd, opId, + OpSelectors.OperationKind.INSERT) + { + public long[] vds() + { + return vds; + } + }); + end(); + return this; + } + + @Override + public SingleOperationBuilder insert(int rowIdx, long[] vds, long[] sds) + { + int opId = opIdCounter++; + long cd = partitionState.possibleCds[rowIdx]; + operations.add(new GeneratingVisitor.GeneratedWriteWithStaticOp(lts, pd, cd, opId, + OpSelectors.OperationKind.INSERT_WITH_STATICS) + { + @Override + public long[] sds() + { + return sds; + } + + @Override + public long[] vds() + { + return vds; + } + }); + end(); + return this; + } + + @Override + public SingleOperationVisitBuilder deletePartition() + { + int opId = opIdCounter++; + operations.add(new GeneratingVisitor.GeneratedDeleteOp(lts, pd, opId, OpSelectors.OperationKind.DELETE_PARTITION, + Query.selectPartition(schemaSpec, pd, false))); + end(); + return this; + } + + @Override + public SingleOperationVisitBuilder deleteRow() + { + int opId = opIdCounter++; + long queryDescriptor = rng.next(opId, lts); + rngSupplier.withSeed(queryDescriptor, (rng) -> { + int cdIdx = rngSupplier.withSeed(queryDescriptor).nextInt(partitionState.possibleCds.length); + long cd = partitionState.possibleCds[cdIdx]; + operations.add(new GeneratingVisitor.GeneratedDeleteRowOp(lts, pd, cd, opId, + OpSelectors.OperationKind.DELETE_ROW)); + }); + end(); + return this; + } + + @Override + public SingleOperationVisitBuilder deleteRow(int cdIdx) + { + int opId = opIdCounter++; + long cd = partitionState.possibleCds[cdIdx]; + operations.add(new GeneratingVisitor.GeneratedDeleteRowOp(lts, pd, cd, opId, + OpSelectors.OperationKind.DELETE_ROW)); + end(); + return this; + } + + @Override + public SingleOperationVisitBuilder deleteColumns() + { + int opId = opIdCounter++; + long queryDescriptor = rng.next(opId, lts); + rngSupplier.withSeed(queryDescriptor, (rng) -> { + int cdIdx = rng.nextInt(partitionState.possibleCds.length); + long cd = partitionState.possibleCds[cdIdx]; + BitSet columns = descriptorSelector.columnMask(pd, lts, opId, OpSelectors.OperationKind.DELETE_COLUMN); + operations.add(new GeneratingVisitor.GeneratedDeleteColumnsOp(lts, pd, cd, opId, + OpSelectors.OperationKind.DELETE_COLUMN, columns)); + }); + end(); + return this; + } + + @Override + public SingleOperationVisitBuilder deleteRowRange() + { + int opId = opIdCounter++; + long queryDescriptor = rng.next(opId, lts); + rngSupplier.withSeed(queryDescriptor, (rng) -> { + Query query = null; + while (query == null) + { + try + { + long cd1 = partitionState.possibleCds[rng.nextInt(partitionState.possibleCds.length)]; + long cd2 = partitionState.possibleCds[rng.nextInt(partitionState.possibleCds.length)]; + while (cd2 == cd1) + cd2 = partitionState.possibleCds[rng.nextInt(partitionState.possibleCds.length)]; + + boolean isMinEq = rng.nextBoolean(); + boolean isMaxEq = rng.nextBoolean(); + query = Query.clusteringRangeQuery(schemaSpec, pd, cd1, cd2, queryDescriptor, isMinEq, isMaxEq, false); + break; + } + catch (IllegalArgumentException retry) + { + continue; + } + } + operations.add(new GeneratingVisitor.GeneratedDeleteOp(lts, pd, opId, OpSelectors.OperationKind.DELETE_SLICE, query)); + }); + end(); + return this; + } + + @Override + public SingleOperationVisitBuilder deleteRowRange(int lowBoundRowIdx, int highBoundRowIdx, boolean isMinEq, boolean isMaxEq) + { + int opId = opIdCounter++; + long queryDescriptor = rng.next(opId, lts); + + long cd1 = partitionState.possibleCds[lowBoundRowIdx]; + long cd2 = partitionState.possibleCds[highBoundRowIdx]; + Query query = Query.clusteringRangeQuery(schemaSpec, pd, cd1, cd2, queryDescriptor, isMinEq, isMaxEq, false); + operations.add(new GeneratingVisitor.GeneratedDeleteOp(lts, pd, opId, OpSelectors.OperationKind.DELETE_SLICE, query)); + end(); + return this; + } + + @Override + public SingleOperationVisitBuilder deleteRowSlice() + { + int opId = opIdCounter++; + long queryDescriptor = rng.next(opId, lts); + rngSupplier.withSeed(queryDescriptor, (rng) -> { + Query query = null; + while (query == null) + { + try + { + int cdIdx = rng.nextInt(partitionState.possibleCds.length); + long cd = partitionState.possibleCds[cdIdx]; + + boolean isGt = rng.nextBoolean(); + boolean isEquals = rng.nextBoolean(); + query = Query.clusteringSliceQuery(schemaSpec, pd, cd, queryDescriptor, isGt, isEquals, false); + break; + } + catch (IllegalArgumentException retry) + { + continue; + } + } + operations.add(new GeneratingVisitor.GeneratedDeleteOp(lts, pd, opId, OpSelectors.OperationKind.DELETE_SLICE, query)); + }); + end(); + return this; + } + + int size() + { + return this.operations.size(); + } + + void end() + { + VisitExecutor.Operation[] ops = new VisitExecutor.Operation[operations.size()]; + operations.toArray(ops); + ReplayingVisitor.Visit visit = new ReplayingVisitor.Visit(lts, pd, ops); + appendToLog.accept(visit); + } + + private static class WithEntropySource + { + private final EntropySource entropySource = new JdkRandomEntropySource(0); + + public void withSeed(long seed, Consumer rng) + { + entropySource.seed(seed); + rng.accept(entropySource); + } + + public EntropySource withSeed(long seed) + { + entropySource.seed(seed); + return entropySource; + } + } +} diff --git a/test/harry/main/org/apache/cassandra/harry/dsl/ValueDescriptorIndexGenerator.java b/test/harry/main/org/apache/cassandra/harry/dsl/ValueDescriptorIndexGenerator.java new file mode 100644 index 0000000000..dc74412c3a --- /dev/null +++ b/test/harry/main/org/apache/cassandra/harry/dsl/ValueDescriptorIndexGenerator.java @@ -0,0 +1,78 @@ +/* + * 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.dsl; + +import java.util.function.LongSupplier; + +import org.apache.cassandra.harry.ddl.ColumnSpec; +import org.apache.cassandra.harry.gen.Bytes; +import org.apache.cassandra.harry.gen.EntropySource; +import org.apache.cassandra.harry.gen.DataGenerators; +import org.apache.cassandra.harry.gen.Surjections; +import org.apache.cassandra.harry.model.OpSelectors; + +/** + * By default, descriptors in Harry are chosen as items in the stream of `pd ^ cd ^ lts ^ col`, + * but for purpose of some tests one may want to have more fine-grained control over values, + * which is particularly useful for 2i testing. + * + * Imagine all possible value descriptors as items in some array (each column would have its own + * array with different descriptors). This generator will pick an item from this array by given index. + */ +public class ValueDescriptorIndexGenerator implements Surjections.Surjection +{ + private final OpSelectors.PureRng rng; + private final long columnHash; + private final long mask; + + public ValueDescriptorIndexGenerator(ColumnSpec columnSpec, + OpSelectors.PureRng rng) + { + this.rng = rng; + this.columnHash = columnSpec.hashCode(); + this.mask = Bytes.bytePatternFor(columnSpec.type.maxSize()); + } + + @Override + public Long inflate(long idx) + { + return rng.randomNumber(idx, columnHash) & mask; + } + + /** + * Returns a supplier that would uniformly pick from at most {@param values} values. + * + * @param values number of possible values + */ + public LongSupplier toSupplier(EntropySource orig, int values, float chanceOfUnset) + { + EntropySource derived = orig.derive(); + if (chanceOfUnset > 0) + { + assert chanceOfUnset < 1.0; + return () -> { + if (orig.nextFloat() < chanceOfUnset) + return DataGenerators.UNSET_DESCR; + return inflate(derived.nextInt(values)); + }; + } + + return () -> inflate(derived.nextInt(values)); + } +} diff --git a/test/harry/main/org/apache/cassandra/harry/gen/Bijections.java b/test/harry/main/org/apache/cassandra/harry/gen/Bijections.java new file mode 100644 index 0000000000..9b8d8ada26 --- /dev/null +++ b/test/harry/main/org/apache/cassandra/harry/gen/Bijections.java @@ -0,0 +1,457 @@ +/* + * 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.gen; + +import java.util.Date; +import java.util.UUID; + +public class Bijections +{ + public static final Bijection INT8_GENERATOR = new ByteGenerator(); + public static final Bijection INT16_GENERATOR = new Int16Generator(); + public static final Bijection INT32_GENERATOR = new Int32Generator(); + public static final Bijection INT64_GENERATOR = new LongGenerator(); + public static final Bijection FLOAT_GENERATOR = new FloatGenerator(); + public static final Bijection DOUBLE_GENERATOR = new DoubleGenerator(); + public static final Bijection BOOLEAN_GENERATOR = new BooleanGenerator(); + + public static final Bijection UUID_GENERATOR = new UUIDGenerator(); + public static final TimeUUIDGenerator TIME_UUID_GENERATOR = new TimeUUIDGenerator(); + public static final Bijection TIMESTAMP_GENERATOR = new TimestampGenerator(); + + /** + * When generating a value, invertible generator first draws a long from the random number generator, and + * passes it to the normalization function. Normalization scales the long value down to the range that corresponds + * to the generated value range. For example, for Boolean, the range is of a size 2. For Integer - 2^32, etc. + *

+ * deflated has to be equal to adjustEntropyDomain value. + *

+ * When inflating, we should inflate up to adjustEntropyDomain values. This way, deflated values will correspond to infoated ones. + */ + public interface Bijection + { + T inflate(long descriptor); + + long deflate(T value); + + // TODO: byteSize is great, but you know what's better? Bit size! For example, for `boolean`, we only need a single bit. + int byteSize(); + + /** + * Compare as if we were comparing the values in question + */ + int compare(long l, long r); + + default long adjustEntropyDomain(long descriptor) + { + return descriptor & Bytes.bytePatternFor(byteSize()); + } + + default long minValue() + { + return minForSize(byteSize()); + } + + default long maxValue() + { + return maxForSize(byteSize()); + } + + default boolean unsigned() + { + return false; + } + } + + protected static long minForSize(int size) + { + long min = 1L << (size * Byte.SIZE - 1); + + if (size < Long.BYTES) + min ^= Bytes.signMaskFor(size); + + return min; + } + + protected static long maxForSize(int size) + { + long max = Bytes.bytePatternFor(size) >>> 1; + + if (size < Long.BYTES) + max ^= Bytes.signMaskFor(size); + + return max; + } + + // TODO: two points: + // * We might be able to avoid boxing if we can generate straight to byte buffer (?) + // * since these data types are quite specialized, we do not strictly need complex interface for them, it might + // be easier to even create a special type for these. We need randomness source in cases of more complex generation, + // but not really here. + + /** + * Reverse type is different from the regular one in that will generate values + * that will sort the order that is opposite to the order of descriptor. + */ + public static class ReverseBijection implements Bijection + { + private final Bijection delegate; + + public ReverseBijection(Bijection delegate) + { + this.delegate = delegate; + } + + public T inflate(long descriptor) + { + return delegate.inflate(descriptor * -1 - 1); + } + + public long deflate(T value) + { + return -1 * (delegate.deflate(value) + 1); + } + + public int byteSize() + { + return delegate.byteSize(); + } + + public int compare(long l, long r) + { + return delegate.compare(r, l); + } + } + + public static class LongGenerator implements Bijection + { + public Long inflate(long current) + { + return current; + } + + public long deflate(Long value) + { + return value; + } + + public int compare(long l, long r) + { + return Long.compare(l, r); + } + + public int byteSize() + { + return Long.BYTES; + } + } + + public static class Int32Generator implements Bijection + { + public Integer inflate(long current) + { + return (int) current; + } + + public long deflate(Integer value) + { + return value & 0xffffffffL; + } + + public int compare(long l, long r) + { + return Integer.compare((int) l, (int) r); + } + + public int byteSize() + { + return Integer.BYTES; + } + } + + public static class Int16Generator implements Bijection + { + public Short inflate(long current) + { + return (short) current; + } + + public long deflate(Short value) + { + return value & 0xffffL; + } + + public int compare(long l, long r) + { + return Short.compare((short) l, (short) r); + } + + public int byteSize() + { + return Short.BYTES; + } + } + + public static class ByteGenerator implements Bijection + { + public Byte inflate(long current) + { + return (byte) current; + } + + public long deflate(Byte value) + { + return value & 0xffL; + } + + public int compare(long l, long r) + { + return Byte.compare((byte) l, (byte) r); + } + + public int byteSize() + { + return Byte.BYTES; + } + } + + public static class BooleanGenerator implements Bijection + { + public Boolean inflate(long current) + { + return inflatePrimitive(current); + } + + private boolean inflatePrimitive(long current) + { + return current == 2; + } + + public long deflate(Boolean value) + { + return value ? 2 : 1; + } + + public int byteSize() + { + return Byte.BYTES; + } + + public int compare(long l, long r) + { + return Byte.compare((byte) l, (byte) r); + } + + public long adjustEntropyDomain(long descriptor) + { + return (descriptor & 1) + 1; + } + } + + public static class FloatGenerator implements Bijection + { + private static final int SIZE = Float.BYTES - 1; + + public Float inflate(long current) + { + return inflatePrimitive(current); + } + + protected float inflatePrimitive(long current) + { + return Float.intBitsToFloat((int) current); + } + + public long deflate(Float value) + { + return Float.floatToRawIntBits(value); + } + + // In other words, there's no way we can extend entropy to a sign + public boolean unsigned() + { + return true; + } + + public int compare(long l, long r) + { + return Float.compare(inflatePrimitive(l), inflatePrimitive(r)); + } + + public int byteSize() + { + return SIZE; + } + } + + public static class ReverseFloatGenerator extends FloatGenerator + { + public float inflatePrimitive(long current) + { + return super.inflatePrimitive(current - 1) * -1; + } + + public long deflate(Float value) + { + return super.deflate(value * -1 ) + 1; + } + + public int compare(long l, long r) + { + return super.compare(r, l); + } + } + + public static class DoubleGenerator implements Bijection + { + private static int SIZE = Double.BYTES - 1; + + public Double inflate(long current) + { + return inflatePrimitive(current); + } + + protected double inflatePrimitive(long current) + { + return Double.longBitsToDouble(current); + } + + public long deflate(Double value) + { + return Double.doubleToRawLongBits(value); + } + + public int compare(long l, long r) + { + return Double.compare(inflatePrimitive(l), inflatePrimitive(r)); + } + + public int byteSize() + { + return SIZE; + } + + /** + * To avoid generating NaNs, we're using a smaller size for Double. But because of that, double became + * sign-less. In other words, even if we generate a double, it will always be positive, since its most + * significant bit isn't set. This means that + */ + public boolean unsigned() + { + return true; + } + } + + public static class ReverseDoubleGenerator extends DoubleGenerator + { + public double inflatePrimitive(long current) + { + return super.inflatePrimitive(current - 1) * -1; + } + + public long deflate(Double value) + { + return super.deflate(value * -1) + 1; + } + + public int compare(long l, long r) + { + return super.compare(r, l); + } + } + + public static class UUIDGenerator implements Bijection + { + public UUID inflate(long current) + { + // order is determined by the top bits + return new UUID(current, current); + } + + public long deflate(UUID value) + { + return value.getMostSignificantBits(); + } + + public int compare(long l, long r) + { + return Long.compare(l, r); + } + + public int byteSize() + { + return Long.BYTES; + } + } + + public static class TimeUUIDGenerator implements Bijection + { + public UUID inflate(long current) + { + return new UUID(createTime(current), current); + } + + public long deflate(UUID value) + { + return value.getLeastSignificantBits(); + } + + public int compare(long left, long right) + { + return Long.compare(left, right); + } + + public int byteSize() + { + return Long.BYTES; + } + + public static long createTime(long nanosSince) + { + long msb = nanosSince; + msb &= 0xffffffffffff10ffL; // sets the version to 1. + msb |= 0x0000000000001000L; + return msb; + } + + } + + public static class TimestampGenerator implements Bijection + { + public Date inflate(long descriptor) + { + return new Date(descriptor); + } + + public long deflate(Date value) + { + return value.getTime(); + } + + public int compare(long l, long r) + { + return Byte.compare((byte) l, (byte) r); + } + + public int byteSize() + { + return Long.BYTES; + } + } +} \ No newline at end of file diff --git a/test/harry/main/org/apache/cassandra/harry/gen/BooleanGenerator.java b/test/harry/main/org/apache/cassandra/harry/gen/BooleanGenerator.java new file mode 100644 index 0000000000..53a1529ac5 --- /dev/null +++ b/test/harry/main/org/apache/cassandra/harry/gen/BooleanGenerator.java @@ -0,0 +1,29 @@ +/* + * 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.gen; + +public class BooleanGenerator implements Generator +{ + public static BooleanGenerator INSTANCE = new BooleanGenerator(); + + public Boolean generate(EntropySource rng) + { + return rng.nextBoolean(); + } +} diff --git a/test/harry/main/org/apache/cassandra/harry/gen/Bytes.java b/test/harry/main/org/apache/cassandra/harry/gen/Bytes.java new file mode 100644 index 0000000000..963de0cda3 --- /dev/null +++ b/test/harry/main/org/apache/cassandra/harry/gen/Bytes.java @@ -0,0 +1,42 @@ +/* + * 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.gen; + +public class Bytes +{ + + public final static long[] BYTES = new long[]{ 0xFFL, + 0xFFFFL, + 0xFFFFFFL, + 0xFFFFFFFFL, + 0xFFFFFFFFFFL, + 0xFFFFFFFFFFFFL, + 0xFFFFFFFFFFFFFFL, + 0xFFFFFFFFFFFFFFFFL }; + + public static long bytePatternFor(int size) + { + return BYTES[size - 1]; + } + + public static long signMaskFor(int size) + { + return 1L << ((Byte.SIZE * size) - 1); + } +} diff --git a/test/harry/main/org/apache/cassandra/harry/gen/Collections.java b/test/harry/main/org/apache/cassandra/harry/gen/Collections.java new file mode 100644 index 0000000000..22ff5b6565 --- /dev/null +++ b/test/harry/main/org/apache/cassandra/harry/gen/Collections.java @@ -0,0 +1,249 @@ +/* + * 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.gen; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; + +import org.apache.cassandra.harry.ddl.ColumnSpec; +import org.apache.cassandra.harry.gen.rng.RngUtils; + +// TODO: collections are currently not deflatable and/or checkable with a model +public class Collections +{ + public static ColumnSpec.DataType> mapColumn(ColumnSpec.DataType k, + ColumnSpec.DataType v, + int maxSize) + { + return new ColumnSpec.DataType>(String.format("map<%s,%s>", k.toString(), v.toString())) + { + private final Bijections.Bijection> gen = mapGen(k.generator(), v.generator(), maxSize); + + public Bijections.Bijection> generator() + { + return gen; + } + + public int maxSize() + { + return Long.BYTES; + } + }; + } + + public static ColumnSpec.DataType> listColumn(ColumnSpec.DataType v, + int maxSize) + { + return new ColumnSpec.DataType>(String.format("set<%s>", v.toString())) + { + private final Bijections.Bijection> gen = listGen(v.generator(), maxSize); + + public Bijections.Bijection> generator() + { + return gen; + } + + public int maxSize() + { + return Long.BYTES; + } + }; + } + + + public static ColumnSpec.DataType> setColumn(ColumnSpec.DataType v, + int maxSize) + { + return new ColumnSpec.DataType>(String.format("set<%s>", v.toString())) + { + private final Bijections.Bijection> gen = setGen(v.generator(), maxSize); + + public Bijections.Bijection> generator() + { + return gen; + } + + public int maxSize() + { + return Long.BYTES; + } + }; + } + + + public static Bijections.Bijection> mapGen(Bijections.Bijection keyGen, + Bijections.Bijection valueGen, + int maxSize) + { + return new MapGenerator<>(keyGen, valueGen, maxSize); + } + + public static Bijections.Bijection> listGen(Bijections.Bijection valueGen, + int maxSize) + { + return new ListGenerator<>(valueGen, maxSize); + } + + public static Bijections.Bijection> setGen(Bijections.Bijection valueGen, + int maxSize) + { + return new SetGenerator<>(valueGen, maxSize); + } + + public static class MapGenerator implements Bijections.Bijection> + { + public final Bijections.Bijection keyGen; + public final Bijections.Bijection valueGen; + public int maxSize; + + public MapGenerator(Bijections.Bijection keyGen, + Bijections.Bijection valueGen, + int maxSize) + { + this.keyGen = keyGen; + this.valueGen = valueGen; + this.maxSize = maxSize; + } + + public Map inflate(long descriptor) + { + long rnd = RngUtils.next(descriptor); + int count = RngUtils.asInt(rnd, 0, maxSize); + Map m = new HashMap<>(); + for (int i = 0; i < count; i++) + { + rnd = RngUtils.next(rnd); + K key = keyGen.inflate(rnd); + rnd = RngUtils.next(rnd); + V value = valueGen.inflate(rnd); + m.put(key, value); + } + + return m; + } + + // At least for non-frozen ones + public long deflate(Map value) + { + throw new UnsupportedOperationException(); + } + + public int byteSize() + { + return Long.BYTES; + } + + public int compare(long l, long r) + { + throw new UnsupportedOperationException(); + } + } + + public static class ListGenerator implements Bijections.Bijection> + { + public final Bijections.Bijection valueGen; + public int maxSize; + + public ListGenerator(Bijections.Bijection valueGen, + int maxSize) + { + this.valueGen = valueGen; + this.maxSize = maxSize; + } + + public List inflate(long descriptor) + { + long rnd = RngUtils.next(descriptor); + int count = RngUtils.asInt(rnd, 0, maxSize); + List m = new ArrayList<>(); + for (int i = 0; i < count; i++) + { + rnd = RngUtils.next(rnd); + V value = valueGen.inflate(rnd); + m.add(value); + } + + return m; + } + + // At least for non-frozen ones + public long deflate(List value) + { + throw new UnsupportedOperationException(); + } + + public int byteSize() + { + return Long.BYTES; + } + + public int compare(long l, long r) + { + throw new UnsupportedOperationException(); + } + } + + public static class SetGenerator implements Bijections.Bijection> + { + public final Bijections.Bijection valueGen; + public int maxSize; + + public SetGenerator(Bijections.Bijection valueGen, + int maxSize) + { + this.valueGen = valueGen; + this.maxSize = maxSize; + } + + public Set inflate(long descriptor) + { + long rnd = RngUtils.next(descriptor); + int count = RngUtils.asInt(rnd, 0, maxSize); + Set m = new HashSet<>(); + for (int i = 0; i < count; i++) + { + rnd = RngUtils.next(rnd); + V value = valueGen.inflate(rnd); + m.add(value); + } + + return m; + } + + // At least for non-frozen ones + public long deflate(Set value) + { + throw new UnsupportedOperationException(); + } + + public int byteSize() + { + return Long.BYTES; + } + + public int compare(long l, long r) + { + throw new UnsupportedOperationException(); + } + } +} diff --git a/test/harry/main/org/apache/cassandra/harry/gen/DataGenerators.java b/test/harry/main/org/apache/cassandra/harry/gen/DataGenerators.java new file mode 100644 index 0000000000..70ae26c63b --- /dev/null +++ b/test/harry/main/org/apache/cassandra/harry/gen/DataGenerators.java @@ -0,0 +1,499 @@ +/* + * 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.gen; + +import java.util.Arrays; +import java.util.Collections; +import java.util.List; + +import com.google.common.annotations.VisibleForTesting; +import org.apache.cassandra.harry.ddl.ColumnSpec; +import org.apache.cassandra.harry.gen.rng.RngUtils; + +public class DataGenerators +{ + public static final Object UNSET_VALUE = new Object() { + public String toString() + { + return "UNSET"; + } + }; + + // There is still a slim chance that we're going to produce either of these values by chance, but we'll catch this + // during value generation + public static long UNSET_DESCR = Long.MAX_VALUE; + public static long NIL_DESCR = Long.MIN_VALUE; + + public static Object[] inflateData(List> columns, long[] descriptors) + { + // This can be not true depending on how we implement subselections + assert columns.size() == descriptors.length; + Object[] data = new Object[descriptors.length]; + for (int i = 0; i < descriptors.length; i++) + { + ColumnSpec columnSpec = columns.get(i); + if (descriptors[i] == UNSET_DESCR) + data[i] = UNSET_VALUE; + else if (descriptors[i] == NIL_DESCR) + data[i] = null; + else + data[i] = columnSpec.inflate(descriptors[i]); + } + return data; + } + + public static long[] deflateData(List> columns, Object[] data) + { + // This can be not true depending on how we implement subselections + assert columns.size() == data.length; + long[] descriptors = new long[data.length]; + for (int i = 0; i < descriptors.length; i++) + { + ColumnSpec columnSpec = columns.get(i); + if (data[i] == null) + descriptors[i] = NIL_DESCR; + else if (data[i] == UNSET_VALUE) + descriptors[i] = UNSET_DESCR; + else + descriptors[i] = columnSpec.deflate(data[i]); + } + return descriptors; + } + + public static int[] requiredBytes(List> columns) + { + switch (columns.size()) + { + case 0: + throw new RuntimeException("Can't inflate empty data column set as it is not inversible"); + case 1: + return new int[]{ Math.min(columns.get(0).type.maxSize(), Long.SIZE) }; + default: + class Pair + { + final int idx, maxSize; + + Pair(int idx, int maxSize) + { + this.idx = idx; + this.maxSize = maxSize; + } + } + int[] bytes = new int[Math.min(4, columns.size())]; + Pair[] sorted = new Pair[bytes.length]; + for (int i = 0; i < sorted.length; i++) + sorted[i] = new Pair(i, columns.get(i).type.maxSize()); + + int remainingBytes = Long.BYTES; + int slotSize = remainingBytes / bytes.length; + // first pass: give it at most a slot number of bytes + for (int i = 0; i < sorted.length; i++) + { + int size = sorted[i].maxSize; + int allotedSize = Math.min(size, slotSize); + remainingBytes -= allotedSize; + bytes[sorted[i].idx] = allotedSize; + } + + // sliced evenly + if (remainingBytes == 0) + return bytes; + + // second pass: try to occupy remaining bytes + // it is possible to improve the second pass and separate additional bytes evenly, but it is + // questionable how much it'll bring since it does not change the total amount of entropy. + for (int i = 0; i < sorted.length; i++) + { + if (remainingBytes == 0) + break; + Pair p = sorted[i]; + if (bytes[p.idx] < p.maxSize) + { + int allotedSize = Math.min(p.maxSize - bytes[p.idx], remainingBytes); + remainingBytes -= allotedSize; + bytes[p.idx] += allotedSize; + } + } + + return bytes; + } + } + + public static Object[] inflateKey(List> columns, long descriptor, long[] slices) + { + assert columns.size() >= slices.length : String.format("Columns: %s. Slices: %s", columns, Arrays.toString(slices)); + assert columns.size() > 0 : "Can't deflate from empty columnset"; + + Object[] res = new Object[columns.size()]; + for (int i = 0; i < slices.length; i++) + { + ColumnSpec spec = columns.get(i); + res[i] = spec.inflate(slices[i]); + } + + // The rest can be random, since prefix is always fixed + long current = descriptor; + for (int i = slices.length; i < columns.size(); i++) + { + current = RngUtils.next(current); + res[i] = columns.get(i).inflate(current); + } + + return res; + } + + public static long[] deflateKey(List> columns, Object[] values) + { + assert columns.size() == values.length : String.format("%s != %s", columns.size(), values.length); + assert columns.size() > 0 : "Can't deflate from empty columnset"; + + int fixedPart = Math.min(4, columns.size()); + + long[] slices = new long[fixedPart]; + boolean allNulls = true; + for (int i = 0; i < fixedPart; i++) + { + ColumnSpec spec = columns.get(i); + Object value = values[i]; + if (value != null) + allNulls = false; + + slices[i] = value == null ? NIL_DESCR : spec.deflate(value); + } + + if (allNulls) + return null; + + return slices; + } + + public static KeyGenerator createKeyGenerator(List> columns) + { + switch (columns.size()) + { + case 0: + return EMPTY_KEY_GEN; + case 1: + return new SinglePartKeyGenerator(columns); + default: + return new MultiPartKeyGenerator(columns); + } + } + + private static final KeyGenerator EMPTY_KEY_GEN = new KeyGenerator(Collections.emptyList()) + { + private final long[] EMPTY_SLICED = new long[0]; + private final Object[] EMPTY_INFLATED = new Object[0]; + + public long[] slice(long descriptor) + { + return EMPTY_SLICED; + } + + public long stitch(long[] parts) + { + return 0; + } + + public long minValue(int idx) + { + return 0; + } + + public long maxValue(int idx) + { + return 0; + } + + @Override + public Object[] inflate(long descriptor) + { + return EMPTY_INFLATED; + } + + @Override + public long deflate(Object[] value) + { + return 0; + } + + public long adjustEntropyDomain(long descriptor) + { + return 0; + } + + public int byteSize() + { + return 0; + } + + public int compare(long l, long r) + { + return 0; + } + }; + + public static abstract class KeyGenerator implements Bijections.Bijection + { + @VisibleForTesting + public final List> columns; + + KeyGenerator(List> columns) + { + this.columns = columns; + } + + public abstract long[] slice(long descriptor); + + public abstract long stitch(long[] parts); + + public long minValue() + { + return Bijections.minForSize(byteSize()); + } + + public long maxValue() + { + return Bijections.maxForSize(byteSize()); + } + + /** + * Min value for a segment: 0, possibly with an inverted 0 sign for stitching. + */ + public abstract long minValue(int idx); + public abstract long maxValue(int idx); + } + + public static class SinglePartKeyGenerator extends KeyGenerator + { + private final Bijections.Bijection keyGen; + private final int totalSize; + + public SinglePartKeyGenerator(List> columns) + { + super(columns); + assert columns.size() == 1; + this.keyGen = columns.get(0).generator(); + this.totalSize = keyGen.byteSize(); + } + + public long[] slice(long descriptor) + { + if (shouldInvertSign()) + descriptor ^= Bytes.signMaskFor(byteSize()); + + descriptor = adjustEntropyDomain(descriptor); + return new long[]{ descriptor }; + } + + public long stitch(long[] parts) + { + long descriptor = parts[0]; + + if (shouldInvertSign()) + descriptor ^= Bytes.signMaskFor(byteSize()); + + return adjustEntropyDomain(descriptor); + } + + public long minValue(int idx) + { + assert idx == 0; + return keyGen.minValue(); + } + + public long maxValue(int idx) + { + assert idx == 0; + return keyGen.maxValue(); + } + + public Object[] inflate(long descriptor) + { + long[] sliced = slice(descriptor); + return new Object[]{ keyGen.inflate(sliced[0]) }; + } + + public boolean shouldInvertSign() + { + return totalSize != Long.BYTES && !keyGen.unsigned(); + } + + public long deflate(Object[] value) + { + Object v = value[0]; + if (v == null) + return NIL_DESCR; + long descriptor = keyGen.deflate(v); + return stitch(new long[] { descriptor }); + } + + public int byteSize() + { + return totalSize; + } + + public int compare(long l, long r) + { + return Long.compare(l, r); + } + } + + public static class MultiPartKeyGenerator extends KeyGenerator + { + @VisibleForTesting + public final int[] sizes; + protected final int totalSize; + + public MultiPartKeyGenerator(List> columns) + { + super(columns); + assert columns.size() > 1 : "It makes sense to use a multipart generator if you have more than one column, but you have " + columns.size(); + + this.sizes = requiredBytes(columns); + int total = 0; + for (int size : sizes) + total += size; + + this.totalSize = total; + } + + public long deflate(Object[] values) + { + long[] stiched = DataGenerators.deflateKey(columns, values); + if (stiched == null) + return NIL_DESCR; + return stitch(stiched); + } + + public Object[] inflate(long descriptor) + { + return DataGenerators.inflateKey(columns, descriptor, slice(descriptor)); + } + + // Checks whether we need to invert a slice sign to preserve order of the sliced descriptor + public boolean shouldInvertSign(int idx) + { + Bijections.Bijection gen = columns.get(idx).generator(); + + int maxSliceSize = gen.byteSize(); + int actualSliceSize = sizes[idx]; + + + if (idx == 0) + { + // We consume a sign of a descriptor (long, long), (int, int), etc. + if (totalSize == Long.BYTES) + { + // If we use only 3 bytes for a 4-byte int, or 4 bytes for a 8-byte int, + // they're effectively unsigned/byte-ordered, so their order won't match + if (maxSliceSize > actualSliceSize) + return true; + // Sign of the current descriptor should match the sign of the slice. + // For example, (tinyint, double) or (double, tinyint). In the first case (tinyint first), + // sign of the first component is going to match the sign of the descriptor. + // In the second case (double first), double is 7-bit, but its most significant bit + // does not hold a sign, so we have to invert it to match sign of the descriptor. + else + return gen.unsigned(); + } + // We do not consume a sign of a descriptor (float, tinyint), (int, tinyint), etc, + // so we have to only invert signs of the values, since their order doesn't match. + else + { + assert maxSliceSize == actualSliceSize; + return !gen.unsigned(); + } + } + else if (gen.unsigned()) + return false; + else + // We invert sign of all subsequent chunks if they have enough entropy to have a sign bit set + return maxSliceSize == actualSliceSize; + } + + public long[] slice(long descriptor) + { + long[] pieces = new long[sizes.length]; + long pos = totalSize; + for (int i = 0; i < sizes.length; i++) + { + final int size = sizes[i]; + long piece = descriptor >> ((pos - size) * Byte.SIZE); + + if (shouldInvertSign(i)) + piece ^= Bytes.signMaskFor(size); + + piece &= Bytes.bytePatternFor(size); + + pieces[i] = piece; + pos -= size; + } + return pieces; + } + + public long stitch(long[] parts) + { + long stitched = 0; + int consumed = 0; + for (int i = sizes.length - 1; i >= 0; i--) + { + int size = sizes[i]; + long piece = parts[i]; + + if (shouldInvertSign(i)) + piece ^= Bytes.signMaskFor(size); + + piece &= Bytes.bytePatternFor(size); + stitched |= piece << (consumed * Byte.SIZE); + consumed += size; + } + return stitched; + } + + + public long minValue(int idx) + { + long res = columns.get(idx).generator().minValue(); + // Inverting sign is important for range queries and RTs, since we're + // making boundaries that'll be stitched later. + if (shouldInvertSign(idx)) + res ^= Bytes.signMaskFor(sizes[idx]); + return res; + } + + public long maxValue(int idx) + { + long res = columns.get(idx).generator().maxValue(); + if (shouldInvertSign(idx)) + res ^= Bytes.signMaskFor(sizes[idx]); + return res; + } + + public int byteSize() + { + return totalSize; + } + + public int compare(long l, long r) + { + return Long.compare(l, r); + } + } +} \ No newline at end of file diff --git a/test/harry/main/org/apache/cassandra/harry/gen/EntropySource.java b/test/harry/main/org/apache/cassandra/harry/gen/EntropySource.java new file mode 100644 index 0000000000..bda37a9070 --- /dev/null +++ b/test/harry/main/org/apache/cassandra/harry/gen/EntropySource.java @@ -0,0 +1,95 @@ +/* + * 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.gen; + +import com.google.common.annotations.VisibleForTesting; +import org.apache.cassandra.harry.gen.rng.PcgRSUFast; + +/** + * Random generator interface that offers: + * * Settable seed + * * Ability to generate multiple "next" random seeds + * * Ability to generate multiple "dependent" seeds, from which we can retrace the base seed with subtraction + */ +public interface EntropySource +{ + long next(); + void seed(long seed); + + // We derive from entropy source here to avoid letting the step change state for other states + // For example, if you start drawing more entropy bits from one of the steps, but won't change + // other steps, their states won't change either. + EntropySource derive(); + + int nextInt(); + int nextInt(int max); + int nextInt(int min, int max); + float nextFloat(); + + /** + * Code is adopted from a similar method in JDK 17, and has to be removed as soon as we migrate to JDK 17. + */ + default long nextLong(long min, long max) { + long ret = next(); + if (min < max) { + final long n = max - min; + final long m = n - 1; + if ((n & m) == 0L) { + ret = (ret & m) + min; + } else if (n > 0L) { + for (long u = ret >>> 1; + u + m - (ret = u % n) < 0L; + u = next() >>> 1) + ; + ret += min; + } + else { + while (ret < min || ret >= max) + ret = next(); + } + } + return ret; + } + + default float nextFloat(float min, float max) { + float r = nextFloat(); + if (min < max) { + r = r * (max - min) + min; + if (r >= max) + r = Float.intBitsToFloat(Float.floatToIntBits(max) - 1); + } + return r; + } + + boolean nextBoolean(); + + @VisibleForTesting + static EntropySource forTests() + { + return forTests(System.currentTimeMillis()); + } + + @VisibleForTesting + static EntropySource forTests(long seed) + { + System.out.println("Seed: " + seed); + return new PcgRSUFast(seed, 1); + } +} + diff --git a/test/harry/main/org/apache/cassandra/harry/gen/Generator.java b/test/harry/main/org/apache/cassandra/harry/gen/Generator.java new file mode 100644 index 0000000000..30d306422c --- /dev/null +++ b/test/harry/main/org/apache/cassandra/harry/gen/Generator.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.harry.gen; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; +import java.util.function.BiFunction; +import java.util.function.Function; +import java.util.function.Supplier; + +import org.apache.cassandra.harry.gen.rng.PcgRSUFast; + +public interface Generator +{ + // It might be better if every generator has its own independent rng (or even implementation-dependent rng). + // This way we can have simpler interface: generate a value from the long and invertible iterator does the opposite. + // More things can be invertible this way. For example, entire primary keys can be invertible. + T generate(EntropySource rng); + + default List generate(EntropySource rng, int n) + { + List res = new ArrayList<>(n); + for (int i = 0; i < n; i++) + res.add(generate(rng)); + return res; + } + + default Surjections.Surjection toSurjection(long streamId) + { + return (current) -> { + EntropySource rng = new PcgRSUFast(current, streamId); + return generate(rng); + }; + } + + // TODO: this is only applicable to surjections, it seems + public static class Value + { + public final long descriptor; + public final T value; + + public Value(long descriptor, T value) + { + this.descriptor = descriptor; + this.value = value; + } + + public String toString() + { + return "Value{" + + "descriptor=" + descriptor + + ", value=" + (value.getClass().isArray() ? Arrays.toString((Object[]) value) : value) + '}'; + } + } + + default Supplier bind(EntropySource rand) + { + return () -> generate(rand); + } + + default Generator map(Function map) + { + return (rand) -> map.apply(generate(rand)); + } + + default Generator flatMap(Function> fmap) + { + return (rand) -> fmap.apply(generate(rand)).generate(rand); + } + + default Generator zip(Generator g1, BiFunction map) + { + return (rand) -> map.apply(Generator.this.generate(rand), g1.generate(rand)); + } + + default Generator zip(Generator g1, Generator g2, TriFunction map) + { + return (rand) -> map.apply(Generator.this.generate(rand), g1.generate(rand), g2.generate(rand)); + } + + default Generator zip(Generator g1, Generator g2, Generator g3, QuatFunction map) + { + return (rand) -> map.apply(Generator.this.generate(rand), g1.generate(rand), g2.generate(rand), g3.generate(rand)); + } + + default Generator zip(Generator g1, Generator g2, Generator g3, Generator g4, QuinFunction map) + { + return (rand) -> map.apply(Generator.this.generate(rand), g1.generate(rand), g2.generate(rand), g3.generate(rand), g4.generate(rand)); + } + + default Generator zip(Generator g1, Generator g2, Generator g3, Generator g4, Generator g5, SechFunction map) + { + return (rand) -> map.apply(Generator.this.generate(rand), g1.generate(rand), g2.generate(rand), g3.generate(rand), g4.generate(rand), g5.generate(rand)); + } + + + public interface TriFunction + { + RES apply(T1 t1, T2 t2, T3 t3); + } + + public interface QuatFunction + { + RES apply(T1 t1, T2 t2, T3 t3, T4 t4); + } + + public interface QuinFunction + { + RES apply(T1 t1, T2 t2, T3 t3, T4 t4, T5 t5); + } + + public interface SechFunction + { + RES apply(T1 t1, T2 t2, T3 t3, T4 t4, T5 t5, T6 t6); + } +} \ No newline at end of file diff --git a/test/harry/main/org/apache/cassandra/harry/gen/Generators.java b/test/harry/main/org/apache/cassandra/harry/gen/Generators.java new file mode 100644 index 0000000000..6b46ea079d --- /dev/null +++ b/test/harry/main/org/apache/cassandra/harry/gen/Generators.java @@ -0,0 +1,86 @@ +/* + * 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.gen; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.HashSet; +import java.util.List; +import java.util.Set; +import java.util.function.Supplier; + +public class Generators +{ + public static Generator pick(List ts) + { + return (rng) -> { + return ts.get(rng.nextInt(0, ts.size() - 1)); + }; + } + + public static Generator pick(T... ts) + { + return pick(Arrays.asList(ts)); + } + + public static Generator> subsetGenerator(List list) + { + return subsetGenerator(list, 0, list.size() - 1); + } + + public static Generator> subsetGenerator(List list, int minSize, int maxSize) + { + return (rng) -> { + int count = rng.nextInt(minSize, maxSize); + Set set = new HashSet<>(); + for (int i = 0; i < count; i++) + set.add(list.get(rng.nextInt(minSize, maxSize))); + + return (List) new ArrayList<>(set); + }; + } + + public static > Generator enumValues(Class e) + { + return pick(Arrays.asList(e.getEnumConstants())); + } + + public static Generator> list(Generator of, int maxSize) + { + return list(of, 0, maxSize); + } + + public static Generator> list(Generator of, int minSize, int maxSize) + { + return (rng) -> { + int count = rng.nextInt(minSize, maxSize); + return of.generate(rng, count); + }; + } + + public static Generator constant(T constant) + { + return (random) -> constant; + } + + public static Generator constant(Supplier constant) + { + return (random) -> constant.get(); + } +} \ No newline at end of file diff --git a/test/harry/main/org/apache/cassandra/harry/gen/StringBijection.java b/test/harry/main/org/apache/cassandra/harry/gen/StringBijection.java new file mode 100644 index 0000000000..91b2aa4f29 --- /dev/null +++ b/test/harry/main/org/apache/cassandra/harry/gen/StringBijection.java @@ -0,0 +1,172 @@ +/* + * 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.gen; + +import java.util.HashMap; +import java.util.Iterator; +import java.util.Map; +import java.util.Random; +import java.util.Set; +import java.util.TreeSet; + +import org.apache.cassandra.harry.gen.rng.RngUtils; + +public class StringBijection implements Bijections.Bijection +{ + public static final int NIBBLES_SIZE = 256; + private final String[] nibbles; + private final Map inverse; + private final int nibbleSize; + private final int maxRandomBytes; + + public StringBijection() + { + this(alphabetNibbles(8), 8, 10); + } + + public StringBijection(int nibbleSize, int maxRandomBytes) + { + this(alphabetNibbles(nibbleSize), nibbleSize, maxRandomBytes); + } + + public StringBijection(String[] nibbles, int nibbleSize, int maxRandomBytes) + { + assert nibbles.length == NIBBLES_SIZE; + this.nibbles = nibbles; + this.inverse = new HashMap<>(); + this.nibbleSize = nibbleSize; + + for (int i = 0; i < nibbles.length; i++) + { + assert nibbles[i].length() == nibbleSize; + inverse.put(nibbles[i], i); + } + + this.maxRandomBytes = maxRandomBytes; + } + + public String inflate(long descriptor) + { + StringBuilder builder = new StringBuilder(); + for (int i = 0; i < Long.BYTES; i++) + { + int idx = getByte(descriptor, i); + builder.append(nibbles[idx]); + } + + appendRandomBytes(builder, descriptor); + + // everything after this point can be just random, since strings are guaranteed + // to have unique prefixes + return builder.toString(); + } + + public static int getByte(long l, int idx) + { + int b = (int) ((l >> (Long.BYTES - idx - 1) * Byte.SIZE) & 0xff); + if (idx == 0) + b ^= 0x80; + return b; + } + + private void appendRandomBytes(StringBuilder builder, long descriptor) + { + long rnd = RngUtils.next(descriptor); + int remaining = RngUtils.asInt(rnd, 0, maxRandomBytes); + + while (remaining > 0) + { + rnd = RngUtils.next(rnd); + for (int i = 0; i < remaining && i < Long.BYTES; i++) + { + builder.append((char) (rnd >> (i * 8)) & 0xff); + remaining--; + } + } + } + + public long deflate(String descriptor) + { + long res = 0; + for (int i = 0; i < Long.BYTES; i++) + { + String nibble = descriptor.substring(nibbleSize * i, nibbleSize * (i + 1)); + assert inverse.containsKey(nibble) : String.format("Bad string: %s, %s", nibble, descriptor); + long idx = inverse.get(nibble); + if (i == 0) + idx ^= 0x80; + res |= idx << (Long.BYTES - i - 1) * Byte.SIZE; + } + return res; + } + + public int compare(long l, long r) + { + for (int i = 0; i < Long.BYTES; i++) + { + int cmp = Integer.compare(getByte(l, i), + getByte(r, i)); + if (cmp != 0) + return cmp; + } + return 0; + } + + public int byteSize() + { + return Long.BYTES; + } + + @Override + public String toString() + { + return "ascii(" + + "nibbleSize=" + nibbleSize + + ", maxRandomBytes=" + maxRandomBytes + + ')'; + } + + private final static char[] characters = { 'a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j', 'k', 'l', 'm', 'n', 'o', 'p', 'q', 'r', 's', 't', 'u', 'v', 'w', 'x', 'y', 'z', + 'A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I', 'J', 'K', 'L', 'M', 'N', 'O', 'P', 'Q', 'R', 'S', 'T', 'U', 'V', 'W', 'X', 'Y', 'Z' }; + + // We don't really care much about algorithmic complexity of this method since it's called only once during startup + public static String[] alphabetNibbles(int nibbleSize) + { + Random rnd = new Random(1); + + // We need to generate 256 _unique_ strings; ideally we don't want to re-sort them + Set strings = new TreeSet(); + while (strings.size() < NIBBLES_SIZE) + { + char[] chars = new char[nibbleSize]; + for (int j = 0; j < nibbleSize; j++) + chars[j] = characters[rnd.nextInt(characters.length)]; + + strings.add(new String(chars)); + } + + assert strings.size() == NIBBLES_SIZE : strings.size(); + String[] nibbles = new String[NIBBLES_SIZE]; + Iterator iter = strings.iterator(); + for (int i = 0; i < NIBBLES_SIZE; i++) + nibbles[i] = iter.next(); + + return nibbles; + } +} diff --git a/test/harry/main/org/apache/cassandra/harry/gen/Surjections.java b/test/harry/main/org/apache/cassandra/harry/gen/Surjections.java new file mode 100644 index 0000000000..2358884ccb --- /dev/null +++ b/test/harry/main/org/apache/cassandra/harry/gen/Surjections.java @@ -0,0 +1,170 @@ +/* + * 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.gen; + +import java.util.Arrays; +import java.util.List; +import java.util.Map; +import java.util.TreeMap; +import java.util.function.Function; +import java.util.function.LongFunction; +import java.util.function.Supplier; + +import com.google.common.annotations.VisibleForTesting; +import org.apache.cassandra.harry.gen.rng.PcgRSUFast; +import org.apache.cassandra.harry.gen.rng.RngUtils; + +public class Surjections +{ + public static Surjection constant(Supplier constant) + { + return (current) -> constant.get(); + } + + public static Surjection constant(T constant) + { + return (current) -> constant; + } + + public static Surjection pick(List ts) + { + return new Surjection() + { + public T inflate(long current) + { + return ts.get(RngUtils.asInt(current, 0, ts.size() - 1)); + } + + @Override + public String toString() + { + return String.format("Surjection#pick{from=%s}", ts); + } + }; + } + + public static long[] weights(int... weights) + { + long[] res = new long[weights.length]; + for (int i = 0; i < weights.length; i++) + { + long w = weights[i]; + res[i] = w << 32 | i; + } + return res; + } + + public static Surjection weighted(int[] weights, T... items) + { + return weighted(weights(weights), items); + } + + public static Surjection weighted(long[] weights, T... items) + { + assert weights.length == items.length; + + Arrays.sort(weights); + TreeMap weightMap = new TreeMap(); + int prev = 0; + for (int i = 0; i < weights.length; i++) + { + long orig = weights[i]; + int weight = (int) (orig >> 32); + int idx = (int) orig; + weightMap.put(prev, items[idx]); + prev += weight; + } + + return (i) -> { + int weight = RngUtils.asInt(i, 0, 100); + return weightMap.floorEntry(weight).getValue(); + }; + } + + public static Surjection weighted(Map weights) + { + TreeMap weightMap = new TreeMap(); + int sum = 0; + for (Map.Entry entry : weights.entrySet()) + { + sum += entry.getValue(); + weightMap.put(sum, entry.getKey()); + } + + int max = sum; + return (i) -> { + int weight = RngUtils.asInt(i, 0, max); + return weightMap.ceilingEntry(weight).getValue(); + }; + } + + public static Surjection pick(T... ts) + { + return pick(Arrays.asList(ts)); + } + + public static > Surjection enumValues(Class e) + { + return enumValues(e, e.getEnumConstants()); + } + + public static > Surjection enumValues(Class klass, T... e) + { + return pick(Arrays.asList(e)); + } + + public interface Surjection + { + T inflate(long descriptor); + + default Surjection map(Function map) + { + return (current) -> map.apply(inflate(current)); + } + + default LongFunction toFn() + { + return new LongFunction() + { + public T apply(long value) + { + return inflate(value); + } + }; + } + + default Generator toGenerator() + { + return new Generator() + { + public T generate(EntropySource rng) + { + return inflate(rng.next()); + } + }; + } + + @VisibleForTesting + default Supplier toSupplier() + { + EntropySource rng = new PcgRSUFast(0, 0); + return () -> inflate(rng.next()); + } + } +} diff --git a/test/harry/main/org/apache/cassandra/harry/gen/distribution/Distribution.java b/test/harry/main/org/apache/cassandra/harry/gen/distribution/Distribution.java new file mode 100644 index 0000000000..ac961f9ac7 --- /dev/null +++ b/test/harry/main/org/apache/cassandra/harry/gen/distribution/Distribution.java @@ -0,0 +1,136 @@ +/* + * 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.gen.distribution; + +import org.apache.cassandra.harry.core.Configuration; + +public interface Distribution +{ + Configuration.DistributionConfig toConfig(); + + interface DistributionFactory + { + Distribution make(); + } + + long skew(long i); + + class IdentityDistribution implements Distribution + { + public Configuration.DistributionConfig toConfig() + { + return new Configuration.IdentityDistributionConfig(); + } + + public long skew(long i) + { + return i; + } + } + + class ConstantDistribution implements Distribution + { + public final long constant; + + public ConstantDistribution(long constant) + { + this.constant = constant; + } + + public Configuration.DistributionConfig toConfig() + { + return new Configuration.ConstantDistributionConfig(constant); + } + + public long skew(long i) + { + return constant; + } + + public String toString() + { + return "ConstantDistribution{" + + "constant=" + constant + + '}'; + } + } + + class ScaledDistribution implements Distribution + { + private final long min; + private final long max; + + public ScaledDistribution(long min, long max) + { + this.min = min; + this.max = max; + } + + public Configuration.DistributionConfig toConfig() + { + return new Configuration.ScaledDistributionConfig(min, max); + } + + public long skew(long i) + { + return scale(i, min, max); + } + + public static long scale(long value, long min, long max) + { + if (value == 0) + return (max - min) / 2; + + double nomalized = (1.0 * Math.abs(value)) / Long.MAX_VALUE; + double diff = 0.5 * (max - min); + if (value > 0) + return (long) (min + nomalized * diff); + else + return (long) (max - nomalized * diff); + } + + public String toString() + { + return "ScaledDistribution{" + + "min=" + min + + ", max=" + max + + '}'; + } + } + + class NormalDistribution implements Distribution + { + private final org.apache.commons.math3.distribution.NormalDistribution delegate; + + public NormalDistribution() + { + delegate = new org.apache.commons.math3.distribution.NormalDistribution(); + } + + public Configuration.DistributionConfig toConfig() + { + return new Configuration.NormalDistributionConfig(); + } + + public long skew(long i) + { + return (long) delegate.cumulativeProbability((double) i); + } + } +} diff --git a/test/harry/main/org/apache/cassandra/harry/gen/rng/JdkRandomEntropySource.java b/test/harry/main/org/apache/cassandra/harry/gen/rng/JdkRandomEntropySource.java new file mode 100644 index 0000000000..5ddc885283 --- /dev/null +++ b/test/harry/main/org/apache/cassandra/harry/gen/rng/JdkRandomEntropySource.java @@ -0,0 +1,83 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.cassandra.harry.gen.rng; + +import java.util.Random; + +import org.apache.cassandra.harry.gen.EntropySource; + +public class JdkRandomEntropySource implements EntropySource +{ + private final Random rng; + + public JdkRandomEntropySource(long seed) + { + this(new Random(seed)); + } + + public JdkRandomEntropySource(Random rng) + { + this.rng = rng; + } + + public long next() + { + return rng.nextLong(); + } + + public void seed(long seed) + { + rng.setSeed(seed); + } + + public EntropySource derive() + { + return new JdkRandomEntropySource(new Random(rng.nextLong())); + } + + public int nextInt() + { + return rng.nextInt(); + } + + public int nextInt(int max) + { + return rng.nextInt(max); + } + + public int nextInt(int min, int max) + { + return rng.nextInt(max) + min; + } + + public long nextLong() + { + return rng.nextLong(); + } + + public float nextFloat() + { + return rng.nextFloat(); + } + + public boolean nextBoolean() + { + return rng.nextBoolean(); + } +} diff --git a/test/harry/main/org/apache/cassandra/harry/gen/rng/PCGFastPure.java b/test/harry/main/org/apache/cassandra/harry/gen/rng/PCGFastPure.java new file mode 100644 index 0000000000..3145d3a669 --- /dev/null +++ b/test/harry/main/org/apache/cassandra/harry/gen/rng/PCGFastPure.java @@ -0,0 +1,145 @@ +/* + * 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.gen.rng; + +/** + * Immutable / pure implementaiton of PCG. + *

+ * Base algorithm translated from C/C++ code: + * https://github.com/imneme/pcg-c + * https://github.com/imneme/pcg-cpp + *

+ * Original library developed by Melissa O'Neill (oneill@pcg-random.org) + */ +public class PCGFastPure +{ + private static final long NEXT_MULTIPLIER = 6364136223846793005L; + + private static final long XORSHIFT_MULTIPLIER = Long.parseUnsignedLong("12605985483714917081"); + private static final long XORSHIFT_UNMULTIPLIER = Long.parseUnsignedLong("15009553638781119849"); + + public static long advance(long generated, long steps, long stream) + { + return shuffle(advanceState(unshuffle(generated), steps, stream)); + } + + public static long streamIncrement(long stream) + { + return (stream << 1) | 1; + } + + public static long advanceState(long state, long steps, long stream) + { + long acc_mult = 1; + long acc_plus = 0; + + long cur_plus = streamIncrement(stream); + long cur_mult = NEXT_MULTIPLIER; + + while (Long.compareUnsigned(steps, 0) > 0) + { + if ((steps & 1) == 1) + { + acc_mult *= cur_mult; + acc_plus = acc_plus * cur_mult + cur_plus; + } + cur_plus *= (cur_mult + 1); + cur_mult *= cur_mult; + steps = Long.divideUnsigned(steps, 2); + } + return (acc_mult * state) + acc_plus; + } + + public static long next(long state, long stream) + { + return shuffle(nextState(unshuffle(state), stream)); + } + + public static long previous(long state, long stream) + { + return advance(state, -1, stream); + } + + public static long nextState(long state, long stream) + { + return (state * NEXT_MULTIPLIER) + streamIncrement(stream); + } + + public static long distance(long curState, long newState, long stream) + { + if (curState == newState) + return 0; + + long curPlus = streamIncrement(stream); + long curMult = NEXT_MULTIPLIER; + + long bit = 1L; + long distance = 0; + + while (curState != newState) + { + if ((curState & bit) != (newState & bit)) + { + curState = curState * curMult + curPlus; + distance |= bit; + } + assert ((curState & bit) == (newState & bit)); + bit <<= 1; + curPlus = (curMult + 1) * curPlus; + curMult *= curMult; + } + + return distance; + } + + public static long shuffle(long state) + { + long word = ((state >>> ((state >>> 59) + 5)) ^ state) * XORSHIFT_MULTIPLIER; + return (word >>> 43) ^ word; + } + + public static long unshuffle(long shuffled) + { + long word = shuffled; + word = unxorshift(word, 43); + word *= XORSHIFT_UNMULTIPLIER; + word = unxorshift(word, (int) (word >>> 59) + 5); + return word; + } + + public static long unxorshift(long x, int shift) + { + return unxorshift(x, 64, shift); + } + + public static long unxorshift(long x, int bits, int shift) + { + if (2 * shift >= bits) + return x ^ (x >>> shift); + + long lowmask1 = (1L << (bits - shift * 2)) - 1L; + long bottomBits = x & lowmask1; + long topBits = (x ^ (x >>> shift)) & ~lowmask1; + x = topBits | bottomBits; + + long lowmask2 = (1L << (bits - shift)) - 1L; + bottomBits = unxorshift(x & lowmask2, bits - shift, shift) & lowmask1; + return topBits | bottomBits; + } +} \ No newline at end of file diff --git a/test/harry/main/org/apache/cassandra/harry/gen/rng/PcgRSUFast.java b/test/harry/main/org/apache/cassandra/harry/gen/rng/PcgRSUFast.java new file mode 100644 index 0000000000..070fe24f67 --- /dev/null +++ b/test/harry/main/org/apache/cassandra/harry/gen/rng/PcgRSUFast.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.harry.gen.rng; + +import org.apache.cassandra.harry.gen.EntropySource; + +public class PcgRSUFast implements EntropySource +{ + private long state; + private long step; + + /** + * Stream number of the rng. + */ + private final long stream; + + public PcgRSUFast(long seed, long streamNumber) + { + this.stream = (streamNumber << 1) | 1; // 2* + 1 + seed(seed); + } + + @Override + public void seed(long seed) + { + state = RngUtils.xorshift64star(seed) + stream; + } + + public void advance(long steps) + { + this.step += steps; + this.state = PCGFastPure.advanceState(state, steps, stream); + } + + protected void nextStep() + { + state = PCGFastPure.nextState(state, stream); + step++; + } + + public void seek(long step) + { + advance(step - this.step); + } + + @Override + public EntropySource derive() + { + return new PcgRSUFast(PCGFastPure.nextState(state, stream), stream); + } + + @Override + public long next() + { + nextStep(); + return PCGFastPure.shuffle(state); + } + + public long nextAt(long step) + { + seek(step); + return next(); + } + + @Override + public int nextInt() + { + return RngUtils.asInt(next()); + } + + @Override + public int nextInt(int max) + { + return RngUtils.asInt(next(), max); + } + + @Override + public int nextInt(int min, int max) + { + return RngUtils.asInt(next(), min, max); + } + + @Override + public long nextLong(long min, long max) + { + return RngUtils.trim(next(), min, max); + } + + @Override + public float nextFloat() + { + return RngUtils.asFloat(next()); + } + + @Override + public boolean nextBoolean() + { + return RngUtils.asBoolean(next()); + } + + public long distance(long generated) + { + return PCGFastPure.distance(state, PCGFastPure.unshuffle(generated), stream); + } + + public long distance(PcgRSUFast other) + { + // Check if they are the same stream... + if (stream != other.stream) + { + throw new IllegalArgumentException("Can not compare generators with different " + + "streams. Those generators will never converge"); + } + + return PCGFastPure.distance(state, other.state, stream); + } +} \ No newline at end of file diff --git a/test/harry/main/org/apache/cassandra/harry/gen/rng/RngUtils.java b/test/harry/main/org/apache/cassandra/harry/gen/rng/RngUtils.java new file mode 100644 index 0000000000..3e2760006d --- /dev/null +++ b/test/harry/main/org/apache/cassandra/harry/gen/rng/RngUtils.java @@ -0,0 +1,172 @@ +/* + * 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.gen.rng; + +import java.util.function.LongSupplier; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +public class RngUtils +{ + private static final Logger logger = LoggerFactory.getLogger(RngUtils.class); + + private static final long CONSTANT = 0x2545F4914F6CDD1DL; + public static long next(long input) + { + if (input == 0) + return next(CONSTANT); + + return xorshift64star(input); + } + + public static long xorshift64star(long input) + { + input ^= input >> 12; + input ^= input << 25; // b + input ^= input >> 27; // c + return input * CONSTANT; + } + + public static long[] next(long current, int n) + { + long[] next = new long[n]; + for (int i = 0; i < n; i++) + { + current = next(current); + next[i] = current; + } + return next; + } + + public static byte[] asBytes(long current) + { + byte[] bytes = new byte[Long.BYTES]; + for (int i = 0; i < Long.BYTES; i++) + { + bytes[i] = (byte) (current & 0xFF); + current >>= current; + } + return bytes; + } + + public static byte asByte(long current) + { + return (byte) current; + } + + public static int asInt(long current) + { + return (int) current; + } + + // TODO: this needs some improvement + public static int asInt(long current, int max) + { + return Math.abs((int) current % (max + 1)); + } + + public static long trim(long current, long max) + { + return Math.abs(current % (max + 1)); + } + + // Generate a value in [min, max] range: from min _inclusive_ to max _inclusive_. + public static int asInt(long current, int min, int max) + { + if (min == max) + return min; + return min + asInt(current, max - min); + } + + public static long trim(long current, long min, long max) + { + if (min == max) + return min; + return min + trim(current, max - min); + } + + public static boolean asBoolean(long current) + { + return (current & 1) == 1; + } + + public static float asFloat(long current) + { + return Float.intBitsToFloat((int) current); + } + + public static double asDouble(long current) + { + return Double.longBitsToDouble(current); + } + + static long bitmask(long n) + { + if (n == 64) return ~0L; + return (1L << n) - 1; + } + + public static long randomBits(long bits, long length, long s) + { + return randomBits(bits, length, new LongSupplier() + { + private long seed = s; + + @Override + public long getAsLong() + { + long next = PCGFastPure.advanceState(seed, 1, 1); + assert next != seed : seed; + seed = next; + return next; + } + }); + } + + public static long randomBits(long bits, long length, LongSupplier rng) + { + long mask = bitmask(length); + if (bits == length) + return mask; + long min = 0; + long max = ~0L; + int n = 0; + int steps = 0; + while (n != bits) + { + if (steps > 10_000) + { + throw new RuntimeException(String.format("Could not generate bits after 10K tries. " + + "Inputs: bits=%d, length=%d", bits, length)); + } + + long x = rng.getAsLong() & mask; + x = min | (x & max); + n = Long.bitCount(x); + if (n > bits) + max = x; + else + min = x; + + steps++; + } + return min; + } +} diff --git a/test/harry/main/org/apache/cassandra/harry/model/AgainstSutChecker.java b/test/harry/main/org/apache/cassandra/harry/model/AgainstSutChecker.java new file mode 100644 index 0000000000..a9c35337df --- /dev/null +++ b/test/harry/main/org/apache/cassandra/harry/model/AgainstSutChecker.java @@ -0,0 +1,99 @@ +/* + * 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.model; + +import java.util.List; + +import org.apache.cassandra.harry.data.ResultSetRow; +import org.apache.cassandra.harry.ddl.SchemaSpec; +import org.apache.cassandra.harry.sut.QueryModifyingSut; +import org.apache.cassandra.harry.sut.SystemUnderTest; +import org.apache.cassandra.harry.operations.CompiledStatement; +import org.apache.cassandra.harry.operations.Query; +import org.apache.cassandra.harry.tracker.DataTracker; + +/** + * A simple way to verify if something might be a Harry issue: check against a different SUT. + * + * For example, if you are using `flush` in your primary SUT, avoid using it for the secondary SUT, + * and compare results. + * + * Usually used in combination with {@link QueryModifyingSut}, which writes to + * the second SUT. + * + * SchemaSpec doubleWriteSchema = schema.cloneWithName(schema.keyspace, schema.keyspace + "_debug"); + * + * sut.schemaChange(doubleWriteSchema.compile().cql()); + * + * QueryModifyingSut sut = new QueryModifyingSut(this.sut, + * schema.table, + * doubleWriteSchema.table); + * + * + * Model model = new AgainstSutChecker(tracker, history.clock(), sut, schema, doubleWriteSchema); + */ +public class AgainstSutChecker implements Model +{ + private final OpSelectors.Clock clock; + private final SystemUnderTest sut; + private final SchemaSpec schema; + private final SchemaSpec doubleWriteTable; + private final DataTracker tracker; + + public AgainstSutChecker(DataTracker tracker, + OpSelectors.Clock clock, + SystemUnderTest sut, + SchemaSpec schema, + SchemaSpec doubleWriteTable) + { + this.clock = clock; + this.sut = sut; + this.schema = schema; + this.doubleWriteTable = doubleWriteTable; + this.tracker = tracker; + } + + public void validate(Query query) + { + tracker.beginValidation(query.pd); + CompiledStatement s1 = query.toSelectStatement(schema.allColumnsSet, true); + CompiledStatement s2 = s1.withSchema(schema.keyspace, schema.table, + doubleWriteTable.keyspace, doubleWriteTable.table); + List rows1 = SelectHelper.execute(sut, clock, s1, schema); + List rows2 = SelectHelper.execute(sut, clock, s2, doubleWriteTable); + + if (rows1.size() != rows2.size()) + throw new IllegalStateException(String.format("Sizes do not match %d %d", rows1.size(), rows2.size())); + + for (int i = 0; i < rows1.size(); i++) + { + if (!rows1.get(i).equals(rows2.get(i))) + { + throw new IllegalStateException(String.format("Rows mismatch:\n" + + "%s\n" + + "%s\n", + rows1.get(i), + rows2.get(i))); + } + } + tracker.endValidation(query.pd); + } + + +} diff --git a/test/harry/main/org/apache/cassandra/harry/model/AlwaysSamePartitionSelector.java b/test/harry/main/org/apache/cassandra/harry/model/AlwaysSamePartitionSelector.java new file mode 100644 index 0000000000..daecd029fe --- /dev/null +++ b/test/harry/main/org/apache/cassandra/harry/model/AlwaysSamePartitionSelector.java @@ -0,0 +1,92 @@ +/* + * 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.model; + +import com.fasterxml.jackson.annotation.JsonProperty; +import com.fasterxml.jackson.annotation.JsonTypeName; +import org.apache.cassandra.harry.core.Configuration; + +/** + * A simple test-only descriptor selector that can used for testing things where you only need one partition + */ +public class AlwaysSamePartitionSelector extends OpSelectors.PdSelector +{ + private final long pd; + + public AlwaysSamePartitionSelector(long pd) + { + this.pd = pd; + } + + protected long pd(long lts) + { + return 0; + } + + public long nextLts(long lts) + { + return lts + 1; + } + + public long prevLts(long lts) + { + return lts - 1; + } + + public long maxLtsFor(long pd) + { + return 1000; + } + + public long minLtsAt(long position) + { + return 0; + } + + public long minLtsFor(long pd) + { + return 0; + } + + public long positionFor(long lts) + { + return 0; + } + + public long maxPosition(long maxLts) + { + return 0; + } + + @JsonTypeName("always_same") + public static class AlwaysSamePartitionSelectorConfiguration implements Configuration.PDSelectorConfiguration + { + private final long pd; + + public AlwaysSamePartitionSelectorConfiguration(@JsonProperty("pd") long pd) + { + this.pd = pd; + } + + public OpSelectors.PdSelector make(OpSelectors.PureRng rng) + { + return new AlwaysSamePartitionSelector(pd); + } + } +} diff --git a/test/harry/main/org/apache/cassandra/harry/model/Model.java b/test/harry/main/org/apache/cassandra/harry/model/Model.java new file mode 100644 index 0000000000..0a85eaffeb --- /dev/null +++ b/test/harry/main/org/apache/cassandra/harry/model/Model.java @@ -0,0 +1,47 @@ +/* + * 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.model; + +import org.apache.cassandra.harry.core.Run; +import org.apache.cassandra.harry.operations.Query; + +public interface Model +{ + long NO_TIMESTAMP = Long.MIN_VALUE; + + void validate(Query query); + + interface ModelFactory + { + Model make(Run run); + } + + class ValidationException extends RuntimeException + { + public ValidationException(String trackerState, String partitionState, String observedState, String format, Object... objects) + { + super(String.format(format, objects) + + "\nTracker state:\n" + trackerState + + "\nPartition state:\n" + partitionState + + "\nObserved state:\n" + observedState); + } + } + + +} \ No newline at end of file diff --git a/test/harry/main/org/apache/cassandra/harry/model/NoOpChecker.java b/test/harry/main/org/apache/cassandra/harry/model/NoOpChecker.java new file mode 100644 index 0000000000..10e7fab994 --- /dev/null +++ b/test/harry/main/org/apache/cassandra/harry/model/NoOpChecker.java @@ -0,0 +1,40 @@ +/* + * 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.model; + +import org.apache.cassandra.harry.core.Run; +import org.apache.cassandra.harry.sut.SystemUnderTest; +import org.apache.cassandra.harry.operations.Query; + +public class NoOpChecker implements Model +{ + private final Run run; + + public NoOpChecker(Run run) + { + this.run = run; + } + + public void validate(Query query) + { + run.sut.execute(query.toSelectStatement(), + // TODO: make it configurable + SystemUnderTest.ConsistencyLevel.QUORUM); + } +} diff --git a/test/harry/main/org/apache/cassandra/harry/model/OpSelectors.java b/test/harry/main/org/apache/cassandra/harry/model/OpSelectors.java new file mode 100644 index 0000000000..fe83a418a8 --- /dev/null +++ b/test/harry/main/org/apache/cassandra/harry/model/OpSelectors.java @@ -0,0 +1,903 @@ +/* + * 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.model; + +import java.util.EnumMap; +import java.util.Iterator; +import java.util.List; +import java.util.Map; + +import org.apache.cassandra.harry.core.Configuration; +import com.google.common.annotations.VisibleForTesting; +import org.apache.cassandra.harry.ddl.ColumnSpec; +import org.apache.cassandra.harry.ddl.SchemaSpec; +import org.apache.cassandra.harry.gen.Bytes; +import org.apache.cassandra.harry.gen.rng.PCGFastPure; +import org.apache.cassandra.harry.gen.rng.RngUtils; +import org.apache.cassandra.harry.gen.Surjections; +import org.apache.cassandra.harry.gen.distribution.Distribution; +import org.apache.cassandra.harry.runner.EarlyExitException; +import org.apache.cassandra.harry.util.BitSet; + +import static org.apache.cassandra.harry.gen.DataGenerators.NIL_DESCR; +import static org.apache.cassandra.harry.gen.DataGenerators.UNSET_DESCR; + +/** + * Row (deflated) data selectors. Not calling them generators, since their output is entirely + * deterministic, and for each input they are able to produce a single output. + *

+ * This is more or less a direct translation of the formalization. + *

+ * All functions implemented by this interface have to _always_ produce same outputs for same inputs. + * Most of the functions, with the exception of real-time clock translations, should be pure. + *

+ * Functions that are reverse of their counterparts are prefixed with "un" + */ +public interface OpSelectors +{ + interface PureRng + { + long randomNumber(long i, long stream); + + long sequenceNumber(long r, long stream); + + default long next(long r) + { + return next(r, 0); + } + + long next(long r, long stream); + + long prev(long r, long stream); + + default long prev(long r) + { + return next(r, 0); + } + } + + /** + * 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 clock can + * be taken to map a real-time timestamp from the value retrieved from the database in order + * to map it back to the logical timestamp of the operation that wrote this value. + */ + interface Clock + { + /** + * Returns a _real-time_ time timestamp given a _logical_ timestamp + */ + long rts(long lts); + /** + * Returns a _logical_ time timestamp given a _real-time_ timestamp + */ + long lts(long rts); + + /** + * Returns the next/smallest logical timestamp that has not yet been visited, and + * moves internal state to indicate that his timestamp has been taken. + */ + long nextLts(); + + /** + * Same as #nextLts, but does not change internal state + */ + long peek(); + + Configuration.ClockConfiguration toConfig(); + } + + interface ClockFactory + { + Clock make(); + } + + /** + * *Partition descriptor selector* controls how partitions is selected based on the current logical + * timestamp. 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 will + * retire one partition descriptor, and pick one instead of it. + */ + abstract class PdSelector + { + @VisibleForTesting + protected abstract long pd(long lts); + + public long pd(long lts, SchemaSpec schema) + { + return schema.adjustPdEntropy(pd(lts)); + } + + // previous and next LTS with that will yield same pd + public abstract long nextLts(long lts); + + public abstract long prevLts(long lts); + + public abstract long maxLtsFor(long pd); + public abstract long minLtsAt(long position); + + public abstract long minLtsFor(long pd); + public abstract long maxPosition(long maxLts); + } + + interface PdSelectorFactory + { + PdSelector make(PureRng rng); + } + + interface DescriptorSelectorFactory + { + DescriptorSelector make(PureRng rng, SchemaSpec schemaSpec); + } + + /** + * DescriptorSelector controls how clustering descriptors are picked within the partition: + * how many rows there can be in a partition, how many rows will be 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. + */ + abstract class DescriptorSelector + { + public abstract int operationsPerLts(long lts); + + public abstract int maxPartitionSize(); + + public abstract boolean isCdVisitedBy(long pd, long lts, long cd); + + // clustering descriptor is calculated using operation id and not modification id, since + // value descriptors are calculated using modification ids. + public long cd(long pd, long lts, long opId, SchemaSpec schema) + { + return schema.adjustCdEntropy(cd(pd, lts, opId)); + } + + /** + * Currently, we do not allow visiting the same row more than once per lts, which means that: + *

+ * * `max(opId)` returned `cds` have to be unique for any `lts/pd` pair + * * {@code max(opId) < maxPartitionSize} + */ + @VisibleForTesting + protected abstract long cd(long pd, long lts, long opId); + + public long randomCd(long pd, long entropy, SchemaSpec schema) + { + return schema.adjustCdEntropy(randomCd(pd, entropy)); + } + + public abstract long randomCd(long pd, long entropy); + + @VisibleForTesting + protected abstract long vd(long pd, long cd, long lts, long opId, int col); + + public long[] vds(long pd, long cd, long lts, long opId, OperationKind opType, SchemaSpec schema) + { + BitSet setColumns = columnMask(pd, lts, opId, opType); + return descriptors(pd, cd, lts, opId, schema.regularColumns, schema.regularColumnsMask(), setColumns, schema.regularColumnsOffset); + } + + public long[] sds(long pd, long cd, long lts, long opId, OperationKind opType, SchemaSpec schema) + { + BitSet setColumns = columnMask(pd, lts, opId, opType); + return descriptors(pd, cd, lts, opId, schema.staticColumns, schema.staticColumnsMask(), setColumns, schema.staticColumnsOffset); + } + + public long[] descriptors(long pd, long cd, long lts, long opId, List> columns, BitSet mask, BitSet setColumns, int offset) + { + assert opId < operationsPerLts(lts) : String.format("Operation id %d exceeds the maximum expected number of operations per lts %d", + opId, operationsPerLts(lts)); + long[] descriptors = new long[columns.size()]; + + for (int i = 0; i < descriptors.length; i++) + { + int col = offset + i; + if (setColumns.isSet(col, mask)) + { + ColumnSpec spec = columns.get(i); + long vd = vd(pd, cd, lts, opId, col) & Bytes.bytePatternFor(spec.type.maxSize()); + assert vd != UNSET_DESCR : "Ambiguous unset descriptor generated for the value"; + assert vd != NIL_DESCR : "Ambiguous nil descriptor generated for the value"; + + descriptors[i] = vd; + } + else + { + descriptors[i] = UNSET_DESCR; + } + } + return descriptors; + } + + public abstract OperationKind operationType(long pd, long lts, long opId); + + public abstract BitSet columnMask(long pd, long lts, long opId, OperationKind opType); + + public abstract long opId(long pd, long lts, long cd); + + } + + class PCGFast implements PureRng + { + private final long seed; + + public PCGFast(long seed) + { + this.seed = seed; + } + + public long randomNumber(long i, long stream) + { + return PCGFastPure.shuffle(PCGFastPure.advanceState(seed, i, stream)); + } + + public long sequenceNumber(long r, long stream) + { + return PCGFastPure.distance(seed, PCGFastPure.unshuffle(r), stream); + } + + public long next(long r, long stream) + { + return PCGFastPure.next(r, stream); + } + + public long prev(long r, long stream) + { + return PCGFastPure.previous(r, stream); + } + } + + /** + * Generates partition descriptors, based on LTS as if we had a sliding window. + *

+ * Each {@code windowSize * switchAfter} steps, we move the window by one, effectively + * expiring one partition descriptor, and adding one partition descriptor to the window. + *

+ * For any LTS, we can calculate previous and next LTS on which it will visit the same + * partition + */ + class DefaultPdSelector extends OpSelectors.PdSelector + { + public final static long PARTITION_DESCRIPTOR_STREAM_ID = 0x706b; + + private final PureRng rng; + + private final long slideAfterRepeats; + private final long switchAfter; + private final long windowSize; + + private final long positionOffset; + private final long positionWindowSize; + + public DefaultPdSelector(PureRng rng, long windowSize, long slideAfterRepeats) + { + this(rng, windowSize, slideAfterRepeats, 0L, Long.MAX_VALUE); + } + + public DefaultPdSelector(PureRng rng, long windowSize, long slideAfterRepeats, long positionOffset, long positionWindowSize) + { + this.rng = rng; + this.slideAfterRepeats = slideAfterRepeats; + this.windowSize = windowSize; + this.switchAfter = windowSize * slideAfterRepeats; + this.positionOffset = positionOffset; + this.positionWindowSize = positionWindowSize; + } + + public Iterator allPds(long maxLts) + { + final long maxPosition = maxPosition(maxLts); + return new Iterator() + { + private long position = 0; + + public boolean hasNext() + { + return position <= maxPosition; + } + + public Long next() + { + long pd = rng.randomNumber(adjustPosition(position), PARTITION_DESCRIPTOR_STREAM_ID); + position++; + return pd; + } + }; + } + + protected long pd(long lts) + { + return rng.randomNumber(adjustPosition(positionFor(lts)), PARTITION_DESCRIPTOR_STREAM_ID); + } + + public long minLtsAt(long position) + { + if (position < windowSize) + return position; + + long windowStart = (position - (windowSize - 1)) * slideAfterRepeats * windowSize; + return windowStart + windowSize - 1; + } + + public long minLtsFor(long pd) + { + long position = positionForPd(pd); + return minLtsAt(position); + } + + public long randomVisitedPd(long maxLts, long visitLts, SchemaSpec schema) + { + long maxPosition = maxPosition(maxLts); + if (maxPosition == 0) + return pdAtPosition(0, schema); + + int idx = RngUtils.asInt(rng.randomNumber(visitLts, maxPosition), 0, (int) (maxPosition - 1)); + return pdAtPosition(idx, schema); + } + + public long maxPosition(long maxLts) + { + long timesCycled = maxLts / switchAfter; + long windowStart = timesCycled * windowSize; + + // We have cycled through _current_ window at least once + if (maxLts > (windowStart * slideAfterRepeats) + windowSize) + return windowStart + windowSize; + return windowStart + maxLts % windowSize; + } + + public long positionFor(long lts) + { + long windowStart = lts / switchAfter; + return windowStart + lts % windowSize; + } + + /** + * We only adjust position right before we go to the PCG RNG to grab a partition id, since we want + * the fact that PCG is actually offset to be hidden from the user and even the rest of this class. + */ + private long adjustPosition(long position) + { + if (position > positionWindowSize) + throw new EarlyExitException(String.format("Partition position has wrapped around, so can not be safely used. " + + "This runner has been given %d partitions, and if we wrap back to " + + "position 0, partition state is not going to be inflatable, since" + + "nextLts will not jump to the lts that is about to be visited. " + + "Increase rows per visit, batch size, or slideAfter repeats.", + positionWindowSize)); + return positionOffset + position; + } + + /** + * When computing position from pd or lts, we need to translate the offset back to the original, since + * it is not aware (and should not be) of the fact that positions are offset. + */ + private long unadjustPosition(long position) + { + return position - positionOffset; + } + + public long positionForPd(long pd) + { + return unadjustPosition(rng.sequenceNumber(pd, PARTITION_DESCRIPTOR_STREAM_ID)); + } + + public long pdAtPosition(long position, SchemaSpec schemaSpec) + { + return schemaSpec.adjustCdEntropy(rng.randomNumber(adjustPosition(position), PARTITION_DESCRIPTOR_STREAM_ID)); + } + + public long nextLts(long lts) + { + long slideCount = lts / switchAfter; + long positionInCycle = lts - slideCount * switchAfter; + long nextRepeat = positionInCycle / windowSize + 1; + + if (nextRepeat > slideAfterRepeats || + (nextRepeat == slideAfterRepeats && (positionInCycle % windowSize) == 0)) + return -1; + + // last cycle before window slides; next window will have shifted by one + if (nextRepeat == slideAfterRepeats) + positionInCycle -= 1; + + return slideCount * switchAfter + windowSize + positionInCycle; + } + + public long prevLts(long lts) + { + long slideCount = lts / switchAfter; + long positionInCycle = lts - slideCount * switchAfter; + long prevRepeat = positionInCycle / windowSize - 1; + + if (lts < windowSize || + prevRepeat < -1 || + (prevRepeat == -1 && (positionInCycle % windowSize) == (windowSize - 1))) + return -1; + + // last cycle before window slides; next window will have shifted by one + if (prevRepeat == -1) + positionInCycle += 1; + + return slideCount * switchAfter - windowSize + positionInCycle; + } + + public long maxLtsFor(long pd) + { + long position = positionForPd(pd); + return position * switchAfter + (slideAfterRepeats - 1) * windowSize; + } + + public String toString() + { + return "DefaultPdSelector{" + + "slideAfterRepeats=" + slideAfterRepeats + + ", windowSize=" + windowSize + + '}'; + } + } + + static ColumnSelectorBuilder columnSelectorBuilder() + { + return new ColumnSelectorBuilder(); + } + + class ColumnSelectorBuilder + { + private Map> m; + + public ColumnSelectorBuilder() + { + this.m = new EnumMap<>(OperationKind.class); + } + + public ColumnSelectorBuilder forAll(SchemaSpec schema) + { + return forAll(schema, BitSet.surjection(schema.allColumns.size())); + } + + // TODO: change bitsets to take into account _all_ columns not only regulars + public ColumnSelectorBuilder forAll(SchemaSpec schema, Surjections.Surjection orig) + { + for (OperationKind type : OperationKind.values()) + { + Surjections.Surjection gen = orig; + + switch (type) + { + case UPDATE_WITH_STATICS: + case DELETE_COLUMN_WITH_STATICS: + gen = (descriptor) -> { + long counter = 0; + while (counter <= 100) + { + BitSet bitSet = orig.inflate(descriptor); + if ((schema.regularColumns.isEmpty() || !bitSet.allUnset(schema.regularColumnsMask)) + && (schema.staticColumns.isEmpty() || !bitSet.allUnset(schema.staticColumnsMask))) + return bitSet; + + descriptor = RngUtils.next(descriptor); + counter++; + } + throw new RuntimeException(String.format("Could not generate a value after %d attempts.", counter)); + }; + break; + // Can not have an UPDATE statement without anything to update + case UPDATE: + gen = descriptor -> { + long counter = 0; + while (counter <= 100) + { + BitSet bitSet = orig.inflate(descriptor); + + if (!bitSet.allUnset(schema.regularColumnsMask)) + return bitSet; + + descriptor = RngUtils.next(descriptor); + counter++; + } + throw new RuntimeException(String.format("Could not generate a value after %d attempts.", counter)); + }; + break; + case DELETE_COLUMN: + gen = (descriptor) -> { + long counter = 0; + while (counter <= 100) + { + BitSet bitSet = orig.inflate(descriptor); + BitSet mask = schema.regularColumnsMask; + + if (!bitSet.allUnset(mask)) + return bitSet; + + descriptor = RngUtils.next(descriptor); + counter++; + } + throw new RuntimeException(String.format("Could not generate a value after %d attempts.", counter)); + }; + break; + } + this.m.put(type, gen); + } + return this; + } + + public ColumnSelectorBuilder forWrite(Surjections.Surjection gen) + { + m.put(OperationKind.INSERT, gen); + return this; + } + + public ColumnSelectorBuilder forWrite(BitSet pickFrom) + { + return forWrite(Surjections.pick(pickFrom)); + } + + public ColumnSelectorBuilder forDelete(Surjections.Surjection gen) + { + m.put(OperationKind.DELETE_ROW, gen); + return this; + } + + public ColumnSelectorBuilder forDelete(BitSet pickFrom) + { + return forDelete(Surjections.pick(pickFrom)); + } + + public ColumnSelectorBuilder forColumnDelete(Surjections.Surjection gen) + { + m.put(OperationKind.DELETE_COLUMN, gen); + return this; + } + + public ColumnSelectorBuilder forColumnDelete(BitSet pickFrom) + { + return forColumnDelete(Surjections.pick(pickFrom)); + } + + public ColumnSelector build() + { + return (kind, descriptor) -> m.get(kind).inflate(descriptor); + } + } + + + /** + * ColumnSelector has to return BitSet specifying _all_ columns + */ + interface ColumnSelector + { + public BitSet columnMask(OperationKind operationKind, long descriptor); + } + + class HierarchicalDescriptorSelector extends DefaultDescriptorSelector + { + private final int[] fractions; + + public HierarchicalDescriptorSelector(PureRng rng, + // how many parts (at most) each subsequent "level" should contain + int[] fractions, + ColumnSelector columnSelector, + OperationSelector operationSelector, + Distribution operationsPerLtsDistribution, + int maxPartitionSize) + { + super(rng, + columnSelector, + operationSelector, + operationsPerLtsDistribution, + maxPartitionSize); + this.fractions = fractions; + } + + @Override + public long cd(long pd, long lts, long opId, SchemaSpec schema) + { + if (schema.clusteringKeys.size() <= 1) + return schema.adjustCdEntropy(super.cd(pd, lts, opId)); + + int partitionSize = maxPartitionSize(); + int clusteringOffset = clusteringOffset(lts); + long res; + if (clusteringOffset == 0) + { + res = rng.prev(opId, pd); + } + else + { + int positionInPartition = (int) ((clusteringOffset + opId) % partitionSize); + res = cd(positionInPartition, fractions, schema, rng, pd); + } + return schema.adjustCdEntropy(res); + } + + @VisibleForTesting + public static long cd(int positionInPartition, int[] fractions, SchemaSpec schema, PureRng rng, long pd) + { + long[] slices = new long[schema.clusteringKeys.size()]; + for (int i = 0; i < slices.length; i++) + { + int idx = i < fractions.length ? (positionInPartition % (fractions[i] - 1)) : positionInPartition; + slices[i] = rng.prev(idx, rng.next(pd, i + 1)); + } + + return schema.ckGenerator.stitch(slices); + } + + protected long cd(long pd, long lts, long opId) + { + throw new RuntimeException("Shouldn't be called"); + } + } + + class DefaultDescriptorSelector extends DescriptorSelector + { + protected final static long OPERATIONS_PER_LTS_STREAM = 0x5e03812e293L; + protected final static long BITSET_IDX_STREAM = 0x92eb607bef1L; + + public static OperationSelector DEFAULT_OP_SELECTOR = OperationSelector.weighted(Surjections.weights(45, 45, 3, 2, 2, 1, 1, 1), + OperationKind.INSERT, + OperationKind.INSERT_WITH_STATICS, + OperationKind.DELETE_ROW, + OperationKind.DELETE_COLUMN, + OperationKind.DELETE_COLUMN_WITH_STATICS, + OperationKind.DELETE_PARTITION, + OperationKind.DELETE_RANGE, + OperationKind.DELETE_SLICE); + + protected final PureRng rng; + protected final OperationSelector operationSelector; + protected final ColumnSelector columnSelector; + protected final Distribution operationsPerLtsDistribution; + protected final int maxPartitionSize; + + public DefaultDescriptorSelector(PureRng rng, + ColumnSelector columnMaskSelector, + OperationSelector operationSelector, + Distribution operationsPerLtsDistribution, + int maxPartitionSize) + { + this.rng = rng; + + this.operationSelector = operationSelector; + this.columnSelector = columnMaskSelector; + this.operationsPerLtsDistribution = operationsPerLtsDistribution; + this.maxPartitionSize = maxPartitionSize; + } + + public int operationsPerLts(long lts) + { + return (int) operationsPerLtsDistribution.skew(rng.randomNumber(lts, OPERATIONS_PER_LTS_STREAM)); + } + + public int maxPartitionSize() + { + return maxPartitionSize; + } + + protected int clusteringOffset(long lts) + { + return RngUtils.asInt(lts, 0, maxPartitionSize() - 1); + } + + // TODO: this won't work for entropy-adjusted CDs, at least the way they're implemented now + public boolean isCdVisitedBy(long pd, long lts, long cd) + { + return opId(pd, lts, cd) < operationsPerLts(lts); + } + + public long randomCd(long pd, long entropy) + { + long positionInPartition = Math.abs(rng.prev(entropy)) % maxPartitionSize(); + return rng.prev(positionInPartition, pd); + } + + protected long cd(long pd, long lts, long opId) + { + int partitionSize = maxPartitionSize(); + int clusteringOffset = clusteringOffset(lts); + if (clusteringOffset == 0) + return rng.prev(opId, pd); + + int positionInPartition = (int) ((clusteringOffset + opId) % partitionSize); + return rng.prev(positionInPartition, pd); + } + + public long opId(long pd, long lts, long cd) + { + int partitionSize = maxPartitionSize(); + int clusteringOffset = clusteringOffset(lts); + int positionInPartition = (int) rng.next(cd, pd); + + if (clusteringOffset == 0) + return positionInPartition; + + if (positionInPartition == 0) + return partitionSize - clusteringOffset; + if (positionInPartition == clusteringOffset) + return 0; + else if (positionInPartition < clusteringOffset) + return partitionSize - clusteringOffset + positionInPartition; + else + return positionInPartition - clusteringOffset; + } + + public OperationKind operationType(long pd, long lts, long opId) + { + return operationType(pd, lts, opId, partitionLevelOperationsMask(pd, lts)); + } + + public BitSet partitionLevelOperationsMask(long pd, long lts) + { + int totalOps = operationsPerLts(lts); + if (totalOps > 64) + { + throw new IllegalArgumentException("RngUtils#randomBits currently supports only up to 64 bits of entropy, so we can not " + + "split partition and row level operations for more than 64 operations at the moment."); + } + + long seed = rng.randomNumber(pd, lts); + + int partitionLevelOps = (int) Math.ceil(operationSelector.partitionLevelThreshold * totalOps); + long partitionLevelOpsMask = RngUtils.randomBits(partitionLevelOps, totalOps, seed); + + return BitSet.create(partitionLevelOpsMask, totalOps); + } + + private OperationKind operationType(long pd, long lts, long opId, BitSet partitionLevelOperationsMask) + { + try + { + long descriptor = rng.randomNumber(pd ^ lts ^ opId, BITSET_IDX_STREAM); + return operationSelector.inflate(descriptor, partitionLevelOperationsMask.isSet((int) opId)); + } + catch (Throwable t) + { + throw new RuntimeException(String.format("Can not generate a random number with the following inputs: " + + "pd=%d lts=%d opId=%d partitionLevelOperationsMask=%s", + pd, lts, opId, partitionLevelOperationsMask)); + } + } + + public BitSet columnMask(long pd, long lts, long opId, OperationKind opType) + { + long descriptor = rng.randomNumber(pd ^ lts ^ opId, BITSET_IDX_STREAM); + return columnSelector.columnMask(opType, descriptor); + } + + public long vd(long pd, long cd, long lts, long opId, int col) + { + return rng.randomNumber(opId + 1, pd ^ cd ^ lts ^ col); + } + } + + enum OperationKind + { + UPDATE(false), + INSERT(false), + UPDATE_WITH_STATICS(true), + INSERT_WITH_STATICS(true), + DELETE_PARTITION(true), + DELETE_ROW(false), + DELETE_COLUMN(false), + DELETE_COLUMN_WITH_STATICS(true), + DELETE_RANGE(false), + DELETE_SLICE(false); + + public final boolean partititonLevel; + + OperationKind(boolean partitionLevel) + { + this.partititonLevel = partitionLevel; + } + + /** + * All operations apart from partiton delition, including partition hist are visible since if there is a + * partition liveness marker, partition's static column is going to survive. We use this method to match + * computed LTS with tracked ones. + */ + public boolean hasVisibleVisit() + { + return this != OpSelectors.OperationKind.DELETE_PARTITION; + } + } + + public static class OperationSelector + { + public final Surjections.Surjection partitionLevelOperationSelector; + public final Surjections.Surjection rowLevelOperationSelector; + // TODO: start using partitionLevelThreshold + public final double partitionLevelThreshold; + + public OperationSelector(Surjections.Surjection partitionLevelOperationSelector, + Surjections.Surjection rowLevelOperationSelector, + double partitionLevelThreshold) + { + this.partitionLevelOperationSelector = partitionLevelOperationSelector; + this.rowLevelOperationSelector = rowLevelOperationSelector; + this.partitionLevelThreshold = partitionLevelThreshold; + } + + public OperationKind inflate(long descriptor, boolean partitionLevel) + { + OperationKind operationKind = partitionLevel ? partitionLevelOperationSelector.inflate(descriptor) : rowLevelOperationSelector.inflate(descriptor); + assert operationKind.partititonLevel == partitionLevel : "Generated operation with an incorrect partition level. Check your generators."; + return operationKind; + } + + public static OperationSelector weighted(Map weightsMap) + { + int[] weights = new int[weightsMap.size()]; + OperationKind[] operationKinds = new OperationKind[weightsMap.size()]; + int i = 0; + for (Map.Entry entry : weightsMap.entrySet()) + { + weights[i] = entry.getValue(); + operationKinds[i] = entry.getKey(); + i++; + } + return weighted(Surjections.weights(weights), operationKinds); + } + + public static OperationSelector weighted(long[] weights, OperationKind... operationKinds) + { + assert weights.length == operationKinds.length; + + Map partitionLevel = new EnumMap(OperationKind.class); + Map rowLevel = new EnumMap(OperationKind.class); + + int partitionLevelSum = 0; + int rowLevelSum = 0; + for (int i = 0; i < weights.length; i++) + { + int v = (int) (weights[i] >> 32); + if (operationKinds[i].partititonLevel) + { + partitionLevel.put(operationKinds[i], v); + partitionLevelSum += v; + } + else + { + rowLevel.put(operationKinds[i], v); + rowLevelSum += v; + } + } + int sum = (partitionLevelSum + rowLevelSum); + + return new OperationSelector(Surjections.weighted(normalize(partitionLevel)), + Surjections.weighted(normalize(rowLevel)), + (partitionLevelSum * 1.0) / sum); + } + + public static Map normalize(Map weights) + { + Map normalized = new EnumMap(OperationKind.class); + int sum = 0; + for (Integer value : weights.values()) + sum += value; + + for (OperationKind kind : weights.keySet()) + { + double dbl = (sum * ((double) weights.get(kind)) / sum); + normalized.put(kind, (int) Math.round(dbl)); + } + + return normalized; + } + } +} diff --git a/test/harry/main/org/apache/cassandra/harry/model/QueryingNoOpValidator.java b/test/harry/main/org/apache/cassandra/harry/model/QueryingNoOpValidator.java new file mode 100644 index 0000000000..723b8e7e44 --- /dev/null +++ b/test/harry/main/org/apache/cassandra/harry/model/QueryingNoOpValidator.java @@ -0,0 +1,65 @@ +/* + * 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.model; + +import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.annotation.JsonTypeName; +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.operations.CompiledStatement; +import org.apache.cassandra.harry.operations.Query; + +/** + * A model that can be used to "simply" run random queries. Does not perform validation + * of the results that it sees. Useful for increasing concurrency and triggering + * exceptions rather than data loss issues. + */ +public class QueryingNoOpValidator implements Model +{ + private final Run run; + + public QueryingNoOpValidator(Run run) + { + this.run = run; + } + + @Override + public void validate(Query query) + { + CompiledStatement compiled = query.toSelectStatement(); + run.sut.execute(compiled.cql(), + SystemUnderTest.ConsistencyLevel.QUORUM, + compiled.bindings()); + } + + @JsonTypeName("no_op") + public static class QueryingNoOpCheckerConfig implements Configuration.ModelConfiguration + { + @JsonCreator + public QueryingNoOpCheckerConfig() + { + } + + public Model make(Run run) + { + return new QueryingNoOpValidator(run); + } + } +} diff --git a/test/harry/main/org/apache/cassandra/harry/model/QuiescentChecker.java b/test/harry/main/org/apache/cassandra/harry/model/QuiescentChecker.java new file mode 100644 index 0000000000..9c8a24398d --- /dev/null +++ b/test/harry/main/org/apache/cassandra/harry/model/QuiescentChecker.java @@ -0,0 +1,296 @@ +/* + * 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.model; + +import java.util.*; +import java.util.function.Supplier; + +import org.apache.cassandra.harry.core.Run; +import org.apache.cassandra.harry.data.ResultSetRow; +import org.apache.cassandra.harry.ddl.ColumnSpec; +import org.apache.cassandra.harry.ddl.SchemaSpec; +import org.apache.cassandra.harry.sut.SystemUnderTest; +import org.apache.cassandra.harry.model.reconciler.PartitionState; +import org.apache.cassandra.harry.operations.Query; +import org.apache.cassandra.harry.model.reconciler.Reconciler; +import org.apache.cassandra.harry.tracker.DataTracker; + +import static org.apache.cassandra.harry.gen.DataGenerators.NIL_DESCR; +import static org.apache.cassandra.harry.gen.DataGenerators.UNSET_DESCR; + +public class QuiescentChecker implements Model +{ + protected final OpSelectors.Clock clock; + + protected final DataTracker tracker; + protected final SystemUnderTest sut; + protected final Reconciler reconciler; + protected final SchemaSpec schema; + + public QuiescentChecker(Run run) + { + this(run, new Reconciler(run)); + } + + public QuiescentChecker(Run run, Reconciler reconciler) + { + this(run.clock, run.sut, run.tracker, run.schemaSpec, reconciler); + } + + public QuiescentChecker(OpSelectors.Clock clock, + SystemUnderTest sut, + DataTracker tracker, + SchemaSpec schema, + Reconciler reconciler) + { + this.clock = clock; + this.sut = sut; + this.reconciler = reconciler; + this.tracker = tracker; + this.schema = schema; + } + + public void validate(Query query) + { + tracker.beginValidation(query.pd); + validate(() -> SelectHelper.execute(sut, clock, query), query); + tracker.endValidation(query.pd); + } + + protected void validate(Supplier> rowsSupplier, Query query) + { + List actualRows = rowsSupplier.get(); + PartitionState partitionState = reconciler.inflatePartitionState(query.pd, tracker, query); + validate(schema, tracker, partitionState, actualRows, query); + } + + public static void validate(SchemaSpec schema, DataTracker tracker, PartitionState partitionState, List actualRows, Query query) + { + Set> columns = new HashSet<>(); + columns.addAll(schema.allColumns); + validate(schema, tracker, columns, partitionState, actualRows, query); + } + + public static Reconciler.RowState adjustForSelection(Reconciler.RowState row, SchemaSpec schema, Set> selection, boolean isStatic) + { + if (selection.size() == schema.allColumns.size()) + return row; + + List> columns = isStatic ? schema.staticColumns : schema.regularColumns; + Reconciler.RowState newRowState = row.clone(); + assert newRowState.vds.length == columns.size(); + for (int i = 0; i < columns.size(); i++) + { + if (!selection.contains(columns.get(i))) + { + newRowState.vds[i] = UNSET_DESCR; + newRowState.lts[i] = NO_TIMESTAMP; + } + } + return newRowState; + } + + public static void validate(SchemaSpec schema, DataTracker tracker, Set> selection, PartitionState partitionState, List actualRows, Query query) + { + boolean isWildcardQuery = selection == null; + String trackerBefore = tracker.toString(); + if (isWildcardQuery) + selection = new HashSet<>(schema.allColumns); + + Iterator actual = actualRows.iterator(); + Collection expectedRows = partitionState.rows(query.reverse); + + Iterator expected = expectedRows.iterator(); + + String trackerState = String.format("Tracker before: %s, Tracker after: %s", trackerBefore, tracker); + + // It is possible that we only get a single row in response, and it is equal to static row + if (partitionState.isEmpty() && partitionState.staticRow() != null && actual.hasNext()) + { + ResultSetRow actualRowState = actual.next(); + if (actualRowState.cd != UNSET_DESCR && actualRowState.cd != partitionState.staticRow().cd) + { + throw new ValidationException(trackerState, + partitionState.toString(schema), + toString(actualRows), + "Found a row while model predicts statics only:" + + "\nExpected: %s" + + "\nActual: %s" + + "\nQuery: %s", + partitionState.staticRow(), + actualRowState, + query.toSelectStatement()); + } + + for (int i = 0; i < actualRowState.vds.length; i++) + { + if (actualRowState.vds[i] != NIL_DESCR || actualRowState.lts[i] != NO_TIMESTAMP) + throw new ValidationException(trackerState, + partitionState.toString(schema), + toString(actualRows), + "Found a row while model predicts statics only:" + + "\nActual: %s" + + "\nQuery: %s", + actualRowState, query.toSelectStatement()); + } + + assertStaticRow(partitionState, actualRows, + adjustForSelection(partitionState.staticRow(), schema, selection, true), + actualRowState, query, trackerState, schema, isWildcardQuery); + } + + while (actual.hasNext() && expected.hasNext()) + { + ResultSetRow actualRowState = actual.next(); + Reconciler.RowState originalExpectedRowState = expected.next(); + Reconciler.RowState expectedRowState = adjustForSelection(originalExpectedRowState, schema, selection, false); + + if (schema.trackLts) + partitionState.compareVisitedLts(actualRowState.visited_lts); + + // TODO: this is not necessarily true. It can also be that ordering is incorrect. + if (actualRowState.cd != UNSET_DESCR && actualRowState.cd != expectedRowState.cd) + { + throw new ValidationException(trackerState, + partitionState.toString(schema), + toString(actualRows), + "Found a row in the model that is not present in the resultset:" + + "\nExpected: %s" + + "\nActual: %s" + + "\nQuery: %s", + expectedRowState.toString(schema), + actualRowState, query.toSelectStatement()); + } + + if (!Arrays.equals(actualRowState.vds, expectedRowState.vds)) + throw new ValidationException(trackerState, + partitionState.toString(schema), + toString(actualRows), + "Returned row state doesn't match the one predicted by the model:" + + "\nExpected: %s (%s)" + + "\nActual: %s (%s)." + + "\nQuery: %s", + descriptorsToString(expectedRowState.vds), expectedRowState.toString(schema), + descriptorsToString(actualRowState.vds), actualRowState, + query.toSelectStatement()); + + // Wildcard queries do not include timestamps + if (!isWildcardQuery && !Arrays.equals(actualRowState.lts, expectedRowState.lts)) + throw new ValidationException(trackerState, + partitionState.toString(schema), + toString(actualRows), + "Timestamps in the row state don't match ones predicted by the model:" + + "\nExpected: %s (%s)" + + "\nActual: %s (%s)." + + "\nQuery: %s", + Arrays.toString(expectedRowState.lts), expectedRowState.toString(schema), + Arrays.toString(actualRowState.lts), actualRowState, + query.toSelectStatement()); + + if (partitionState.staticRow() != null || actualRowState.hasStaticColumns()) + { + Reconciler.RowState expectedStaticRowState = adjustForSelection(partitionState.staticRow(), schema, selection, true); + assertStaticRow(partitionState, actualRows, expectedStaticRowState, actualRowState, query, trackerState, schema, isWildcardQuery); + } + } + + if (actual.hasNext() || expected.hasNext()) + { + throw new ValidationException(trackerState, + partitionState.toString(schema), + toString(actualRows), + "Expected results to have the same number of results, but %s result iterator has more results." + + "\nExpected: %s" + + "\nActual: %s" + + "\nQuery: %s", + actual.hasNext() ? "actual" : "expected", + expectedRows, + actualRows, + query.toSelectStatement()); + } + } + + public static void assertStaticRow(PartitionState partitionState, + List actualRows, + Reconciler.RowState staticRow, + ResultSetRow actualRowState, + Query query, + String trackerState, + SchemaSpec schemaSpec, + boolean isWildcardQuery) + { + if (!Arrays.equals(staticRow.vds, actualRowState.sds)) + throw new ValidationException(trackerState, + partitionState.toString(schemaSpec), + toString(actualRows), + "Returned static row state doesn't match the one predicted by the model:" + + "\nExpected: %s (%s)" + + "\nActual: %s (%s)." + + "\nQuery: %s", + descriptorsToString(staticRow.vds), staticRow.toString(schemaSpec), + descriptorsToString(actualRowState.sds), actualRowState, + query.toSelectStatement()); + + if (!isWildcardQuery && !Arrays.equals(staticRow.lts, actualRowState.slts)) + throw new ValidationException(trackerState, + partitionState.toString(schemaSpec), + toString(actualRows), + "Timestamps in the static row state don't match ones predicted by the model:" + + "\nExpected: %s (%s)" + + "\nActual: %s (%s)." + + "\nQuery: %s", + Arrays.toString(staticRow.lts), staticRow.toString(schemaSpec), + Arrays.toString(actualRowState.slts), actualRowState, + query.toSelectStatement()); + } + + public static String descriptorsToString(long[] descriptors) + { + StringBuilder sb = new StringBuilder(); + for (int i = 0; i < descriptors.length; i++) + { + if (descriptors[i] == NIL_DESCR) + sb.append("NIL"); + if (descriptors[i] == UNSET_DESCR) + sb.append("UNSET"); + else + sb.append(descriptors[i]); + if (i > 0) + sb.append(", "); + } + return sb.toString(); + } + public static String toString(Collection collection, SchemaSpec schema) + { + StringBuilder builder = new StringBuilder(); + + for (Reconciler.RowState rowState : collection) + builder.append(rowState.toString(schema)).append("\n"); + return builder.toString(); + } + + public static String toString(List collection) + { + StringBuilder builder = new StringBuilder(); + + for (ResultSetRow rowState : collection) + builder.append(rowState.toString()).append("\n"); + return builder.toString(); + } +} diff --git a/test/harry/main/org/apache/cassandra/harry/model/QuiescentLocalStateCheckerBase.java b/test/harry/main/org/apache/cassandra/harry/model/QuiescentLocalStateCheckerBase.java new file mode 100644 index 0000000000..c47657c057 --- /dev/null +++ b/test/harry/main/org/apache/cassandra/harry/model/QuiescentLocalStateCheckerBase.java @@ -0,0 +1,123 @@ +/* + * 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.model; + +import java.util.ArrayList; +import java.util.List; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import org.apache.cassandra.harry.core.Run; +import org.apache.cassandra.harry.data.ResultSetRow; +import org.apache.cassandra.harry.ddl.SchemaSpec; +import org.apache.cassandra.harry.model.reconciler.Reconciler; +import org.apache.cassandra.harry.sut.SystemUnderTest; +import org.apache.cassandra.harry.sut.TokenPlacementModel; +import org.apache.cassandra.harry.operations.CompiledStatement; +import org.apache.cassandra.harry.operations.Query; +import org.apache.cassandra.harry.tracker.DataTracker; + +import static org.apache.cassandra.harry.model.SelectHelper.resultSetToRow; +import static org.apache.cassandra.harry.sut.TokenPlacementModel.Node; + +public abstract class QuiescentLocalStateCheckerBase extends QuiescentChecker +{ + private static final Logger logger = LoggerFactory.getLogger(QuiescentLocalStateCheckerBase.class); + + public final SystemUnderTest sut; + public final TokenPlacementModel.ReplicationFactor rf; + private final OpSelectors.PdSelector pdSelector; + + public QuiescentLocalStateCheckerBase(Run run, TokenPlacementModel.ReplicationFactor rf) + { + this(run.clock, + run.pdSelector, + run.sut, + run.tracker, + run.schemaSpec, + new Reconciler(run), + rf); + } + + public QuiescentLocalStateCheckerBase(OpSelectors.Clock clock, + OpSelectors.PdSelector pdSelector, + SystemUnderTest sut, + DataTracker tracker, + SchemaSpec schema, + Reconciler reconciler, + TokenPlacementModel.ReplicationFactor rf) + { + super(clock, sut, tracker, schema, reconciler); + this.sut = sut; + this.rf = rf; + this.pdSelector = pdSelector; + } + + @SuppressWarnings("unused") + public void validateAll() + { + TokenPlacementModel.ReplicatedRanges ring = getRing(); + + for (int lts = 0; lts < clock.peek(); lts++) + validate(Query.selectPartition(schema, pdSelector.pd(lts, schema), false), ring); + } + + @Override + public void validate(Query query) + { + TokenPlacementModel.ReplicatedRanges ring = getRing(); + tracker.beginValidation(query.pd); + validate(query, ring); + tracker.endValidation(query.pd); + } + + protected void validate(Query query, TokenPlacementModel.ReplicatedRanges ring) + { + CompiledStatement compiled = query.toSelectStatement(); + List replicas = ring.replicasFor(token(query.pd)); + + logger.trace("Predicted {} as replicas for {}. Ring: {}", replicas, query.pd, ring); + for (Node node : replicas) + { + try + { + validate(() -> { + Object[][] objects = executeNodeLocal(compiled.cql(), node, compiled.bindings()); + + List 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 %s of replica set %s", + node, replicas), + e); + } + } + } + + protected abstract TokenPlacementModel.ReplicatedRanges getRing(); + protected abstract long token(long pd); + protected abstract Object[][] executeNodeLocal(String statement, Node node, Object... bindings); +} diff --git a/test/harry/main/org/apache/cassandra/harry/model/RepairingLocalStateValidator.java b/test/harry/main/org/apache/cassandra/harry/model/RepairingLocalStateValidator.java new file mode 100644 index 0000000000..2f310db93f --- /dev/null +++ b/test/harry/main/org/apache/cassandra/harry/model/RepairingLocalStateValidator.java @@ -0,0 +1,92 @@ +/* + * 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.model; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.annotation.JsonProperty; +import com.fasterxml.jackson.annotation.JsonTypeName; +import org.apache.cassandra.harry.core.Configuration; +import org.apache.cassandra.harry.core.Run; +import org.apache.cassandra.harry.sut.injvm.InJvmSut; +import org.apache.cassandra.harry.visitors.AllPartitionsValidator; +import org.apache.cassandra.harry.visitors.QueryLogger; +import org.apache.cassandra.harry.visitors.Visitor; + +/** + * Validator similar to {@link AllPartitionsValidator}, but performs + * repair before checking node states. + * + * Can be useful for testing repair, bootstrap, and streaming code. + */ +public class RepairingLocalStateValidator extends AllPartitionsValidator +{ + private static final Logger logger = LoggerFactory.getLogger(RepairingLocalStateValidator.class); + + private final InJvmSut inJvmSut; + + public static Configuration.VisitorConfiguration factoryForTests(int concurrency, Model.ModelFactory modelFactory) + { + return (r) -> new RepairingLocalStateValidator(r, concurrency, modelFactory); + } + + public RepairingLocalStateValidator(Run run, int concurrency, Model.ModelFactory modelFactory) + { + this(run, concurrency, modelFactory, QueryLogger.NO_OP); + } + + public RepairingLocalStateValidator(Run run, int concurrency, Model.ModelFactory modelFactory, QueryLogger queryLogger) + { + super(run, concurrency, modelFactory, queryLogger); + this.inJvmSut = (InJvmSut) run.sut; + } + + public void visit() + { + logger.debug("Starting repair..."); + inJvmSut.cluster().stream().forEach((instance) -> instance.nodetool("repair", "--full")); + logger.debug("Validating partitions..."); + super.visit(); + } + + @JsonTypeName("repair_and_validate_local_states") + public static class RepairingLocalStateValidatorConfiguration implements Configuration.VisitorConfiguration + { + private final int concurrency; + private final Configuration.ModelConfiguration modelConfiguration; + private final Configuration.QueryLoggerConfiguration query_logger; + + @JsonCreator + public RepairingLocalStateValidatorConfiguration(@JsonProperty("concurrency") int concurrency, + @JsonProperty("model") Configuration.ModelConfiguration model, + @JsonProperty("query_logger") Configuration.QueryLoggerConfiguration query_logger) + { + this.concurrency = concurrency; + this.modelConfiguration = model; + this.query_logger = QueryLogger.thisOrDefault(query_logger); + } + + public Visitor make(Run run) + { + return new RepairingLocalStateValidator(run, concurrency, modelConfiguration, query_logger.make()); + } + } +} diff --git a/test/harry/main/org/apache/cassandra/harry/model/SelectHelper.java b/test/harry/main/org/apache/cassandra/harry/model/SelectHelper.java new file mode 100644 index 0000000000..36e4499c7d --- /dev/null +++ b/test/harry/main/org/apache/cassandra/harry/model/SelectHelper.java @@ -0,0 +1,337 @@ +/* + * 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.model; + +import java.util.*; + +import org.apache.cassandra.harry.data.ResultSetRow; +import org.apache.cassandra.harry.ddl.ColumnSpec; +import org.apache.cassandra.harry.ddl.SchemaSpec; +import org.apache.cassandra.harry.gen.DataGenerators; +import org.apache.cassandra.harry.sut.SystemUnderTest; +import org.apache.cassandra.harry.operations.CompiledStatement; +import org.apache.cassandra.harry.operations.Relation; +import org.apache.cassandra.harry.operations.Query; + +import static org.apache.cassandra.harry.gen.DataGenerators.UNSET_DESCR; + +public class SelectHelper +{ + private static final long[] EMPTY_ARR = new long[]{}; + public static CompiledStatement selectWildcard(SchemaSpec schema, long pd) + { + return select(schema, pd, null, Collections.emptyList(), false, true); + } + + public static CompiledStatement select(SchemaSpec schema, long pd) + { + return select(schema, pd, schema.allColumnsSet, Collections.emptyList(), false, true); + } + + /** + * Here, {@code reverse} should be understood not in ASC/DESC sense, but rather in terms + * of how we're going to iterate through this partition (in other words, if first clustering component order + * is DESC, we'll iterate in ASC order) + */ + public static CompiledStatement select(SchemaSpec schema, long pd, List relations, boolean reverse, boolean includeWriteTime) + { + return select(schema, pd, schema.allColumnsSet, relations, reverse, includeWriteTime); + } + + public static CompiledStatement selectWildcard(SchemaSpec schema, long pd, List relations, boolean reverse, boolean includeWriteTime) + { + return select(schema, pd, null, relations, reverse, includeWriteTime); + } + + public static CompiledStatement select(SchemaSpec schema, long pd, Set> columns, List relations, boolean reverse, boolean includeWriteTime) + { + boolean isWildcardQuery = columns == null; + if (isWildcardQuery) + { + columns = schema.allColumnsSet; + includeWriteTime = false; + } + + StringBuilder b = new StringBuilder(); + b.append("SELECT "); + + boolean isFirst = true; + if (isWildcardQuery) + { + b.append("*"); + } + else + { + for (int i = 0; i < schema.allColumns.size(); i++) + { + ColumnSpec spec = schema.allColumns.get(i); + if (columns != null && !columns.contains(spec)) + continue; + + if (isFirst) + isFirst = false; + else + b.append(", "); + b.append(spec.name); + } + } + + if (includeWriteTime) + { + for (ColumnSpec spec : schema.staticColumns) + { + if (columns != null && !columns.contains(spec)) + continue; + b.append(", ") + .append("writetime(") + .append(spec.name) + .append(")"); + } + + for (ColumnSpec spec : schema.regularColumns) + { + if (columns != null && !columns.contains(spec)) + continue; + b.append(", ") + .append("writetime(") + .append(spec.name) + .append(")"); + } + } + + if (schema.trackLts) + b.append(", visited_lts"); + + b.append(" FROM ") + .append(schema.keyspace) + .append(".") + .append(schema.table) + .append(" WHERE "); + + List bindings = new ArrayList<>(); + + schema.inflateRelations(pd, + relations, + new SchemaSpec.AddRelationCallback() + { + boolean isFirst = true; + public void accept(ColumnSpec spec, Relation.RelationKind kind, Object value) + { + if (isFirst) + isFirst = false; + else + b.append(" AND "); + b.append(kind.getClause(spec)); + bindings.add(value); + } + }); + addOrderBy(schema, b, reverse); + b.append(";"); + Object[] bindingsArr = bindings.toArray(new Object[bindings.size()]); + return new CompiledStatement(b.toString(), bindingsArr); + } + + public static CompiledStatement count(SchemaSpec schema, long pd) + { + StringBuilder b = new StringBuilder(); + b.append("SELECT count(*) "); + + b.append(" FROM ") + .append(schema.keyspace) + .append(".") + .append(schema.table) + .append(" WHERE "); + + List bindings = new ArrayList<>(schema.partitionKeys.size()); + + schema.inflateRelations(pd, + Collections.emptyList(), + new SchemaSpec.AddRelationCallback() + { + boolean isFirst = true; + public void accept(ColumnSpec spec, Relation.RelationKind kind, Object value) + { + if (isFirst) + isFirst = false; + else + b.append(" AND "); + b.append(kind.getClause(spec)); + bindings.add(value); + } + }); + + Object[] bindingsArr = bindings.toArray(new Object[bindings.size()]); + return new CompiledStatement(b.toString(), bindingsArr); + } + + private static void addOrderBy(SchemaSpec schema, StringBuilder b, boolean reverse) + { + if (reverse && schema.clusteringKeys.size() > 0) + { + b.append(" ORDER BY "); + for (int i = 0; i < schema.clusteringKeys.size(); i++) + { + ColumnSpec c = schema.clusteringKeys.get(i); + if (i > 0) + b.append(", "); + b.append(c.isReversed() ? asc(c.name) : desc(c.name)); + } + } + } + + public static String asc(String name) + { + return name + " ASC"; + } + + public static String desc(String name) + { + return name + " DESC"; + } + + + public static Object[] broadenResult(SchemaSpec schemaSpec, Set> columns, Object[] result) + { + boolean isWildcardQuery = columns == null; + + if (isWildcardQuery) + columns = schemaSpec.allColumnsSet; + else if (schemaSpec.allColumns.size() == columns.size()) + return result; + + Object[] newRes = new Object[schemaSpec.allColumns.size() + schemaSpec.staticColumns.size() + schemaSpec.regularColumns.size()]; + + int origPointer = 0; + int newPointer = 0; + for (int i = 0; i < schemaSpec.allColumns.size(); i++) + { + ColumnSpec column = schemaSpec.allColumns.get(i); + if (columns.contains(column)) + newRes[newPointer] = result[origPointer++]; + else + newRes[newPointer] = DataGenerators.UNSET_VALUE; + newPointer++; + } + + // Make sure to include writetime, but only in case query actually includes writetime (for example, it's not a wildcard query) + for (int i = 0; i < schemaSpec.staticColumns.size() && origPointer < result.length; i++) + { + ColumnSpec column = schemaSpec.staticColumns.get(i); + if (columns.contains(column)) + newRes[newPointer] = result[origPointer++]; + else + newRes[newPointer] = null; + newPointer++; + } + + for (int i = 0; i < schemaSpec.regularColumns.size() && origPointer < result.length; i++) + { + ColumnSpec column = schemaSpec.regularColumns.get(i); + if (columns.contains(column)) + newRes[newPointer] = result[origPointer++]; + else + newRes[newPointer] = null; + newPointer++; + } + + return newRes; + } + + static boolean isDeflatable(Object[] columns) + { + for (Object column : columns) + { + if (column == DataGenerators.UNSET_VALUE) + return false; + } + return true; + } + + public static ResultSetRow resultSetToRow(SchemaSpec schema, OpSelectors.Clock clock, Object[] result) + { + Object[] partitionKey = new Object[schema.partitionKeys.size()]; + Object[] clusteringKey = new Object[schema.clusteringKeys.size()]; + Object[] staticColumns = new Object[schema.staticColumns.size()]; + Object[] regularColumns = new Object[schema.regularColumns.size()]; + + System.arraycopy(result, 0, partitionKey, 0, partitionKey.length); + System.arraycopy(result, partitionKey.length, clusteringKey, 0, clusteringKey.length); + System.arraycopy(result, partitionKey.length + clusteringKey.length, staticColumns, 0, staticColumns.length); + System.arraycopy(result, partitionKey.length + clusteringKey.length + staticColumns.length, regularColumns, 0, regularColumns.length); + + + List visited_lts_list; + if (schema.trackLts) + { + visited_lts_list = (List) result[result.length - 1]; + visited_lts_list.sort(Long::compare); + } + else + { + visited_lts_list = Collections.emptyList(); + } + + long[] slts = new long[schema.staticColumns.size()]; + for (int i = 0; i < slts.length; i++) + { + Object v = result[schema.allColumns.size() + i]; + slts[i] = v == null ? Model.NO_TIMESTAMP : clock.lts((long) v); + } + + long[] lts = new long[schema.regularColumns.size()]; + for (int i = 0; i < lts.length; i++) + { + Object v = result[schema.allColumns.size() + slts.length + i]; + lts[i] = v == null ? Model.NO_TIMESTAMP : clock.lts((long) v); + } + + return new ResultSetRow(isDeflatable(partitionKey) ? schema.deflatePartitionKey(partitionKey) : UNSET_DESCR, + isDeflatable(clusteringKey) ? schema.deflateClusteringKey(clusteringKey) : UNSET_DESCR, + schema.staticColumns.isEmpty() ? EMPTY_ARR : schema.deflateStaticColumns(staticColumns), + schema.staticColumns.isEmpty() ? EMPTY_ARR : slts, + schema.deflateRegularColumns(regularColumns), + lts, + visited_lts_list); + } + + public static List execute(SystemUnderTest sut, OpSelectors.Clock clock, Query query) + { + return execute(sut, clock, query, query.schemaSpec.allColumnsSet); + } + + public static List execute(SystemUnderTest sut, OpSelectors.Clock clock, Query query, Set> columns) + { + CompiledStatement compiled = query.toSelectStatement(columns, true); + Object[][] objects = sut.executeIdempotent(compiled.cql(), SystemUnderTest.ConsistencyLevel.QUORUM, compiled.bindings()); + List result = new ArrayList<>(); + for (Object[] obj : objects) + result.add(resultSetToRow(query.schemaSpec, clock, broadenResult(query.schemaSpec, columns, obj))); + return result; + } + + public static List execute(SystemUnderTest sut, OpSelectors.Clock clock, CompiledStatement compiled, SchemaSpec schemaSpec) + { + Set> columns = schemaSpec.allColumnsSet; + Object[][] objects = sut.executeIdempotent(compiled.cql(), SystemUnderTest.ConsistencyLevel.QUORUM, compiled.bindings()); + List result = new ArrayList<>(); + for (Object[] obj : objects) + result.add(resultSetToRow(schemaSpec, clock, broadenResult(schemaSpec, columns, obj))); + return result; + } +} diff --git a/test/harry/main/org/apache/cassandra/harry/model/reconciler/PartitionState.java b/test/harry/main/org/apache/cassandra/harry/model/reconciler/PartitionState.java new file mode 100644 index 0000000000..9d789cdd8c --- /dev/null +++ b/test/harry/main/org/apache/cassandra/harry/model/reconciler/PartitionState.java @@ -0,0 +1,311 @@ +/* + * 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.model.reconciler; + +import java.util.*; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import org.apache.cassandra.harry.ddl.ColumnSpec; +import org.apache.cassandra.harry.ddl.SchemaSpec; +import org.apache.cassandra.harry.operations.FilteringQuery; +import org.apache.cassandra.harry.operations.Query; +import org.apache.cassandra.harry.util.BitSet; +import org.apache.cassandra.harry.util.DescriptorRanges; +import org.apache.cassandra.harry.gen.DataGenerators; +import org.apache.cassandra.harry.model.Model; + +public class PartitionState implements Iterable +{ + private static final Logger logger = LoggerFactory.getLogger(Reconciler.class); + + public final long pd; + final long debugCd; + final SchemaSpec schema; + final List visitedLts = new ArrayList<>(); + final List skippedLts = new ArrayList<>(); + + // Collected state + Reconciler.RowState staticRow; + + final NavigableMap rows; + + public PartitionState(long pd, long debugCd, SchemaSpec schema) + { + this.pd = pd; + this.rows = new TreeMap<>(); + this.staticRow = new Reconciler.RowState(this, + Reconciler.STATIC_CLUSTERING, + Reconciler.arr(schema.staticColumns.size(), DataGenerators.NIL_DESCR), + Reconciler.arr(schema.staticColumns.size(), Model.NO_TIMESTAMP)); + this.debugCd = debugCd; + this.schema = schema; + } + + private PartitionState(long pd, long debugCd, Reconciler.RowState staticRow, NavigableMap rows, SchemaSpec schema) + { + this.pd = pd; + this.rows = rows; + this.staticRow = new Reconciler.RowState(this, Reconciler.STATIC_CLUSTERING, + Arrays.copyOf(staticRow.vds, staticRow.vds.length), + Arrays.copyOf(staticRow.lts, staticRow.lts.length)); + this.debugCd = debugCd; + this.schema = schema; + } + + public void compareVisitedLts(List actualVisitedLts) + { + long min = actualVisitedLts.get(0); + int predictedIdx = Collections.binarySearch(visitedLts, min); + List predictedSublist = visitedLts.subList(predictedIdx, visitedLts.size()); + Set set = new HashSet<>(actualVisitedLts); + for (long lts : predictedSublist) + { + if (!set.contains(lts)) + throw new IllegalStateException(String.format("Predicted visit to %d, but did not see it in the debug row\n" + + "Actual: %s\n" + + "Predicted: %s", + lts, actualVisitedLts, predictedSublist)); + } + } + + public NavigableMap rows() + { + return rows; + } + + public PartitionState filter(FilteringQuery query) + { + NavigableMap rows = new TreeMap<>(); + for (Long cd : this.rows.keySet()) + { + Reconciler.RowState rowState = this.rows.get(cd); + if (query.match(rowState)) + rows.put(cd, rowState); + } + PartitionState ps = new PartitionState(pd, debugCd, staticRow, rows, schema); + System.out.println("ps.rows.size() = " + ps.rows.size()); + return ps; + } + + public void writeStaticRow(long[] sds, long lts) + { + staticRow = updateRowState(staticRow, schema.staticColumns, Reconciler.STATIC_CLUSTERING, sds, lts, false); + } + + public void write(long cd, long[] vds, long lts, boolean writePrimaryKeyLiveness) + { + rows.compute(cd, (cd_, current) -> updateRowState(current, schema.regularColumns, cd, vds, lts, writePrimaryKeyLiveness)); + } + + public void delete(DescriptorRanges.DescriptorRange range, long lts) + { + if (range.minBound > range.maxBound) + return; + + Iterator> iter = rows.subMap(range.minBound, range.minInclusive, + range.maxBound, range.maxInclusive) + .entrySet() + .iterator(); + while (iter.hasNext()) + { + Map.Entry e = iter.next(); + if (debugCd != -1 && e.getKey() == debugCd) + logger.info("Hiding {} at {} because of range tombstone {}", debugCd, lts, range); + + // assert row state doesn't have fresher lts + iter.remove(); + } + } + + public void delete(long cd, long lts) + { + Reconciler.RowState state = rows.remove(cd); + if (state != null) + { + for (long v : state.lts) + assert lts >= v : String.format("Attempted to remove a row with a tombstone that has older timestamp (%d): %s", lts, state); + } + } + + public boolean isEmpty() + { + return rows.isEmpty(); + } + + /** + * Method used to update row state of both static and regular rows. + */ + private Reconciler.RowState updateRowState(Reconciler.RowState currentState, List> columns, long cd, long[] vds, long lts, boolean writePrimaryKeyLiveness) + { + if (currentState == null) + { + long[] ltss = new long[vds.length]; + long[] vdsCopy = new long[vds.length]; + for (int i = 0; i < vds.length; i++) + { + if (vds[i] != DataGenerators.UNSET_DESCR) + { + ltss[i] = lts; + vdsCopy[i] = vds[i]; + } + else + { + ltss[i] = Model.NO_TIMESTAMP; + vdsCopy[i] = DataGenerators.NIL_DESCR; + } + } + + currentState = new Reconciler.RowState(this, cd, vdsCopy, ltss); + } + else + { + assert currentState.vds.length == vds.length : String.format("Vds: %d, sds: %d", currentState.vds.length, vds.length); + for (int i = 0; i < vds.length; i++) + { + if (vds[i] == DataGenerators.UNSET_DESCR) + continue; + + assert lts >= currentState.lts[i] : String.format("Out-of-order LTS: %d. Max seen: %s", lts, currentState.lts[i]); // sanity check; we're iterating in lts order + + if (currentState.lts[i] == lts) + { + // Timestamp collision case + ColumnSpec column = columns.get(i); + if (column.type.compareLexicographically(vds[i], currentState.vds[i]) > 0) + currentState.vds[i] = vds[i]; + } + else + { + currentState.vds[i] = vds[i]; + assert lts > currentState.lts[i]; + currentState.lts[i] = lts; + } + } + } + + if (writePrimaryKeyLiveness) + currentState.hasPrimaryKeyLivenessInfo = true; + + return currentState; + } + + public void deleteRegularColumns(long lts, long cd, int columnOffset, org.apache.cassandra.harry.util.BitSet columns, org.apache.cassandra.harry.util.BitSet mask) + { + deleteColumns(lts, rows.get(cd), columnOffset, columns, mask); + } + + public void deleteStaticColumns(long lts, int columnOffset, org.apache.cassandra.harry.util.BitSet columns, org.apache.cassandra.harry.util.BitSet mask) + { + deleteColumns(lts, staticRow, columnOffset, columns, mask); + } + + public void deleteColumns(long lts, Reconciler.RowState state, int columnOffset, org.apache.cassandra.harry.util.BitSet columns, BitSet mask) + { + if (state == null) + return; + + //TODO: optimise by iterating over the columns that were removed by this deletion + //TODO: optimise final decision to fully remove the column by counting a number of set/unset columns + boolean allNil = true; + for (int i = 0; i < state.vds.length; i++) + { + if (columns.isSet(columnOffset + i, mask)) + { + state.vds[i] = DataGenerators.NIL_DESCR; + state.lts[i] = Model.NO_TIMESTAMP; + } + else if (state.vds[i] != DataGenerators.NIL_DESCR) + { + allNil = false; + } + } + + if (state.cd != Reconciler.STATIC_CLUSTERING && allNil & !state.hasPrimaryKeyLivenessInfo) + delete(state.cd, lts); + } + + public void deletePartition(long lts) + { + if (debugCd != -1) + logger.info("Hiding {} at {} because partition deletion", debugCd, lts); + + rows.clear(); + if (!schema.staticColumns.isEmpty()) + { + Arrays.fill(staticRow.vds, DataGenerators.NIL_DESCR); + Arrays.fill(staticRow.lts, Model.NO_TIMESTAMP); + } + } + + public Iterator iterator() + { + return iterator(false); + } + + public Iterator iterator(boolean reverse) + { + if (reverse) + return rows.descendingMap().values().iterator(); + + return rows.values().iterator(); + } + + public Collection rows(boolean reverse) + { + if (reverse) + return rows.descendingMap().values(); + + return rows.values(); + } + + public Reconciler.RowState staticRow() + { + return staticRow; + } + + public PartitionState apply(Query query) + { + PartitionState partitionState = new PartitionState(pd, debugCd, schema); + partitionState.staticRow = staticRow; + // TODO: we could improve this if we could get original descriptors + for (Reconciler.RowState rowState : rows.values()) + if (query.matchCd(rowState.cd)) + partitionState.rows.put(rowState.cd, rowState); + + return partitionState; + } + + public String toString(SchemaSpec schema) + { + StringBuilder sb = new StringBuilder(); + + sb.append("Visited LTS: " + visitedLts).append("\n"); + sb.append("Skipped LTS: " + skippedLts).append("\n"); + + if (staticRow != null) + sb.append("Static row: " + staticRow.toString(schema)).append("\n"); + + for (Reconciler.RowState row : rows.values()) + sb.append(row.toString(schema)).append("\n"); + + return sb.toString(); + } +} diff --git a/test/harry/main/org/apache/cassandra/harry/model/reconciler/Reconciler.java b/test/harry/main/org/apache/cassandra/harry/model/reconciler/Reconciler.java new file mode 100644 index 0000000000..fb0f690490 --- /dev/null +++ b/test/harry/main/org/apache/cassandra/harry/model/reconciler/Reconciler.java @@ -0,0 +1,331 @@ +/* + * 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.model.reconciler; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; +import java.util.function.Function; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import org.apache.cassandra.harry.core.Run; +import org.apache.cassandra.harry.ddl.SchemaSpec; +import org.apache.cassandra.harry.model.OpSelectors; +import org.apache.cassandra.harry.operations.Query; +import org.apache.cassandra.harry.tracker.DataTracker; +import org.apache.cassandra.harry.util.DescriptorRanges; +import org.apache.cassandra.harry.util.StringUtils; +import org.apache.cassandra.harry.visitors.GeneratingVisitor; +import org.apache.cassandra.harry.visitors.LtsVisitor; +import org.apache.cassandra.harry.visitors.VisitExecutor; +import org.apache.cassandra.harry.gen.DataGenerators; + +/** + * A simple Cassandra-style reconciler for operations against model state. + *

+ * It is useful both as a testing/debugging tool (to avoid starting Cassandra + * cluster to get a result set), and as a quiescent model checker. + * + * TODO: it might be useful to actually record deletions instead of just removing values as we do right now. + */ +public class Reconciler +{ + private static final Logger logger = LoggerFactory.getLogger(Reconciler.class); + + public static long STATIC_CLUSTERING = DataGenerators.NIL_DESCR; + + private final OpSelectors.PdSelector pdSelector; + private final SchemaSpec schema; + + private final Function visitorFactory; + + public Reconciler(Run run) + { + this(run, + (processor) -> new GeneratingVisitor(run, processor)); + } + + public Reconciler(Run run, Function ltsVisitorFactory) + { + this(run.pdSelector, run.schemaSpec, ltsVisitorFactory); + } + + public Reconciler(OpSelectors.PdSelector pdSelector, SchemaSpec schema, Function ltsVisitorFactory) + { + this.pdSelector = pdSelector; + this.schema = schema; + this.visitorFactory = ltsVisitorFactory; + } + + private final long debugCd = -1L; + + public PartitionState inflatePartitionState(final long pd, DataTracker tracker, Query query) + { + PartitionState partitionState = new PartitionState(pd, debugCd, schema); + + class Processor extends VisitExecutor + { + // Whether a partition deletion was encountered on this LTS. + private boolean hadPartitionDeletion = false; + private boolean hadTrackingRowWrite = false; + private final List rangeDeletes = new ArrayList<>(); + private final List writes = new ArrayList<>(); + private final List columnDeletes = new ArrayList<>(); + + @Override + protected void operation(Operation operation) + { + if (hadPartitionDeletion) + return; + + long lts = operation.lts(); + assert pdSelector.pd(operation.lts(), schema) == operation.pd() : String.format("Computed partition descriptor (%d) does for the lts %d. Does not match actual descriptor %d", + pdSelector.pd(operation.lts(), schema), + operation.lts(), + operation.pd()); + + if (operation.kind().hasVisibleVisit()) + partitionState.visitedLts.add(operation.lts()); + + if (schema.trackLts) + hadTrackingRowWrite = true; + + switch (operation.kind()) + { + case DELETE_RANGE: + case DELETE_SLICE: + DescriptorRanges.DescriptorRange range = ((DeleteOp) operation).relations().toRange(lts); + rangeDeletes.add(range); + partitionState.delete(range, lts); + break; + case DELETE_ROW: + long cd = ((DeleteRowOp) operation).cd(); + range = new DescriptorRanges.DescriptorRange(cd, cd, true, true, lts); + rangeDeletes.add(range); + partitionState.delete(cd, lts); + break; + case DELETE_PARTITION: + partitionState.deletePartition(lts); + rangeDeletes.clear(); + writes.clear(); + columnDeletes.clear(); + hadPartitionDeletion = true; + break; + case INSERT_WITH_STATICS: + case INSERT: + case UPDATE: + case UPDATE_WITH_STATICS: + writes.add(operation); + break; + case DELETE_COLUMN_WITH_STATICS: + case DELETE_COLUMN: + columnDeletes.add(operation); + break; + default: + throw new IllegalStateException(); + } + } + + @Override + protected void beforeLts(long lts, long pd) + { + rangeDeletes.clear(); + writes.clear(); + columnDeletes.clear(); + hadPartitionDeletion = false; + } + + @Override + protected void afterLts(long lts, long pd) + { + if (hadPartitionDeletion) + return; + + outer: for (Operation op : writes) + { + WriteOp writeOp = (WriteOp) op; + long opId = op.opId(); + long cd = writeOp.cd(); + + if (hadTrackingRowWrite) + { + long[] statics = new long[schema.staticColumns.size()]; + Arrays.fill(statics, DataGenerators.UNSET_DESCR); + partitionState.writeStaticRow(statics, lts); + } + + switch (op.kind()) + { + case INSERT_WITH_STATICS: + case UPDATE_WITH_STATICS: + WriteStaticOp writeStaticOp = (WriteStaticOp) op; + // We could apply static columns during the first iteration, but it's more convenient + // to reconcile static-level deletions. + partitionState.writeStaticRow(writeStaticOp.sds(), lts); + case INSERT: + case UPDATE: + if (!query.matchCd(cd)) + { + if (debugCd != -1 && cd == debugCd) + logger.info("Hiding {} at {}/{} because there was no query match", debugCd, lts, opId); + continue outer; + } + + for (DescriptorRanges.DescriptorRange range : rangeDeletes) + { + if (range.timestamp >= lts && range.contains(cd)) + { + if (debugCd != -1 && cd == debugCd) + logger.info("Hiding {} at {}/{} because of range tombstone {}", debugCd, lts, opId, range); + continue outer; + } + } + + partitionState.write(cd, + writeOp.vds(), + lts, + op.kind() == OpSelectors.OperationKind.INSERT || op.kind() == OpSelectors.OperationKind.INSERT_WITH_STATICS); + break; + default: + throw new IllegalStateException(op.kind().toString()); + } + } + + outer: for (Operation op : columnDeletes) + { + DeleteColumnsOp deleteColumnsOp = (DeleteColumnsOp) op; + long opId = op.opId(); + long cd = deleteColumnsOp.cd(); + + switch (op.kind()) + { + case DELETE_COLUMN_WITH_STATICS: + partitionState.deleteStaticColumns(lts, + schema.staticColumnsOffset, + deleteColumnsOp.columns(), // descriptorSelector.columnMask(pd, lts, opId, op.opKind()) + schema.staticColumnsMask()); + case DELETE_COLUMN: + if (!query.matchCd(cd)) + { + if (debugCd != -1 && cd == debugCd) + logger.info("Hiding {} at {}/{} because there was no query match", debugCd, lts, opId); + continue outer; + } + + for (DescriptorRanges.DescriptorRange range : rangeDeletes) + { + if (range.timestamp >= lts && range.contains(cd)) + { + if (debugCd != -1 && cd == debugCd) + logger.info("Hiding {} at {}/{} because of range tombstone {}", debugCd, lts, opId, range); + continue outer; + } + } + + partitionState.deleteRegularColumns(lts, + cd, + schema.regularColumnsOffset, + deleteColumnsOp.columns(), + schema.regularColumnsMask()); + break; + } + } + } + + @Override + public void shutdown() throws InterruptedException {} + } + + LtsVisitor visitor = visitorFactory.apply(new Processor()); + + long currentLts = pdSelector.minLtsFor(pd); + long maxStarted = tracker.maxStarted(); + while (currentLts <= maxStarted && currentLts >= 0) + { + if (tracker.isFinished(currentLts)) + { + visitor.visit(currentLts); + } + else + { + partitionState.skippedLts.add(currentLts); + } + + currentLts = pdSelector.nextLts(currentLts); + } + + return partitionState; + } + + public static long[] arr(int length, long fill) + { + long[] arr = new long[length]; + Arrays.fill(arr, fill); + return arr; + } + + public static class RowState + { + public boolean hasPrimaryKeyLivenessInfo = false; + + public final PartitionState partitionState; + public final long cd; + public final long[] vds; + public final long[] lts; + + public RowState(PartitionState partitionState, + long cd, + long[] vds, + long[] lts) + { + this.partitionState = partitionState; + this.cd = cd; + this.vds = vds; + this.lts = lts; + } + + public RowState clone() + { + RowState rowState = new RowState(partitionState, cd, Arrays.copyOf(vds, vds.length), Arrays.copyOf(lts, lts.length)); + rowState.hasPrimaryKeyLivenessInfo = hasPrimaryKeyLivenessInfo; + return rowState; + } + + public String toString() + { + return toString(null); + } + + public String toString(SchemaSpec schema) + { + return " rowStateRow(" + + partitionState.pd + + "L, " + cd + + (partitionState.staticRow == null ? "" : ", values(" + StringUtils.toString(partitionState.staticRow.vds) + ")") + + (partitionState.staticRow == null ? "" : ", lts(" + StringUtils.toString(partitionState.staticRow.lts) + ")") + + ", values(" + StringUtils.toString(vds) + ")" + + ", lts(" + StringUtils.toString(lts) + ")" + + (schema == null ? "" : ", clustering=" + (cd == STATIC_CLUSTERING ? "static" : Arrays.toString(schema.inflateClusteringKey(cd)))) + + (schema == null ? "" : ", values=" + Arrays.toString(cd == STATIC_CLUSTERING ? schema.inflateStaticColumns(vds) : schema.inflateRegularColumns(vds))) + + ")"; + } + } +} \ No newline at end of file diff --git a/test/harry/main/org/apache/cassandra/harry/model/ring/PlacementSimulator.java b/test/harry/main/org/apache/cassandra/harry/model/ring/PlacementSimulator.java new file mode 100644 index 0000000000..f6b7513212 --- /dev/null +++ b/test/harry/main/org/apache/cassandra/harry/model/ring/PlacementSimulator.java @@ -0,0 +1,1052 @@ +///* +// * 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.model.ring; +// +//import java.io.BufferedWriter; +//import java.io.File; +//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.function.Function; +//import java.util.function.Predicate; +//import java.util.stream.Collectors; +// +//import org.apache.cassandra.distributed.api.TokenSupplier; +//import org.apache.cassandra.harry.sut.TokenPlacementModel; +// +//import static org.apache.cassandra.harry.sut.TokenPlacementModel.*; +// +///** +// * A small class that helps to avoid doing mental arithmetics on ranges. +// */ +//public class PlacementSimulator +//{ +// @SuppressWarnings("unused") // for debugging convenience +// public static List readableTokens(int number) +// { +// final List longs; +// longs = new ArrayList<>(); +// for (int i = 0; i < number; i++) +// { +// longs.add((i + 1) * 100L); +// } +// Collections.shuffle(longs, new Random(1)); +// +// return longs; +// } +// +// public static DebugLog debug = new DebugLog(); +// +// public static class SimulatedPlacements +// { +// public final ReplicationFactor rf; +// public final List nodes; +// public final NavigableMap> readPlacements; +// public final NavigableMap> writePlacements; +// // Stashed states are steps required to finish the operation. For example, in case of +// // bootstrap, this could be adding replicas to write (and then read) sets after splitting ranges. +// public final List stashedStates; +// +// public SimulatedPlacements(ReplicationFactor rf, +// List nodes, +// NavigableMap> readPlacements, +// NavigableMap> writePlacements, +// List stashedStates) +// { +// this.rf = rf; +// this.nodes = nodes; +// this.readPlacements = readPlacements; +// this.writePlacements = writePlacements; +// this.stashedStates = stashedStates; +// } +// +// public SimulatedPlacements withNodes(List newNodes) +// { +// return new SimulatedPlacements(rf, newNodes, readPlacements, writePlacements, stashedStates); +// } +// +// public SimulatedPlacements withReadPlacements(NavigableMap> newReadPlacements) +// { +// return new SimulatedPlacements(rf, nodes, newReadPlacements, writePlacements, stashedStates); +// } +// +// public SimulatedPlacements withWritePlacements(NavigableMap> newWritePlacements) +// { +// return new SimulatedPlacements(rf, nodes, readPlacements, newWritePlacements, stashedStates); +// } +// +// public SimulatedPlacements withStashed(Transformations steps) +// { +// List newStashed = new ArrayList<>(); +// newStashed.addAll(stashedStates); +// newStashed.add(steps); +// return new SimulatedPlacements(rf, nodes, readPlacements, writePlacements, newStashed); +// } +// +// private SimulatedPlacements withoutStashed(Transformations finished) +// { +// List newStates = new ArrayList<>(); +// for (Transformations s : stashedStates) +// if (s != finished) +// newStates.add(s); +// return new SimulatedPlacements(rf, nodes, readPlacements, writePlacements, newStates); +// } +// +// public boolean isWriteTargetFor(long token, Predicate predicate) +// { +// return writePlacementsFor(token).stream().anyMatch(predicate); +// } +// +// public boolean isReadReplicaFor(long token, Predicate predicate) +// { +// return readReplicasFor(token).stream().anyMatch(predicate); +// } +// +// public boolean isReadReplicaFor(long minToken, long maxToken, Predicate predicate) +// { +// return readReplicasFor(minToken, maxToken).stream().anyMatch(predicate); +// } +// +// public List writePlacementsFor(long token) +// { +// for (Map.Entry> e : writePlacements.entrySet()) +// { +// if (e.getKey().contains(token)) +// return e.getValue(); +// } +// +// throw new AssertionError(); +// } +// +// public List readReplicasFor(long minToken, long maxToken) +// { +// for (Map.Entry> e : readPlacements.entrySet()) +// { +// if (e.getKey().contains(minToken, maxToken)) +// return e.getValue(); +// } +// +// throw new AssertionError(); +// } +// +// +// public List readReplicasFor(long token) +// { +// for (Map.Entry> e : readPlacements.entrySet()) +// { +// if (e.getKey().contains(token)) +// return e.getValue(); +// } +// +// throw new AssertionError(); +// } +// +// public String toString() +// { +// return "ModelState{" + +// "\nrf=" + rf + +// "\nnodes=" + nodes + +// "\nreadPlacements=\n" + placementsToString(readPlacements) + +// "\nwritePlacements=\n" + placementsToString(writePlacements) + +// '}'; +// } +// } +// +// public interface SimulatedPlacementHolder +// { +// SimulatedPlacements get(); +// +// /** +// * Applies _one_ of the transformations given to the current state, returning the resulting state. +// * _Does_ set the state within the holder as well. +// */ +// SimulatedPlacements applyNext(Transformations fn); +// SimulatedPlacementHolder set(SimulatedPlacements placements); +// SimulatedPlacementHolder fork(); +// } +// +// public static class RefSimulatedPlacementHolder implements SimulatedPlacementHolder +// { +// private SimulatedPlacements state; +// +// public RefSimulatedPlacementHolder(SimulatedPlacements state) +// { +// this.state = state; +// } +// +// public SimulatedPlacements get() +// { +// return state; +// } +// +// public SimulatedPlacements applyNext(Transformations fn) +// { +// return state = fn.advance(state); +// } +// +// public SimulatedPlacementHolder set(SimulatedPlacements newState) +// { +// state = newState; +// return this; +// } +// +// public SimulatedPlacementHolder fork() +// { +// return new RefSimulatedPlacementHolder(state); +// } +// } +// +// public static class Transformation +// { +// private Function apply; +// private Function revert; +// +// Transformation(Function apply, +// Function revert) +// { +// this.apply = apply; +// this.revert = revert; +// } +// +// public Transformation prepare(Function apply, +// Function revert) +// { +// this.apply = apply; +// this.revert = revert; +// return this; +// } +// } +// +// public static class Transformations +// { +// private final List steps = new ArrayList<>(); +// private int idx = 0; +// +// public void add(Transformation step) +// { +// steps.add(step); +// } +// +// public boolean hasNext() +// { +// return idx < steps.size(); +// } +// +// public SimulatedPlacements advance(SimulatedPlacements prev) +// { +// if (idx >= steps.size()) +// throw new IllegalStateException("Cannot advance transformations, no more steps remaining"); +// +// SimulatedPlacements next = steps.get(idx++).apply.apply(prev); +// if (!hasNext()) +// next = next.withoutStashed(this); +// +// return next; +// } +// +// public boolean hasPrevious() +// { +// return idx > 0; +// } +// +// public SimulatedPlacements revertPublishedEffects(SimulatedPlacements state) +// { +// while (hasPrevious()) +// state = steps.get(--idx).revert.apply(state); +// +// return state.withoutStashed(this); +// } +// } +// +// public static SimulatedPlacements joinFully(SimulatedPlacements baseState, Node node) +// { +// Transformations transformations = join(baseState, node); +// baseState = baseState.withStashed(transformations); +// +// while (transformations.hasNext()) +// baseState = transformations.advance(baseState); +// +// return baseState; +// } +// +// /** +// * Diff-based bootstrap (very close implementation-wise to what production code does) +// */ +// public static Transformations join(SimulatedPlacements baseState, Node bootstrappingNode) +// { +// long token = bootstrappingNode.token(); +// List splitNodes = split(baseState.nodes, token); +// Map> maximalStateWithPlacement = baseState.rf.replicate(move(splitNodes, token, bootstrappingNode)).placementsForRange; +// +// NavigableMap> splitReadPlacements = baseState.rf.replicate(splitNodes).placementsForRange; +// NavigableMap> splitWritePlacements = baseState.rf.replicate(splitNodes).placementsForRange; +// +// Map> allWriteCommands = diff(splitWritePlacements, maximalStateWithPlacement); +// Map> step1WriteCommands = map(allWriteCommands, Diff::onlyAdditions); +// Map> step3WriteCommands = map(allWriteCommands, Diff::onlyRemovals); +// Map> readCommands = diff(splitReadPlacements, maximalStateWithPlacement); +// +// Transformations steps = new Transformations(); +// +// steps.add(new Transformation( +// (model) -> { // apply +// // add the new node to the system and split ranges according to its token, while retaining current +// // placement. This step will always be executed immediately, whereas subsequent steps may be deferred +// debug.log("Splitting ranges to prepare for join of " + bootstrappingNode + "\n"); +// return model.withReadPlacements(splitReplicated(baseState.readPlacements, token)) +// .withWritePlacements(splitReplicated(baseState.writePlacements, token)); +// }, +// (model) -> { // revert +// // final stage of reverting a join is to undo the range splits performed by preparing the operation +// debug.log("Reverting range splits from prepare-join of " + bootstrappingNode + "\n"); +// return model.withWritePlacements(mergeReplicated(model.writePlacements, token)) +// .withReadPlacements(mergeReplicated(model.readPlacements, token)); +// }) +// ); +// +// // Step 1: add new node as a write replica to all ranges it is gaining +// steps.add(new Transformation( +// (model) -> { // apply +// debug.log("Executing start-join of " + bootstrappingNode + "\n"); +// debug.log(String.format("Commands for step 1 of bootstrap of %s.\n" + +// "\twriteModifications=\n%s", +// bootstrappingNode, diffsToString(step1WriteCommands))); +// return model.withWritePlacements(PlacementSimulator.apply(model.writePlacements, step1WriteCommands)); +// }, +// (model) -> { // revert +// debug.log("Reverting start-join of " + bootstrappingNode + "\n"); +// Map> inverted = map(step1WriteCommands, Diff::invert); +// debug.log("Commands for reverting step 1 of bootstrap of %s.\n" + +// "\twriteModifications=\n%s", +// bootstrappingNode, diffsToString(inverted)); +// return model.withWritePlacements(PlacementSimulator.apply(model.writePlacements, inverted)); +// }) +// ); +// +// // Step 2: add new node as a read replica to the ranges it is gaining; remove old node from reads at the same time +// steps.add(new Transformation( +// (model) -> { // apply +// debug.log("Executing mid-join of " + bootstrappingNode + "\n"); +// debug.log(String.format("Commands for step 2 of bootstrap of %s.\n" + +// "\treadCommands=\n%s", +// bootstrappingNode, diffsToString(readCommands))); +// return model.withReadPlacements(PlacementSimulator.apply(model.readPlacements, readCommands)); +// }, +// (model) -> { // revert +// debug.log("Reverting mid-join of " + bootstrappingNode + "\n"); +// Map> inverted = map(readCommands, Diff::invert); +// debug.log(String.format("Commands for reverting step 2 of bootstrap of %s.\n" + +// "\treadCommands=\n%s", +// bootstrappingNode, diffsToString(inverted))); +// return model.withReadPlacements(PlacementSimulator.apply(model.readPlacements, inverted)); +// }) +// ); +// +// +// // Step 3: finally remove the old node from writes +// steps.add(new Transformation( +// (model) -> { // apply +// debug.log("Executing finish-join of " + bootstrappingNode + "\n"); +// debug.log(String.format("Commands for step 3 of bootstrap of %s.\n" + +// "\twriteModifications=\n%s", +// bootstrappingNode, +// diffsToString(step3WriteCommands))); +// List newNodes = new ArrayList<>(model.nodes); +// newNodes.add(bootstrappingNode); +// newNodes.sort(Node::compareTo); +// return model.withNodes(newNodes) +// .withWritePlacements(PlacementSimulator.apply(model.writePlacements, step3WriteCommands)); +// }, +// (model) -> { //revert +// throw new IllegalStateException("Can't revert finish-join of " + bootstrappingNode + ", operation is already complete\n"); +// }) +// ); +// +// debug.log("Planned bootstrap of " + bootstrappingNode + "\n"); +// return steps; +// } +// +// public static Transformations move(SimulatedPlacements baseState, Node movingNode, long newToken) +// { +// List origNodes = new ArrayList<>(baseState.nodes); +// List finalNodes = new ArrayList<>(); +// for (int i = 0; i < origNodes.size(); i++) +// { +// if (origNodes.get(i).idx() == movingNode.idx()) +// continue; +// finalNodes.add(origNodes.get(i)); +// } +// finalNodes.add(movingNode.overrideToken(newToken)); +// finalNodes.sort(Node::compareTo); +// +// Map> start = splitReplicated(baseState.rf.replicate(origNodes).placementsForRange, newToken); +// Map> end = splitReplicated(baseState.rf.replicate(finalNodes).placementsForRange, movingNode.token()); +// +// Map> fromStartToEnd = diff(start, end); +// +// Transformations steps = new Transformations(); +// +// // Step 1: Prepare Move, +// steps.add(new Transformation( +// (model) -> { // apply +// debug.log(String.format("Splitting ranges to prepare for move of %s to %d\n", movingNode, newToken)); +// return model.withReadPlacements(splitReplicated(model.readPlacements, newToken)) +// .withWritePlacements(splitReplicated(model.writePlacements, newToken)); +// }, +// (model) -> { // revert +// debug.log(String.format("Reverting range splits from prepare move of %s to %d\n", movingNode, newToken)); +// return model.withWritePlacements(mergeReplicated(model.writePlacements, newToken)) +// .withReadPlacements(mergeReplicated(model.readPlacements, newToken)); +// }) +// ); +// +// // Step 2: Start Move, add all potential owners to write quorums +// steps.add(new Transformation( +// (model) -> { // apply +// Map> diff = map(fromStartToEnd, Diff::onlyAdditions); +// debug.log("Executing start-move of " + movingNode + "\n"); +// debug.log(String.format("Commands for step 1 of move of %s to %d.\n" + +// "\twriteModifications=\n%s", +// movingNode, newToken, diffsToString(diff))); +// +// NavigableMap> placements = model.writePlacements; +// placements = PlacementSimulator.apply(placements, diff); +// return model.withWritePlacements(placements); +// }, +// (model) -> { // revert +// debug.log("Reverting start-move of " + movingNode + "\n"); +// Map> diff = map(fromStartToEnd, Diff::onlyAdditions); +// Map> inverted = map(diff, Diff::invert); +// debug.log(String.format("Commands for reverting step 1 of move of %s to %d.\n" + +// "\twriteModifications=\n%s", +// movingNode, newToken, diffsToString(inverted))); +// +// return model.withWritePlacements(PlacementSimulator.apply(model.writePlacements, inverted)); +// } +// )); +// // Step 3: Mid Move, remove all nodes that are losing ranges from read quorums, add all nodes gaining ranges to read quorums +// steps.add(new Transformation( +// (model) -> { +// debug.log("Executing mid-move of " + movingNode + "\n"); +// debug.log(String.format("Commands for step 2 of move of %s to %d.\n" + +// "\treadModifications=\n%s", +// movingNode, newToken, diffsToString(fromStartToEnd))); +// +// NavigableMap> placements = model.readPlacements; +// placements = PlacementSimulator.apply(placements, fromStartToEnd); +// return model.withReadPlacements(placements); +// }, +// (model) -> { +// NavigableMap> placements = PlacementSimulator.apply(model.readPlacements, map(fromStartToEnd, Diff::invert)); +// return model.withReadPlacements(placements); +// }) +// ); +// +// // Step 4: Finish Move, remove all nodes that are losing ranges from write quorums +// steps.add(new Transformation( +// (model) -> { +// Map> diff = map(fromStartToEnd, Diff::onlyRemovals); +// +// debug.log("Executing finish-move of " + movingNode + "\n"); +// debug.log(String.format("Commands for step 2 of move of %s to %d.\n" + +// "\twriteModifications=\n%s", +// movingNode, newToken, diffsToString(diff))); +// +// List currentNodes = new ArrayList<>(model.nodes); +// List newNodes = new ArrayList<>(); +// for (int i = 0; i < currentNodes.size(); i++) +// { +// if (currentNodes.get(i).idx() == movingNode.idx()) +// continue; +// newNodes.add(currentNodes.get(i)); +// } +// newNodes.add(movingNode.overrideToken(newToken)); +// newNodes.sort(Node::compareTo); +// +// Map> writePlacements = model.writePlacements; +// writePlacements = PlacementSimulator.apply(writePlacements, diff); +// +// return model.withWritePlacements(mergeReplicated(writePlacements, movingNode.token())) +// .withReadPlacements(mergeReplicated(model.readPlacements, movingNode.token())) +// .withNodes(newNodes); +// }, +// (model) -> { +// throw new IllegalStateException(String.format("Can't revert finish-move of %d, operation is already complete", newToken)); +// }) +// ); +// +// return steps; +// } +// +// public static Transformations leave(SimulatedPlacements baseState, Node toRemove) +// { +// // calculate current placements - this is start state +// Map> start = baseState.rf.replicate(baseState.nodes).placementsForRange; +// +// List afterLeaveNodes = new ArrayList<>(baseState.nodes); +// afterLeaveNodes.remove(toRemove); +// // calculate placements based on existing ranges but final set of nodes - this is end state +// Map> end = baseState.rf.replicate(toRanges(baseState.nodes), afterLeaveNodes).placementsForRange; +// // maximal state is union of start & end +// +// Map> allWriteCommands = diff(start, end); +// Map> step1WriteCommands = map(allWriteCommands, Diff::onlyAdditions); +// Map> step3WriteCommands = map(allWriteCommands, Diff::onlyRemovals); +// Map> readCommands = diff(start, end); +// Transformations steps = new Transformations(); +// steps.add(new Transformation( +// (model) -> { // apply +// debug.log("Executing start-leave of " + toRemove + "\n"); +// debug.log(String.format("Commands for step 1 of decommission of %s.\n" + +// "\twriteModifications=\n%s", +// toRemove, diffsToString(step1WriteCommands))); +// return model.withWritePlacements(PlacementSimulator.apply(model.writePlacements, step1WriteCommands)); +// }, +// (model) -> { // revert +// debug.log("Reverting start-leave of " + toRemove + "\n"); +// Map> inverted = map(step1WriteCommands, Diff::invert); +// debug.log(String.format("Commands for reverting step 1 of decommission of %s.\n" + +// "\twriteModifications=\n%s", +// toRemove, diffsToString(inverted))); +// return model.withWritePlacements(PlacementSimulator.apply(model.writePlacements, inverted)); +// }) +// ); +// +// steps.add(new Transformation( +// (model) -> { // apply +// debug.log("Executing mid-leave of " + toRemove + "\n"); +// debug.log(String.format("Commands for step 2 of decommission of %s.\n" + +// "\treadModifications=\n%s", +// toRemove, +// diffsToString(readCommands))); +// return model.withReadPlacements(PlacementSimulator.apply(model.readPlacements, readCommands)); +// }, +// (model) -> { // revert +// debug.log("Reverting mid-leave of " + toRemove + "\n"); +// Map> inverted = map(readCommands, Diff::invert); +// debug.log(String.format("Commands for reverting step 2 of decommission of %s.\n" + +// "\treadModifications=\n%s", +// toRemove, +// diffsToString(inverted))); +// return model.withReadPlacements(PlacementSimulator.apply(model.readPlacements, inverted)); +// }) +// ); +// +// steps.add(new Transformation( +// (model) -> { // apply +// debug.log("Executing finish-leave decommission of " + toRemove + "\n"); +// debug.log(String.format("Commands for step 3 of decommission of %s.\n" + +// "\twriteModifications=\n%s", +// toRemove, +// diffsToString(step3WriteCommands))); +// List newNodes = new ArrayList<>(model.nodes); +// newNodes.remove(toRemove); +// newNodes.sort(Node::compareTo); +// Map> writes = PlacementSimulator.apply(model.writePlacements, step3WriteCommands); +// return model.withReadPlacements(mergeReplicated(model.readPlacements, toRemove.token())) +// .withWritePlacements(mergeReplicated(writes, toRemove.token())) +// .withNodes(newNodes); +// }, +// (model) -> { // revert +// throw new IllegalStateException("Can't revert finish-leave of " + toRemove + ", operation is already complete\n"); +// })); +// +// debug.log("Planned decommission of " + toRemove + "\n"); +// return steps; +// } +// +// public static Transformations replace(SimulatedPlacements baseState, Node toReplace, Node replacement) +// { +// Map> start = baseState.rf.replicate(baseState.nodes).placementsForRange; +// Map> allCommands = new TreeMap<>(); +// start.forEach((range, nodes) -> { +// if (nodes.contains(toReplace)) +// { +// allCommands.put(range, new Diff<>(Collections.singletonList(replacement), +// Collections.singletonList(toReplace))); +// } +// }); +// Map> step1WriteCommands = map(allCommands, Diff::onlyAdditions); +// Map> step3WriteCommands = map(allCommands, Diff::onlyRemovals); +// Map> readCommands = allCommands; +// Transformations steps = new Transformations(); +// steps.add(new Transformation( +// (model) -> { // apply +// debug.log(String.format("Executing start-replace of %s for %s\n", replacement, toReplace)); +// debug.log(String.format("Commands for step 1 of bootstrap of %s for replacement of %s.\n" + +// "\twriteModifications=\n%s", +// replacement, toReplace, diffsToString(step1WriteCommands))); +// return model.withWritePlacements(PlacementSimulator.apply(model.writePlacements, step1WriteCommands)); +// }, +// (model) -> { // revert +// debug.log(String.format("Reverting start-replace of %s for %s\n", replacement, toReplace)); +// Map> inverted = map(step1WriteCommands, Diff::invert); +// debug.log(String.format("Commands for reverting step 1 of bootstrap of %s for replacement of %s.\n" + +// "\twriteModifications=\n%s", +// replacement, toReplace, diffsToString(inverted))); +// return model.withWritePlacements(PlacementSimulator.apply(model.writePlacements, inverted)); +// }) +// ); +// +// steps.add(new Transformation( +// (model) -> { // apply +// debug.log(String.format("Executing mid-replace of %s for %s\n", replacement, toReplace)); +// debug.log(String.format("Commands for step 2 of bootstrap of %s for replacement of %s.\n" + +// "\treadModifications=\n%s", +// replacement, toReplace, +// diffsToString(readCommands))); +// return model.withReadPlacements(PlacementSimulator.apply(model.readPlacements, readCommands)); +// }, +// (model) -> { // revert +// debug.log(String.format("Reverting mid-replace of %s for %s\n", replacement, toReplace)); +// Map> inverted = map(readCommands, Diff::invert); +// debug.log(String.format("Commands for reverting step 2 of bootstrap of %s for replacement of %s.\n" + +// "\treadModifications=\n%s", +// replacement, toReplace, +// diffsToString(inverted))); +// return model.withReadPlacements(PlacementSimulator.apply(model.readPlacements, inverted)); +// }) +// ); +// +// steps.add(new Transformation( +// (model) -> { // apply +// debug.log(String.format("Executing finish-replace of %s for %s\n", replacement, toReplace)); +// debug.log(String.format("Commands for step 3 of bootstrap of %sfor replacement of %s.\n" + +// "\twriteModifications=\n%s", +// replacement, toReplace, +// diffsToString(step3WriteCommands))); +// List newNodes = new ArrayList<>(model.nodes); +// newNodes.remove(toReplace); +// newNodes.add(replacement); +// newNodes.sort(Node::compareTo); +// return model.withNodes(newNodes) +// .withWritePlacements(PlacementSimulator.apply(model.writePlacements, step3WriteCommands)); +// }, +// (model) -> { // revert +// throw new IllegalStateException(String.format("Can't revert finish-replace of %s for %s, operation is already complete\n", replacement, toReplace)); +// }) +// ); +// +// debug.log(String.format("Planned bootstrap of %s for replacement of %s\n", replacement, toReplace)); +// return steps; +// } +// +// public static void assertPlacements(SimulatedPlacements placements, Map> r, Map> w) +// { +// assertRanges(r, placements.readPlacements); +// assertRanges(w, placements.writePlacements); +// } +// +// public static void assertRanges(Map> expected, Map> actual) +// { +// Assert.assertEquals(expected.keySet(), actual.keySet()); +// expected.forEach((k, v) -> { +// // When comparing replica sets, we only care about the endpoint (i.e. the node.id). For the purpose +// // of simulation, during split operations we duplicate the node holding the range being split as if giving +// // it two tokens, the original one and the split point. e.g. With N1@100, N2@200 then splitting at 150, +// // we will end up with (100, 150] -> N2@150 and (150, 200] -> N2@200. As this is purely an artefact of the +// // 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.idx()).sorted().collect(Collectors.toList()), +// actual.get(k).stream().map(n -> n.idx()).sorted().collect(Collectors.toList())); +// }); +// } +// +// public static boolean containsAll(Set a, Set b) +// { +// if (a.isEmpty() && !b.isEmpty()) +// return false; // empty set does not contain all entries of a non-empty one +// for (T v : b) +// if (!a.contains(v)) +// return false; +// +// return true; +// } +// +// /** +// * Applies a given diff to the placement map +// */ +// public static NavigableMap> apply(Map> orig, Map> diff) +// { +// assert containsAll(orig.keySet(), diff.keySet()) : String.format("Can't apply diff to a map with different sets of keys:" + +// "\nOrig ks: %s" + +// "\nDiff ks: %s" + +// "\nDiff: %s", +// orig.keySet(), diff.keySet(), diff); +// NavigableMap> res = new TreeMap<>(); +// for (Map.Entry> entry : orig.entrySet()) +// { +// Range range = entry.getKey(); +// if (diff.containsKey(range)) +// res.put(range, apply(entry.getValue(), diff.get(range))); +// else +// res.put(range, entry.getValue()); +// } +// return Collections.unmodifiableNavigableMap(res); +// } +// +// /** +// * Apply diff to a list of nodes +// */ +// public static List apply(List nodes, Diff diff) +// { +// Set tmp = new HashSet<>(nodes); +// tmp.addAll(diff.additions); +// for (Node removal : diff.removals) +// tmp.remove(removal); +// List newNodes = new ArrayList<>(tmp); +// newNodes.sort(Node::compareTo); +// return Collections.unmodifiableList(newNodes); +// } +// +// /** +// * Diff two placement maps +// */ +// public static Map> diff(Map> l, Map> r) +// { +// assert l.keySet().equals(r.keySet()) : String.format("Can't diff events from different bases %s %s", l.keySet(), r.keySet()); +// Map> diff = new TreeMap<>(); +// for (Map.Entry> entry : l.entrySet()) +// { +// Range range = entry.getKey(); +// Diff d = diff(entry.getValue(), r.get(range)); +// if (!d.removals.isEmpty() || !d.additions.isEmpty()) +// diff.put(range, d); +// } +// return Collections.unmodifiableMap(diff); +// } +// +// public static Map map(Map diff, Function fn) +// { +// Map newDiff = new TreeMap<>(); +// for (Map.Entry entry : diff.entrySet()) +// { +// T newV = fn.apply(entry.getValue()); +// if (newV != null) +// newDiff.put(entry.getKey(), newV); +// } +// return Collections.unmodifiableMap(newDiff); +// } +// +// public static List map(List coll, Function map) +// { +// List newColl = new ArrayList<>(coll); +// for (T v : coll) +// newColl.add(map.apply(v)); +// return newColl; +// } +// +// /** +// * Produce a diff (i.e. set of additions/subtractions that should be applied to the list of nodes in order to produce +// * r from l) +// */ +// public static Diff diff(List l, List r) +// { +// // additions things present in r but not in l +// List additions = new ArrayList<>(); +// // removals are things present in l but not r +// List removals = new ArrayList<>(); +// +// for (Node i : r) +// { +// boolean isPresentInL = false; +// for (Node j : l) +// { +// if (i.equals(j)) +// { +// isPresentInL = true; +// break; +// } +// } +// +// if (!isPresentInL) +// additions.add(i); +// } +// +// for (Node i : l) +// { +// boolean isPresentInR = false; +// for (Node j : r) +// { +// if (i.equals(j)) +// { +// isPresentInR = true; +// break; +// } +// } +// +// if (!isPresentInR) +// removals.add(i); +// } +// return new Diff<>(additions, removals); +// } +// +// public static Map> superset(Map> l, Map> r) +// { +// assert l.keySet().equals(r.keySet()) : String.format("%s != %s", l.keySet(), r.keySet()); +// +// Map> newState = new TreeMap<>(); +// for (Map.Entry> entry : l.entrySet()) +// { +// Range range = entry.getKey(); +// Set nodes = new HashSet<>(); +// nodes.addAll(entry.getValue()); +// nodes.addAll(r.get(range)); +// newState.put(range, new ArrayList<>(nodes)); +// } +// +// return newState; +// } +// +// public static NavigableMap> mergeReplicated(Map> orig, long removingToken) +// { +// NavigableMap> newState = new TreeMap<>(); +// Iterator>> iter = orig.entrySet().iterator(); +// while (iter.hasNext()) +// { +// Map.Entry> current = iter.next(); +// if (current.getKey().end == removingToken) +// { +// assert iter.hasNext() : "Cannot merge range, no more ranges in list"; +// Map.Entry> next = iter.next(); +// assert current.getValue().containsAll(next.getValue()) && current.getValue().size() == next.getValue().size() +// : "Cannot merge ranges with different replica groups"; +// Range merged = new Range(current.getKey().start, next.getKey().end); +// newState.put(merged, current.getValue()); +// } +// else +// { +// newState.put(current.getKey(), current.getValue()); +// } +// } +// +// return newState; +// } +// +// public static NavigableMap> splitReplicated(Map> orig, long splitAt) +// { +// NavigableMap> newState = new TreeMap<>(); +// for (Map.Entry> entry : orig.entrySet()) +// { +// Range range = entry.getKey(); +// if (range.contains(splitAt)) +// { +// newState.put(new Range(range.start, splitAt), entry.getValue()); +// newState.put(new Range(splitAt, range.end), entry.getValue()); +// } +// else +// { +// newState.put(range, entry.getValue()); +// } +// } +// return newState; +// } +// +// /** +// * "Split" the list of nodes at splitAt, without changing ownership +// */ +// public static List split(List nodes, long splitAt) +// { +// List newNodes = new ArrayList<>(); +// boolean inserted = false; +// Node previous = null; +// for (int i = nodes.size() - 1; i >= 0; i--) +// { +// Node node = nodes.get(i); +// if (!inserted && splitAt > node.token()) +// { +// // We're trying to split rightmost range +// if (previous == null) +// { +// newNodes.add(nodes.get(0).overrideToken(splitAt)); +// } +// else +// { +// newNodes.add(previous.overrideToken(splitAt)); +// } +// inserted = true; +// } +// +// newNodes.add(node); +// previous = node; +// } +// +// // Leftmost is split +// if (!inserted) +// newNodes.add(previous.overrideToken(splitAt)); +// +// newNodes.sort(Node::compareTo); +// return Collections.unmodifiableList(newNodes); +// } +// +// /** +// * Change the ownership of the freshly split token +// */ +// public static List move(List nodes, long tokenToMove, Node newOwner) +// { +// List newNodes = new ArrayList<>(); +// for (Node node : nodes) +// { +// if (node.token() == tokenToMove) +// newNodes.add(newOwner.overrideToken(tokenToMove)); +// else +// newNodes.add(node); +// } +// newNodes.sort(Node::compareTo); +// return Collections.unmodifiableList(newNodes); +// } +// +// public static List filter(List nodes, Predicate pred) +// { +// List newNodes = new ArrayList<>(); +// for (Node node : nodes) +// { +// if (pred.test(node)) +// newNodes.add(node); +// } +// newNodes.sort(Node::compareTo); +// return Collections.unmodifiableList(newNodes); +// } +// +// private static , T1, T2> Map mapValues(Map allDCs, Function map) +// { +// NavigableMap res = new TreeMap<>(); +// for (Map.Entry e : allDCs.entrySet()) +// { +// res.put(e.getKey(), map.apply(e.getValue())); +// } +// return res; +// } +// +// public static Map> nodesByDC(List nodes) +// { +// Map> nodesByDC = new HashMap<>(); +// for (Node node : nodes) +// nodesByDC.computeIfAbsent(node.dc(), (k) -> new ArrayList<>()).add(node); +// +// return nodesByDC; +// } +// +// public static Map> racksByDC(List nodes) +// { +// Map> racksByDC = new HashMap<>(); +// for (Node node : nodes) +// racksByDC.computeIfAbsent(node.dc(), (k) -> new HashSet<>()).add(node.rack()); +// +// return racksByDC; +// } +// +// public static class Diff { +// public final List additions; +// public final List removals; +// +// public Diff(List additions, List removals) +// { +// this.additions = additions; +// this.removals = removals; +// } +// +// public String toString() +// { +// return "Diff{" + +// "additions=" + additions + +// ", removals=" + removals + +// '}'; +// } +// +// public Diff onlyAdditions() +// { +// if (additions.isEmpty()) return null; +// return new Diff<>(additions, Collections.emptyList()); +// } +// +// public Diff onlyRemovals() +// { +// if (removals.isEmpty()) return null; +// return new Diff<>(Collections.emptyList(), removals); +// } +// +// public Diff invert() +// { +// // invert removals & additions +// return new Diff<>(removals, additions); +// } +// } +// +// +// public static String diffsToString(Map> placements) +// { +// StringBuilder builder = new StringBuilder(); +// for (Map.Entry> e : placements.entrySet()) +// { +// builder.append("\t\t").append(e.getKey()).append(": ").append(e.getValue()).append("\n"); +// } +// return builder.toString(); +// } +// +// public static String placementsToString(Map> placements) +// { +// StringBuilder builder = new StringBuilder(); +// for (Map.Entry> e : placements.entrySet()) +// { +// builder.append("\t\t").append(e.getKey()).append(": ").append(e.getValue()).append("\n"); +// } +// return builder.toString(); +// } +// +// public static class DebugLog +// { +// private final BufferedWriter operationLog; +// public DebugLog() +// { +// File f = new File("simulated.log"); +// try +// { +// operationLog = new BufferedWriter(new OutputStreamWriter(new FileOutputStream(f))); +// } +// catch (FileNotFoundException e) +// { +// throw new RuntimeException(e); +// } +// } +// +// public void log(long seq, Object t) +// { +// log("%d: %s\n", seq, t); +// } +// +// private void log(String format, Object... objects) +// { +// try +// { +// operationLog.write(String.format(format, objects)); +// operationLog.flush(); +// } +// catch (IOException e) +// { +// // ignore +// } +// } +// } +// +//} diff --git a/test/harry/main/org/apache/cassandra/harry/operations/CompiledStatement.java b/test/harry/main/org/apache/cassandra/harry/operations/CompiledStatement.java new file mode 100644 index 0000000000..dc29bb1cbb --- /dev/null +++ b/test/harry/main/org/apache/cassandra/harry/operations/CompiledStatement.java @@ -0,0 +1,88 @@ +/* + * 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.operations; + +public class CompiledStatement +{ + private final String cql; + private final Object[] bindings; + + public CompiledStatement(String cql, Object... bindings) + { + this.cql = cql; + this.bindings = bindings; + } + + public String cql() + { + return cql; + } + + public CompiledStatement withSchema(String oldKs, String oldTable, String newKs, String newTable) + { + return new CompiledStatement(cql.replace(oldKs + "." + oldTable, + newKs + "." + newTable), + bindings); + } + + public Object[] bindings() + { + return bindings; + } + + public static CompiledStatement create(String cql, Object... bindings) + { + return new CompiledStatement(cql, bindings); + } + + public String toString() + { + return "CompiledStatement{" + + "cql='" + cql + '\'' + + ", bindings=" + bindingsToString(bindings) + + '}'; + } + + public static String bindingsToString(Object... bindings) + { + StringBuilder sb = new StringBuilder(); + boolean isFirst = true; + for (Object binding : bindings) + { + if (isFirst) + isFirst = false; + else + sb.append(","); + + if (binding instanceof String) + sb.append("\"").append(binding).append("\""); + else if (binding instanceof Short) + sb.append("(short)").append(binding); + else if (binding instanceof Byte) + sb.append("(byte)").append(binding); + else if (binding instanceof Float) + sb.append("(float)").append(binding); + else if (binding instanceof Long) + sb.append(binding).append("L"); + else + sb.append(binding); + } + return sb.toString(); + } +} diff --git a/test/harry/main/org/apache/cassandra/harry/operations/DeleteHelper.java b/test/harry/main/org/apache/cassandra/harry/operations/DeleteHelper.java new file mode 100644 index 0000000000..1ad5ede560 --- /dev/null +++ b/test/harry/main/org/apache/cassandra/harry/operations/DeleteHelper.java @@ -0,0 +1,190 @@ +/* + * 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.operations; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import java.util.function.IntConsumer; + +import org.apache.cassandra.harry.ddl.ColumnSpec; +import org.apache.cassandra.harry.ddl.SchemaSpec; +import org.apache.cassandra.harry.util.BitSet; + +public class DeleteHelper +{ + public static CompiledStatement deleteColumn(SchemaSpec schema, + long pd, + long cd, + BitSet columns, + BitSet mask, + long rts) + { + if (columns == null || columns.allUnset(mask)) + throw new IllegalArgumentException("Can't have a delete column query with no columns set. Column mask: " + columns); + + return delete(schema, pd, cd, columns, mask, rts); + } + + public static CompiledStatement deleteColumn(SchemaSpec schema, + long pd, + BitSet columns, + BitSet mask, + long rts) + { + if (columns == null || columns.allUnset(mask)) + throw new IllegalArgumentException("Can't have a delete column query with no columns set. Column mask: " + columns); + + return delete(schema, pd, columns, mask, rts); + } + + public static CompiledStatement deleteRow(SchemaSpec schema, + long pd, + long cd, + long rts) + { + return delete(schema, pd, cd, null, null, rts); + } + + public static CompiledStatement delete(SchemaSpec schema, + long pd, + List relations, + BitSet columnsToDelete, + BitSet mask, + long rts) + { + assert (columnsToDelete == null && mask == null) || (columnsToDelete != null && mask != null); + return compile(schema, + pd, + relations, + columnsToDelete, + mask, + rts); + } + + private static CompiledStatement delete(SchemaSpec schema, + long pd, + long cd, + BitSet columnsToDelete, + BitSet mask, + long rts) + { + return compile(schema, + pd, + Relation.eqRelations(schema.ckGenerator.slice(cd), + schema.clusteringKeys), + columnsToDelete, + mask, + rts); + } + + private static CompiledStatement delete(SchemaSpec schema, + long pd, + BitSet columnsToDelete, + BitSet mask, + long rts) + { + return compile(schema, + pd, + new ArrayList<>(), + columnsToDelete, + mask, + rts); + } + + public static CompiledStatement delete(SchemaSpec schema, + long pd, + long rts) + { + return compile(schema, + pd, + Collections.emptyList(), + null, + null, + rts); + } + + private static CompiledStatement compile(SchemaSpec schema, + long pd, + List relations, + BitSet columnsToDelete, + BitSet mask, + long ts) + { + StringBuilder b = new StringBuilder(); + b.append("DELETE "); + if (columnsToDelete != null) + { + assert mask != null; + assert relations == null || relations.stream().allMatch((r) -> r.kind == Relation.RelationKind.EQ); + String[] names = columnNames(schema.allColumns, columnsToDelete, mask); + for (int i = 0; i < names.length; i++) + { + if (i > 0) + b.append(", "); + b.append(names[i]); + } + b.append(" "); + } + b.append("FROM ") + .append(schema.keyspace).append(".").append(schema.table) + .append(" USING TIMESTAMP ") + .append(ts) + .append(" WHERE "); + + List bindings = new ArrayList<>(); + + schema.inflateRelations(pd, + relations, + new SchemaSpec.AddRelationCallback() + { + boolean isFirst = true; + public void accept(ColumnSpec spec, Relation.RelationKind kind, Object value) + { + if (isFirst) + isFirst = false; + else + b.append(" AND "); + b.append(kind.getClause(spec)); + bindings.add(value); + } + }); + + b.append(";"); + + Object[] bindingsArr = bindings.toArray(new Object[bindings.size()]); + + return new CompiledStatement(b.toString(), bindingsArr); + } + + private static String[] columnNames(List> columns, BitSet selectedColumns, BitSet mask) + { + String[] columnNames = new String[selectedColumns.setCount(mask)]; + selectedColumns.eachSetBit(new IntConsumer() + { + int i = 0; + + public void accept(int idx) + { + columnNames[i++] = columns.get(idx).name; + } + }, mask); + return columnNames; + } +} diff --git a/test/harry/main/org/apache/cassandra/harry/operations/FilteringQuery.java b/test/harry/main/org/apache/cassandra/harry/operations/FilteringQuery.java new file mode 100644 index 0000000000..e698025901 --- /dev/null +++ b/test/harry/main/org/apache/cassandra/harry/operations/FilteringQuery.java @@ -0,0 +1,68 @@ +/* + * 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.operations; + +import java.util.List; + +import org.apache.cassandra.harry.ddl.SchemaSpec; +import org.apache.cassandra.harry.model.reconciler.Reconciler; +import org.apache.cassandra.harry.util.DescriptorRanges; + +public class FilteringQuery extends Query +{ + public FilteringQuery(long pd, + boolean reverse, + List relations, + SchemaSpec schemaSpec) + { + super(QueryKind.SINGLE_PARTITION, pd, reverse, relations, schemaSpec); + } + + public boolean match(Reconciler.RowState rowState) + { + for (Relation relation : relations) + { + switch (relation.columnSpec.kind) + { + case CLUSTERING: + if (!matchCd(rowState.cd)) + return false; + break; + case REGULAR: + if (!relation.match(rowState.vds[relation.columnSpec.getColumnIndex()])) + return false; + break; + case STATIC: + if (!relation.match(rowState.partitionState.staticRow().vds[relation.columnSpec.getColumnIndex()])) + return false; + break; + case PARTITION_KEY: + if (!relation.match(rowState.partitionState.pd)) + return false; + break; + } + } + return true; + } + + public DescriptorRanges.DescriptorRange toRange(long ts) + { + throw new IllegalStateException("not implemented for filtering query"); + } +} diff --git a/test/harry/main/org/apache/cassandra/harry/operations/Query.java b/test/harry/main/org/apache/cassandra/harry/operations/Query.java new file mode 100644 index 0000000000..92ae2995ce --- /dev/null +++ b/test/harry/main/org/apache/cassandra/harry/operations/Query.java @@ -0,0 +1,501 @@ +/* + * 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.operations; + +import java.util.*; +import java.util.function.LongSupplier; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import org.apache.cassandra.harry.ddl.ColumnSpec; +import org.apache.cassandra.harry.ddl.SchemaSpec; +import org.apache.cassandra.harry.gen.DataGenerators; +import org.apache.cassandra.harry.gen.rng.RngUtils; +import org.apache.cassandra.harry.model.SelectHelper; +import org.apache.cassandra.harry.util.DescriptorRanges; + +import static org.apache.cassandra.harry.operations.QueryGenerator.relationKind; +import static org.apache.cassandra.harry.operations.Relation.FORWARD_COMPARATOR; + +/** + * A class representing relations in the query, essentially what WHERE clause means. + */ +public abstract class Query +{ + private static final Logger logger = LoggerFactory.getLogger(Query.class); + // TODO: There are queries without PD + public final long pd; + public final boolean reverse; + public final List relations; + public final Map> relationsMap; + public final SchemaSpec schemaSpec; + public final QueryKind queryKind; + + public Query(QueryKind kind, long pd, boolean reverse, List relations, SchemaSpec schemaSpec) + { + this.queryKind = kind; + this.pd = pd; + this.reverse = reverse; + this.relations = relations; + this.relationsMap = new HashMap<>(); + for (Relation relation : relations) + this.relationsMap.computeIfAbsent(relation.column(), column -> new ArrayList<>()).add(relation); + this.schemaSpec = schemaSpec; + } + + // TODO: pd, values, filtering? + public boolean matchCd(long cd) + { + return simpleMatch(this, cd); + } + + public static boolean simpleMatch(Query query, + long cd) + { + long[] sliced = query.schemaSpec.ckGenerator.slice(cd); + for (int i = 0; i < query.schemaSpec.clusteringKeys.size(); i++) + { + List relations = query.relationsMap.get(query.schemaSpec.clusteringKeys.get(i).name); + if (relations == null) + continue; + + for (Relation r : relations) + { + if (!r.match(sliced[i])) + return false; + } + } + + return true; + } + + public static class SinglePartitionQuery extends Query + { + public SinglePartitionQuery(QueryKind kind, long pd, boolean reverse, List allRelations, SchemaSpec schemaSpec) + { + super(kind, pd, reverse, allRelations, schemaSpec); + } + + public boolean matchCd(long cd) + { + return true; + } + + public DescriptorRanges.DescriptorRange toRange(long ts) + { + return new DescriptorRanges.DescriptorRange(Long.MIN_VALUE, Long.MAX_VALUE, true, true, ts); + } + + public String toString() + { + return "SinglePartitionQuery{" + + "pd=" + pd + + ", reverse=" + reverse + + ", relations=" + relations + + ", relationsMap=" + relationsMap + + ", schemaSpec=" + schemaSpec + + '}'; + } + } + + public static class SingleClusteringQuery extends Query + { + private final long cd; + + public SingleClusteringQuery(QueryKind kind, long pd, long cd, boolean reverse, List allRelations, SchemaSpec schemaSpec) + { + super(kind, pd, reverse, allRelations, schemaSpec); + this.cd = cd; + } + + public DescriptorRanges.DescriptorRange toRange(long ts) + { + return new DescriptorRanges.DescriptorRange(cd, cd, true, true, ts); + } + + @Override + public boolean matchCd(long cd) + { + return cd == this.cd; + } + } + + public static class ClusteringSliceQuery extends ClusteringRangeQuery + { + public ClusteringSliceQuery(QueryKind kind, + long pd, + long cdMin, + long cdMax, + Relation.RelationKind minRelation, + Relation.RelationKind maxRelation, + boolean reverse, + List allRelations, + SchemaSpec schemaSpec) + { + super(kind, pd, cdMin, cdMax, minRelation, maxRelation, reverse, allRelations, schemaSpec); + } + + public String toString() + { + return "ClusteringSliceQuery{" + + "\n" + toSelectStatement() + + "\npd=" + pd + + "\nreverse=" + reverse + + "\nrelations=" + relations + + "\nrelationsMap=" + relationsMap + + "\nschemaSpec=" + schemaSpec + + "\nqueryKind=" + queryKind + + "\ncdMin=" + cdMin + + "(" + Arrays.toString(schemaSpec.ckGenerator.slice(cdMin)) + ")" + + "\ncdMax=" + cdMax + + "(" + Arrays.toString(schemaSpec.ckGenerator.slice(cdMax)) + ")" + + "\nminRelation=" + minRelation + + "\nmaxRelation=" + maxRelation + + '}' + "\n" + toSelectStatement().cql(); + } + } + + public static class ClusteringRangeQuery extends Query + { + protected final long cdMin; + protected final long cdMax; + protected final Relation.RelationKind minRelation; + protected final Relation.RelationKind maxRelation; + + public ClusteringRangeQuery(QueryKind kind, + long pd, + long cdMin, + long cdMax, + Relation.RelationKind minRelation, + Relation.RelationKind maxRelation, + boolean reverse, + List allRelations, + SchemaSpec schemaSpec) + { + super(kind, pd, reverse, allRelations, schemaSpec); + this.cdMin = cdMin; + this.cdMax = cdMax; + this.minRelation = minRelation; + this.maxRelation = maxRelation; + } + + public DescriptorRanges.DescriptorRange toRange(long ts) + { + return new DescriptorRanges.DescriptorRange(cdMin, cdMax, minRelation.isInclusive(), maxRelation.isInclusive(), ts); + } + + public boolean matchCd(long cd) + { + // TODO: looks like we don't really need comparator here. + Relation.LongComparator cmp = FORWARD_COMPARATOR; + boolean res = minRelation.match(cmp, cd, cdMin) && maxRelation.match(cmp, cd, cdMax); + if (!logger.isDebugEnabled()) + return res; + boolean superRes = super.matchCd(cd); + if (res != superRes) + { + logger.debug("Query did not pass a sanity check.\n{}\n Super call returned: {}, while current call: {}\n" + + "cd = {} {} ({})\n" + + "this.cdMin = {} {} ({})\n" + + "this.cdMax = {} {} ({})\n" + + "minRelation.match(cmp, cd, this.cdMin) = {}\n" + + "maxRelation.match(cmp, cd, this.cdMax) = {}\n", + this, + superRes, res, + cd, Long.toHexString(cd), Arrays.toString(schemaSpec.ckGenerator.slice(cd)), + cdMin, Long.toHexString(cdMin), Arrays.toString(schemaSpec.ckGenerator.slice(cdMin)), + cdMax, Long.toHexString(cdMax), Arrays.toString(schemaSpec.ckGenerator.slice(cdMax)), + minRelation.match(cmp, cd, cdMin), + maxRelation.match(cmp, cd, cdMax)); + } + return res; + } + + public String toString() + { + return "ClusteringRangeQuery{" + + "pd=" + pd + + ", reverse=" + reverse + + ", relations=" + relations + + ", relationsMap=" + relationsMap + + ", schemaSpec=" + schemaSpec + + ", cdMin=" + cdMin + + ", cdMax=" + cdMax + + ", minRelation=" + minRelation + + ", maxRelation=" + maxRelation + + '}'; + } + } + + public CompiledStatement toWildcardSelectStatement() + { + return SelectHelper.select(schemaSpec, pd, null, reverse, false); + } + + public CompiledStatement toSelectStatement() + { + return SelectHelper.select(schemaSpec, pd, schemaSpec.allColumnsSet, relations, reverse, true); + } + + public CompiledStatement toSelectStatement(boolean includeWriteTime) + { + return SelectHelper.select(schemaSpec, pd, schemaSpec.allColumnsSet, relations, reverse, includeWriteTime); + } + + public CompiledStatement toSelectStatement(Set> columns, boolean includeWriteTime) + { + return SelectHelper.select(schemaSpec, pd, columns, relations, reverse, includeWriteTime); + } + + public CompiledStatement toDeleteStatement(long rts) + { + return DeleteHelper.delete(schemaSpec, pd, relations, null, null, rts); + } + + public abstract DescriptorRanges.DescriptorRange toRange(long ts); + + public static Query selectPartition(SchemaSpec schemaSpec, long pd, boolean reverse) + { + return new Query.SinglePartitionQuery(Query.QueryKind.SINGLE_PARTITION, + pd, + reverse, + Collections.emptyList(), + schemaSpec); + } + + public static Query singleClustering(SchemaSpec schema, long pd, long cd, boolean reverse) + { + return new Query.SingleClusteringQuery(Query.QueryKind.SINGLE_CLUSTERING, + pd, + cd, + reverse, + Relation.eqRelations(schema.ckGenerator.slice(cd), schema.clusteringKeys), + schema); + } + + public static Query clusteringSliceQuery(SchemaSpec schema, long pd, long cd, long queryDescriptor, boolean isGt, boolean isEquals, boolean reverse) + { + List relations = new ArrayList<>(); + + long[] sliced = schema.ckGenerator.slice(cd); + long min; + long max; + int nonEqFrom = RngUtils.asInt(queryDescriptor, 0, sliced.length - 1); + + long[] minBound = new long[sliced.length]; + long[] maxBound = new long[sliced.length]; + + // Algorithm that determines boundaries for a clustering slice. + // + // Basic principles are not hard but there are a few edge cases. I haven't figured out how to simplify + // those, so there might be some room for improvement. In short, what we want to achieve is: + // + // 1. Every part that is restricted with an EQ relation goes into the bound verbatim. + // 2. Every part that is restricted with a non-EQ relation (LT, GT, LTE, GTE) is taken into the bound + // if it is required to satisfy the relationship. For example, in `ck1 = 0 AND ck2 < 5`, ck2 will go + // to the _max_ boundary, and minimum value will go to the _min_ boundary, since we can select every + // descriptor that is prefixed with ck1. + // 3. Every other part (e.g., ones that are not explicitly mentioned in the query) has to be restricted + // according to equality. For example, in `ck1 = 0 AND ck2 < 5`, ck3 that is present in schema but not + // mentioned in query, makes sure that any value between [0, min_value, min_value] and [0, 5, min_value] + // is matched. + // + // One edge case is a query on the first clustering key: `ck1 < 5`. In this case, we have to fixup the lower + // value to the minimum possible value. We could really just do Long.MIN_VALUE, but in case we forget to + // adjust entropy elsewhere, it'll be caught correctly here. + for (int i = 0; i < sliced.length; i++) + { + long v = sliced[i]; + DataGenerators.KeyGenerator gen = schema.ckGenerator; + ColumnSpec column = schema.clusteringKeys.get(i); + int idx = i; + LongSupplier maxSupplier = () -> gen.maxValue(idx); + LongSupplier minSupplier = () -> gen.minValue(idx); + + if (i < nonEqFrom) + { + relations.add(Relation.eqRelation(schema.clusteringKeys.get(i), v)); + minBound[i] = v; + maxBound[i] = v; + } + else if (i == nonEqFrom) + { + relations.add(Relation.relation(relationKind(isGt, isEquals), schema.clusteringKeys.get(i), v)); + + if (column.isReversed()) + { + minBound[i] = isGt ? minSupplier.getAsLong() : v; + maxBound[i] = isGt ? v : maxSupplier.getAsLong(); + } + else + { + minBound[i] = isGt ? v : minSupplier.getAsLong(); + maxBound[i] = isGt ? maxSupplier.getAsLong() : v; + } + } + else + { + if (isEquals) + { + minBound[i] = minSupplier.getAsLong(); + maxBound[i] = maxSupplier.getAsLong(); + } + // If we have a non-eq case, all subsequent bounds have to correspond to the maximum in normal case, + // or minimum in case the last bound locked with a relation was reversed. + // + // For example, if we have (ck1, ck2, ck3) as (ASC, DESC, ASC), and query ck1 > X, we'll have: + // [xxxxx | max_value | max_value] + // ck1 ck2 ck3 + // which will exclude xxxx, but take every possible (ck1 > xxxxx) prefixed value. + // + // Similarly, if we have (ck1, ck2, ck3) as (ASC, DESC, ASC), and query ck1 <= X, we'll have: + // [xxxxx | max_value | max_value] + // which will include every (ck1 < xxxxx), and any clustering prefixed with xxxxx. + else if (schema.clusteringKeys.get(nonEqFrom).isReversed()) + maxBound[i] = minBound[i] = isGt ? minSupplier.getAsLong() : maxSupplier.getAsLong(); + else + maxBound[i] = minBound[i] = isGt ? maxSupplier.getAsLong() : minSupplier.getAsLong(); + } + } + + if (schema.clusteringKeys.get(nonEqFrom).isReversed()) + isGt = !isGt; + + min = schema.ckGenerator.stitch(minBound); + max = schema.ckGenerator.stitch(maxBound); + + if (nonEqFrom == 0) + { + min = isGt ? min : schema.ckGenerator.minValue(); + max = !isGt ? max : schema.ckGenerator.maxValue(); + } + + // if we're about to create an "impossible" query, just bump the modifier and re-generate + if (min == max && !isEquals) + throw new IllegalArgumentException("Impossible Query"); + + return new Query.ClusteringSliceQuery(Query.QueryKind.CLUSTERING_SLICE, + pd, + min, + max, + relationKind(true, isGt ? isEquals : true), + relationKind(false, !isGt ? isEquals : true), + reverse, + relations, + schema); + } + + public static Query clusteringRangeQuery(SchemaSpec schema, long pd, long cd1, long cd2, long queryDescriptor, boolean isMinEq, boolean isMaxEq, boolean reverse) + { + List relations = new ArrayList<>(); + + long[] minBound = schema.ckGenerator.slice(cd1); + long[] maxBound = schema.ckGenerator.slice(cd2); + + int nonEqFrom = RngUtils.asInt(queryDescriptor, 0, schema.clusteringKeys.size() - 1); + + // Logic here is similar to how clustering slices are implemented, except for both lower and upper bound + // get their values from sliced value in (1) and (2) cases: + // + // 1. Every part that is restricted with an EQ relation, takes its value from the min bound. + // TODO: this can actually be improved, since in case of hierarchical clustering generation we can + // pick out of the keys that are already locked. That said, we'll exercise more cases the way + // it is implemented right now. + // 2. Every part that is restricted with a non-EQ relation is taken into the bound, if it is used in + // the query. For example in, `ck1 = 0 AND ck2 > 2 AND ck2 < 5`, ck2 values 2 and 5 will be placed, + // correspondingly, to the min and max bound. + // 3. Every other part has to be restricted according to equality. Similar to clustering slice, we have + // to decide whether we use a min or the max value for the bound. Foe example `ck1 = 0 AND ck2 > 2 AND ck2 <= 5`, + // assuming we have ck3 that is present in schema but not mentioned in the query, we'll have bounds + // created as follows: [0, 2, max_value] and [0, 5, max_value]. Idea here is that since ck2 = 2 is excluded, + // we also disallow all ck3 values for [0, 2] prefix. Similarly, since ck2 = 5 is included, we allow every + // ck3 value with a prefix of [0, 5]. + for (int i = 0; i < schema.clusteringKeys.size(); i++) + { + ColumnSpec col = schema.clusteringKeys.get(i); + if (i < nonEqFrom) + { + relations.add(Relation.eqRelation(col, minBound[i])); + maxBound[i] = minBound[i]; + } + else if (i == nonEqFrom) + { + long minLocked = Math.min(minBound[nonEqFrom], maxBound[nonEqFrom]); + long maxLocked = Math.max(minBound[nonEqFrom], maxBound[nonEqFrom]); + relations.add(Relation.relation(relationKind(true, col.isReversed() ? isMaxEq : isMinEq), col, + col.isReversed() ? maxLocked : minLocked)); + relations.add(Relation.relation(relationKind(false, col.isReversed() ? isMinEq : isMaxEq), col, + col.isReversed() ? minLocked : maxLocked)); + minBound[i] = minLocked; + maxBound[i] = maxLocked; + } + else + { + minBound[i] = isMinEq ? schema.ckGenerator.minValue(i) : schema.ckGenerator.maxValue(i); + maxBound[i] = isMaxEq ? schema.ckGenerator.maxValue(i) : schema.ckGenerator.minValue(i); + } + } + + long stitchedMin = schema.ckGenerator.stitch(minBound); + long stitchedMax = schema.ckGenerator.stitch(maxBound); + + // TODO: one of the ways to get rid of garbage here, and potentially even simplify the code is to + // simply return bounds here. After bounds are created, we slice them and generate query right + // from the bounds. In this case, we can even say that things like -inf/+inf are special values, + // and use them as placeholders. Also, it'll be easier to manipulate relations. + return new Query.ClusteringRangeQuery(Query.QueryKind.CLUSTERING_RANGE, + pd, + stitchedMin, + stitchedMax, + relationKind(true, isMinEq), + relationKind(false, isMaxEq), + reverse, + relations, + schema); + } + + public enum QueryKind + { + SINGLE_PARTITION, + SINGLE_CLUSTERING, + // Not sure if that's the best way to name these, but the difference is as follows: + // + // For a single clustering, clustering slice is essentially [x; ∞) or (-∞; x]. + // + // However, in case of multiple clusterings, such as (ck1, ck2, ck3), can result into a range query with locked prefix: + // ck1 = x and ck2 = y and ck3 > z + // + // This would translate into bounds such as: + // ( (x, y, z); (x, y, max_ck3) ) + // + // Logic here is that when we're "locking" x and y, and allow third slice to be in the range [z; ∞). + // Implementation is a bit more involved than that, since we have to make sure it works for all clustering sizes + // and for reversed type. + CLUSTERING_SLICE, + // For a single clustering, clustering slice is essentially [x; y]. + // + // For multiple-clusterings case, for example (ck1, ck2, ck3), we select how many clusterings we "lock" with EQ + // relation, and do a range slice for the rest. For example: + // ck1 = x and ck2 = y and ck3 > z1 and ck3 < z2 + // + // Such queries only make sense if written partition actually has clusterings that have intersecting parts. + CLUSTERING_RANGE + } +} diff --git a/test/harry/main/org/apache/cassandra/harry/operations/QueryGenerator.java b/test/harry/main/org/apache/cassandra/harry/operations/QueryGenerator.java new file mode 100644 index 0000000000..269f60a903 --- /dev/null +++ b/test/harry/main/org/apache/cassandra/harry/operations/QueryGenerator.java @@ -0,0 +1,176 @@ +/* + * 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.operations; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import org.apache.cassandra.harry.core.Run; +import org.apache.cassandra.harry.ddl.SchemaSpec; +import org.apache.cassandra.harry.gen.rng.RngUtils; +import org.apache.cassandra.harry.gen.Surjections; +import org.apache.cassandra.harry.model.OpSelectors; + +// TODO: there's a lot of potential to reduce an amount of garbage here. +// TODO: refactor. Currently, this class is a base for both SELECT and DELETE statements. In retrospect, +// a better way to do the same thing would've been to just inflate bounds, be able to inflate +// any type of query from the bounds, and leave things like "reverse" up to the last mile / implementation. +public class QueryGenerator +{ + private static final Logger logger = LoggerFactory.getLogger(QueryGenerator.class); + + private static final long GT_STREAM = 0b1; + private static final long E_STREAM = 0b10; + + private final OpSelectors.PureRng rng; + private final OpSelectors.PdSelector pdSelector; + private final OpSelectors.DescriptorSelector descriptorSelector; + private final SchemaSpec schema; + + public QueryGenerator(Run run) + { + this(run.schemaSpec, run.pdSelector, run.descriptorSelector, run.rng); + } + + public QueryGenerator(SchemaSpec schema, + OpSelectors.PdSelector pdSelector, + OpSelectors.DescriptorSelector descriptorSelector, + OpSelectors.PureRng rng) + { + this.pdSelector = pdSelector; + this.descriptorSelector = descriptorSelector; + this.schema = schema; + this.rng = rng; + } + + public static class TypedQueryGenerator + { + private final OpSelectors.PureRng rng; + private final QueryGenerator queryGenerator; + private final Surjections.Surjection queryKindGen; + + public TypedQueryGenerator(Run run) + { + this(run.rng, new QueryGenerator(run)); + } + + public TypedQueryGenerator(OpSelectors.PureRng rng, + QueryGenerator queryGenerator) + { + this(rng, Surjections.enumValues(Query.QueryKind.class), queryGenerator); + } + + public TypedQueryGenerator(OpSelectors.PureRng rng, + Surjections.Surjection queryKindGen, + QueryGenerator queryGenerator) + { + this.rng = rng; + this.queryGenerator = queryGenerator; + this.queryKindGen = queryKindGen; + } + + // Queries are inflated from LTS, which identifies the partition, and i, a modifier for the query to + // be able to generate different queries for the same lts. + public Query inflate(long lts, long modifier) + { + long descriptor = rng.next(modifier, lts); + Query.QueryKind queryKind = queryKindGen.inflate(descriptor); + return queryGenerator.inflate(lts, modifier, queryKind); + } + } + + public Query inflate(long lts, long modifier, Query.QueryKind queryKind) + { + long pd = pdSelector.pd(lts, schema); + long queryDescriptor = rng.next(modifier, lts); + boolean reverse = queryDescriptor % 2 == 0; + switch (queryKind) + { + case SINGLE_PARTITION: + return singlePartition(pd, reverse); + case SINGLE_CLUSTERING: + { + long cd = descriptorSelector.randomCd(pd, queryDescriptor, schema); + return singleClustering(pd, cd, reverse); + } + case CLUSTERING_SLICE: + { + long cd = descriptorSelector.randomCd(pd, queryDescriptor, schema); + try + { + return clusteringSliceQuery(pd, cd, queryDescriptor, reverse); + } + catch (IllegalArgumentException retry) + { + return inflate(lts, modifier + 1, queryKind); + } + } + case CLUSTERING_RANGE: + { + try + { + long cd1 = descriptorSelector.randomCd(pd, queryDescriptor, schema); + long cd2 = descriptorSelector.randomCd(pd, rng.next(queryDescriptor, lts), schema); + return clusteringRangeQuery(pd, cd1, cd2, queryDescriptor, reverse); + } + catch (IllegalArgumentException retry) + { + return inflate(lts, modifier + 1, queryKind); + } + } + default: + throw new IllegalArgumentException("Shouldn't happen"); + } + } + + public Query singlePartition(long pd, boolean reverse) + { + return Query.selectPartition(schema, pd, reverse); + } + + public Query singleClustering(long pd, long cd, boolean reverse) + { + return Query.singleClustering(schema, pd, cd, reverse); + } + + public Query clusteringSliceQuery(long pd, long cd, long queryDescriptor, boolean reverse) + { + boolean isGt = RngUtils.asBoolean(rng.next(queryDescriptor, GT_STREAM)); + // TODO: make generation of EQ configurable; turn it off and on + boolean isEquals = RngUtils.asBoolean(rng.next(queryDescriptor, E_STREAM)); + + return Query.clusteringSliceQuery(schema, pd, cd, queryDescriptor, isGt, isEquals, reverse); + } + + public Query clusteringRangeQuery(long pd, long cd1, long cd2, long queryDescriptor, boolean reverse) + { + boolean isMinEq = RngUtils.asBoolean(queryDescriptor); + boolean isMaxEq = RngUtils.asBoolean(rng.next(queryDescriptor, pd)); + + return Query.clusteringRangeQuery(schema, pd, cd1, cd2, queryDescriptor, isMinEq, isMaxEq, reverse); + } + + public static Relation.RelationKind relationKind(boolean isGt, boolean isEquals) + { + if (isGt) + return isEquals ? Relation.RelationKind.GTE : Relation.RelationKind.GT; + else + return isEquals ? Relation.RelationKind.LTE : Relation.RelationKind.LT; + } +} diff --git a/test/harry/main/org/apache/cassandra/harry/operations/Relation.java b/test/harry/main/org/apache/cassandra/harry/operations/Relation.java new file mode 100644 index 0000000000..c953a0da4d --- /dev/null +++ b/test/harry/main/org/apache/cassandra/harry/operations/Relation.java @@ -0,0 +1,299 @@ +/* + * 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.operations; + +import java.util.ArrayList; +import java.util.List; + +import org.apache.cassandra.harry.ddl.ColumnSpec; +import org.apache.cassandra.harry.gen.DataGenerators; + +public class Relation +{ + public final RelationKind kind; + public final ColumnSpec columnSpec; + // Theoretically, in model, we'll just be able to compare stuff according to relation, and pass it to DB + public long descriptor; + + Relation(RelationKind kind, + ColumnSpec columnSpec, + long descriptor) + { + this.kind = kind; + this.columnSpec = columnSpec; + this.descriptor = descriptor; + } + + public boolean match(long l) + { + // TODO: there are == NULL queries + if (l == DataGenerators.NIL_DESCR || l == DataGenerators.UNSET_DESCR) + return false; + + return kind.match(columnSpec.type.generator()::compare, l, descriptor); + } + + public Object value() + { + return columnSpec.inflate(descriptor); + } + + public String column() + { + return columnSpec.name; + } + + public String toClause() + { + return kind.getClause(column()); + } + + public String toString() + { + return "Relation{" + + "kind=" + kind + + ", columnSpec=" + columnSpec + + ", descriptor=" + descriptor + " (" + Long.toHexString(descriptor) + ")" + + '}'; + } + + public static Relation relation(RelationKind kind, ColumnSpec columnSpec, long descriptor) + { + return new Relation(kind, columnSpec, descriptor); + } + + public static Relation eqRelation(ColumnSpec columnSpec, long descriptor) + { + return new Relation(RelationKind.EQ, columnSpec, descriptor); + } + + public static List eqRelations(long[] key, List> columnSpecs) + { + List relations = new ArrayList<>(key.length); + addEqRelation(key, columnSpecs, relations); + return relations; + } + + public static void addEqRelation(long[] key, List> columnSpecs, List relations) + { + addRelation(key, columnSpecs, relations, RelationKind.EQ); + } + + public static void addRelation(long[] key, List> columnSpecs, List relations, RelationKind kind) + { + assert key.length == columnSpecs.size() : + String.format("Key size (%d) should equal to column spec size (%d). Specs: %s", key.length, columnSpecs.size(), columnSpecs); + for (int i = 0; i < key.length; i++) + { + ColumnSpec spec = columnSpecs.get(i); + relations.add(relation(kind, spec, key[i])); + } + } + + public enum RelationKind + { + LT + { + public boolean isNegatable() + { + return true; + } + + public boolean isInclusive() + { + return false; + } + + public RelationKind negate() + { + return GT; + } + + public long nextMatch(long n) + { + return Math.subtractExact(n, 1); + } + + public String toString() + { + return "<"; + } + + public boolean match(LongComparator comparator, long l, long r) + { + return comparator.compare(l, r) < 0; + } + }, + GT + { + public boolean isNegatable() + { + return true; + } + + public boolean isInclusive() + { + return false; + } + + public RelationKind negate() + { + return LT; + } + + public String toString() + { + return ">"; + } + + public boolean match(LongComparator comparator, long l, long r) + { + return comparator.compare(l, r) > 0; + } + + public long nextMatch(long n) + { + return Math.addExact(n, 1); + } + }, + LTE + { + public boolean isNegatable() + { + return true; + } + + public boolean isInclusive() + { + return true; + } + + public RelationKind negate() + { + return GTE; + } + + public String toString() + { + return "<="; + } + + public boolean match(LongComparator comparator, long l, long r) + { + return comparator.compare(l, r) <= 0; + } + + public long nextMatch(long n) + { + return Math.subtractExact(n, 1); + } + }, + GTE + { + public boolean isNegatable() + { + return true; + } + + public boolean isInclusive() + { + return true; + } + + public RelationKind negate() + { + return LTE; + } + + public String toString() + { + return ">="; + } + + public boolean match(LongComparator comparator, long l, long r) + { + return comparator.compare(l, r) >= 0; + } + + public long nextMatch(long n) + { + return Math.addExact(n, 1); + } + }, + EQ + { + public boolean isNegatable() + { + return false; + } + + public boolean isInclusive() + { + return true; + } + + public RelationKind negate() + { + throw new IllegalArgumentException("Cannot negate EQ"); + } + + public long nextMatch(long n) + { + return n; + } + + public String toString() + { + return "="; + } + + public boolean match(LongComparator comparator, long l, long r) + { + return comparator.compare(l, r) == 0; + } + }; + + public abstract boolean match(LongComparator comparator, long l, long r); + + public String getClause(String name) + { + return String.format("%s %s ?", name, toString()); + } + + public String getClause(ColumnSpec spec) + { + return getClause(spec.name); + } + + public abstract boolean isNegatable(); + + public abstract boolean isInclusive(); + + public abstract RelationKind negate(); + + public abstract long nextMatch(long n); + } + + public static interface LongComparator + { + public int compare(long l, long r); + } + + public static LongComparator FORWARD_COMPARATOR = Long::compare; +} diff --git a/test/harry/main/org/apache/cassandra/harry/operations/WriteHelper.java b/test/harry/main/org/apache/cassandra/harry/operations/WriteHelper.java new file mode 100644 index 0000000000..75921dfa47 --- /dev/null +++ b/test/harry/main/org/apache/cassandra/harry/operations/WriteHelper.java @@ -0,0 +1,166 @@ +/* + * 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.operations; + +import java.util.List; + +import org.apache.cassandra.harry.ddl.ColumnSpec; +import org.apache.cassandra.harry.ddl.SchemaSpec; +import org.apache.cassandra.harry.gen.DataGenerators; + +public class WriteHelper +{ + public static CompiledStatement inflateInsert(SchemaSpec schema, + long pd, + long cd, + long[] vds, + long[] sds, + long timestamp) + { + Object[] partitionKey = schema.inflatePartitionKey(pd); + Object[] clusteringKey = schema.inflateClusteringKey(cd); + Object[] staticColumns = sds == null ? null : schema.inflateStaticColumns(sds); + Object[] regularColumns = schema.inflateRegularColumns(vds); + + Object[] bindings = new Object[schema.allColumns.size()]; + + StringBuilder b = new StringBuilder(); + b.append("INSERT INTO ") + .append(schema.keyspace) + .append('.') + .append(schema.table) + .append(" ("); + + int bindingsCount = 0; + bindingsCount += appendStatements(b, bindings, schema.partitionKeys, partitionKey, bindingsCount, true, ",", "%s"); + bindingsCount += appendStatements(b, bindings, schema.clusteringKeys, clusteringKey, bindingsCount, false, ",", "%s"); + bindingsCount += appendStatements(b, bindings, schema.regularColumns, regularColumns, bindingsCount, false, ",", "%s"); + if (staticColumns != null) + bindingsCount += appendStatements(b, bindings, schema.staticColumns, staticColumns, bindingsCount, false, ",", "%s"); + + b.append(") VALUES ("); + + for (int i = 0; i < bindingsCount; i++) + { + if (i > 0) + b.append(", "); + b.append("?"); + } + + b.append(") USING TIMESTAMP ") + .append(timestamp) + .append(";"); + + return new CompiledStatement(b.toString(), adjustArraySize(bindings, bindingsCount)); + } + + public static Object[] adjustArraySize(Object[] bindings, int bindingsCount) + { + if (bindingsCount != bindings.length) + { + Object[] tmp = new Object[bindingsCount]; + System.arraycopy(bindings, 0, tmp, 0, bindingsCount); + bindings = tmp; + } + return bindings; + } + + public static CompiledStatement inflateUpdate(SchemaSpec schema, + long pd, + long cd, + long[] vds, + long[] sds, + long timestamp) + { + Object[] partitionKey = schema.inflatePartitionKey(pd); + Object[] clusteringKey = schema.inflateClusteringKey(cd); + Object[] staticColumns = sds == null ? null : schema.inflateStaticColumns(sds); + Object[] regularColumns = schema.inflateRegularColumns(vds); + + Object[] bindings = new Object[schema.allColumns.size()]; + + StringBuilder b = new StringBuilder(); + b.append("UPDATE ") + .append(schema.keyspace) + .append('.') + .append(schema.table) + .append(" USING TIMESTAMP ") + .append(timestamp) + .append(" SET "); + + int bindingsCount = 0; + bindingsCount += addSetStatements(b, bindings, schema.regularColumns, regularColumns, bindingsCount); + if (staticColumns != null) + bindingsCount += addSetStatements(b, bindings, schema.staticColumns, staticColumns, bindingsCount); + + assert bindingsCount > 0 : "Can not have an UPDATE statement without any updates"; + b.append(" WHERE "); + + bindingsCount += addWhereStatements(b, bindings, schema.partitionKeys, partitionKey, bindingsCount, true); + bindingsCount += addWhereStatements(b, bindings, schema.clusteringKeys, clusteringKey, bindingsCount, false); + b.append(";"); + return new CompiledStatement(b.toString(), adjustArraySize(bindings, bindingsCount)); + } + + private static int addSetStatements(StringBuilder b, + Object[] bindings, + List> columns, + Object[] values, + int bound) + { + return appendStatements(b, bindings, columns, values, bound, bound == 0, ", ", "%s = ?"); + } + + private static int addWhereStatements(StringBuilder b, + Object[] bindings, + List> columns, + Object[] values, + int bound, + boolean firstStatement) + { + return appendStatements(b, bindings, columns, values, bound, firstStatement, " AND ", "%s = ?"); + } + + private static int appendStatements(StringBuilder b, + Object[] allBindings, + List> columns, + Object[] values, + int bound, + boolean firstStatement, + String separator, + String nameFormatter) + { + int bindingsCount = 0; + for (int i = 0; i < values.length; i++) + { + Object value = values[i]; + if (value == DataGenerators.UNSET_VALUE) + continue; + + ColumnSpec column = columns.get(i); + if (bindingsCount > 0 || !firstStatement) + b.append(separator); + + b.append(String.format(nameFormatter, column.name)); + allBindings[bound + bindingsCount] = value; + bindingsCount++; + } + return bindingsCount; + } +} \ No newline at end of file diff --git a/test/harry/main/org/apache/cassandra/harry/runner/EarlyExitException.java b/test/harry/main/org/apache/cassandra/harry/runner/EarlyExitException.java new file mode 100644 index 0000000000..76b4229fd8 --- /dev/null +++ b/test/harry/main/org/apache/cassandra/harry/runner/EarlyExitException.java @@ -0,0 +1,27 @@ +/* + * 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.runner; + +public class EarlyExitException extends RuntimeException +{ + public EarlyExitException(String e) + { + super(e); + } +} diff --git a/test/distributed/org/apache/cassandra/distributed/harry/FlaggedRunner.java b/test/harry/main/org/apache/cassandra/harry/runner/FlaggedRunner.java similarity index 82% rename from test/distributed/org/apache/cassandra/distributed/harry/FlaggedRunner.java rename to test/harry/main/org/apache/cassandra/harry/runner/FlaggedRunner.java index b39c7eb51f..c2450cd8bd 100644 --- a/test/distributed/org/apache/cassandra/distributed/harry/FlaggedRunner.java +++ b/test/harry/main/org/apache/cassandra/harry/runner/FlaggedRunner.java @@ -16,24 +16,23 @@ * limitations under the License. */ -package org.apache.cassandra.distributed.harry; +package org.apache.cassandra.harry.runner; import java.util.ArrayList; import java.util.HashMap; import java.util.List; import java.util.Map; -import harry.concurrent.ExecutorFactory; -import harry.concurrent.Interruptible; -import harry.core.Configuration; -import harry.core.Run; -import harry.runner.Runner; -import harry.visitors.Visitor; +import org.apache.cassandra.concurrent.ExecutorFactory; +import org.apache.cassandra.concurrent.Interruptible; +import org.apache.cassandra.harry.core.Configuration; +import org.apache.cassandra.harry.core.Run; +import org.apache.cassandra.harry.visitors.Visitor; import org.apache.cassandra.utils.concurrent.CountDownLatch; -import static harry.concurrent.InfiniteLoopExecutor.Daemon.NON_DAEMON; -import static harry.concurrent.InfiniteLoopExecutor.Interrupts.UNSYNCHRONIZED; -import static harry.concurrent.InfiniteLoopExecutor.SimulatorSafe.SAFE; +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 FlaggedRunner extends Runner { diff --git a/test/harry/main/org/apache/cassandra/harry/runner/HarryRunner.java b/test/harry/main/org/apache/cassandra/harry/runner/HarryRunner.java new file mode 100644 index 0000000000..0bfbb31b66 --- /dev/null +++ b/test/harry/main/org/apache/cassandra/harry/runner/HarryRunner.java @@ -0,0 +1,115 @@ +/* + * 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.runner; + +import java.io.File; +import java.io.FileNotFoundException; + +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.util.ThrowingRunnable; + +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.ORG_APACHE_CASSANDRA_DISABLE_MBEAN_REGISTRATION; + +public abstract class HarryRunner +{ + public static final Logger logger = LoggerFactory.getLogger(HarryRunner.class); + + public void run(Configuration config) throws Throwable + { + DISABLE_TCACTIVE_OPENSSL.setBoolean(true); + IO_NETTY_TRANSPORT_NONATIVE.setBoolean(true); + ORG_APACHE_CASSANDRA_DISABLE_MBEAN_REGISTRATION.setBoolean(true); + + Runner runner = config.createRunner(); + Run run = runner.getRun(); + + Throwable thrown = null; + + try + { + beforeRun(runner); + runner.run(); + } + catch (Throwable e) + { + logger.error("Failed due to exception: " + e.getMessage(), e); + thrown = e; + } + finally + { + logger.info("Shutting down runner.."); + boolean failed = thrown != null; + if (!failed) + { + logger.info("Shutting down cluster.."); + tryRun(run.sut::shutdown); + } + afterRun(runner, thrown); + logger.info("Exiting..."); + if (failed) + System.exit(1); + else + System.exit(0); + } + } + + public void tryRun(ThrowingRunnable runnable) + { + try + { + runnable.run(); + } + catch (Throwable t) + { + logger.error("Encountered an error while shutting down, ignoring.", t); + } + } + + /** + * Parses the command-line args and returns a File for the configuration YAML. + * @param args Command-line args. + * @return Configuration YAML file. + * @throws Exception If file is not found or cannot be read. + */ + public static File loadConfig(String... args) throws Exception { + if (args == null || args.length == 0) { + throw new Exception("Harry config YAML not provided."); + } + + File configFile = new File(args[0]); + if (!configFile.exists()) { + throw new FileNotFoundException(configFile.getAbsolutePath()); + } + + if (!configFile.canRead()) { + throw new Exception("Cannot read config file, check your permissions on " + configFile.getAbsolutePath()); + } + + return configFile; + } + + public abstract void beforeRun(Runner runner); + public abstract void afterRun(Runner runner, Object result); +} \ No newline at end of file diff --git a/test/harry/main/org/apache/cassandra/harry/runner/Runner.java b/test/harry/main/org/apache/cassandra/harry/runner/Runner.java new file mode 100644 index 0000000000..c62e1b671a --- /dev/null +++ b/test/harry/main/org/apache/cassandra/harry/runner/Runner.java @@ -0,0 +1,479 @@ +/* + * 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.runner; + +import java.io.BufferedWriter; +import java.io.File; +import java.io.FileOutputStream; +import java.io.IOException; +import java.io.OutputStreamWriter; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.List; +import java.util.concurrent.CopyOnWriteArrayList; +import java.util.concurrent.TimeUnit; +import java.util.function.Consumer; +import java.util.function.Supplier; +import java.util.stream.Collectors; +import java.util.stream.Stream; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import org.apache.cassandra.concurrent.ExecutorFactory; +import org.apache.cassandra.concurrent.Interruptible; +import org.apache.cassandra.concurrent.Shutdownable; +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.visitors.Visitor; +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; +import static org.apache.cassandra.utils.Clock.Global.nanoTime; + +public abstract class Runner +{ + private static final Logger logger = LoggerFactory.getLogger(Runner.class); + + protected final Run run; + protected final Configuration config; + + // If there's an error, there's a good chance we're going to hit it more than once + // since we have multiple concurrent checkers running + protected final List errors; + + public Runner(Run run, Configuration config) + { + this.run = run; + this.config = config; + this.errors = new CopyOnWriteArrayList<>(); + } + + public void run() throws Throwable + { + init(config, run); + try + { + runInternal(); + } + catch (EarlyExitException t) + { + logger.warn("Exiting early...", t); + } + teardown(); + } + + protected abstract void runInternal() throws Throwable; + + public Run getRun() + { + return run; + } + + public static void init(Configuration config, Run run) + { + if (config.create_schema) + { + if (config.keyspace_ddl == null) + run.sut.schemaChange("CREATE KEYSPACE IF NOT EXISTS " + run.schemaSpec.keyspace + " WITH replication = {'class': 'SimpleStrategy', 'replication_factor': 3};"); + else + run.sut.schemaChange(config.keyspace_ddl); + + String schema = run.schemaSpec.compile().cql(); + logger.info("Creating table: " + schema); + run.sut.schemaChange(schema); + } + + if (config.truncate_table) + { + run.sut.schemaChange(String.format("truncate %s.%s;", + run.schemaSpec.keyspace, + run.schemaSpec.table)); + } + + run.sut.afterSchemaInit(); + + int res = run.sut.execute(String.format("SELECT * FROM %s.%s LIMIT 1", run.schemaSpec.keyspace, run.schemaSpec.table), SystemUnderTest.ConsistencyLevel.QUORUM).length; + if (res > 0) + { + System.out.println("========================================================================================================================"); + System.out.println("| |"); + System.out.println("| WARNING: Starting a run with non-empty tables! |"); + System.out.println("| |"); + System.out.println("========================================================================================================================"); + } + } + + public void teardown() + { + logger.info("Tearing down setup..."); + if (config.drop_schema) + { + if (!errors.isEmpty()) + { + logger.info("Preserving table {} due to errors during execution.", + run.schemaSpec.table); + return; + } + + logger.info("Dropping table: " + run.schemaSpec.table); + run.sut.schemaChange(String.format("DROP TABLE IF EXISTS %s.%s;", + run.schemaSpec.keyspace, + run.schemaSpec.table)); + } + } + + protected void maybeReportErrors() + { + if (!errors.isEmpty()) { + dumpStateToFile(run, config, errors); + } + } + + public abstract String type(); + + public interface RunnerFactory + { + Runner make(Run run, Configuration config); + } + + public abstract static class TimedRunner extends Runner + { + public final long runtime; + public final TimeUnit runtimeUnit; + + public TimedRunner(Run run, Configuration config, long runtime, TimeUnit runtimeUnit) + { + super(run, config); + + this.runtime = runtime; + this.runtimeUnit = runtimeUnit; + } + } + + public static Configuration.RunnerConfiguration single(Visitor.VisitorFactory visitor) + { + return (run, config) -> new SingleVisitRunner(run, config, Collections.singletonList(visitor)); + } + + public static class SingleVisitRunner extends Runner + { + private final List visitors; + + public SingleVisitRunner(Run run, + Configuration config, + List visitorFactories) + { + super(run, config); + this.visitors = visitorFactories.stream().map(factory -> factory.make(run)).collect(Collectors.toList()); + } + + @Override + public String type() + { + return "single"; + } + + public void runInternal() + { + for (Visitor value : visitors) + value.visit(); + } + } + + public static Configuration.RunnerConfiguration sequential(Visitor.VisitorFactory visitor, + long runtime, TimeUnit runtimeUnit) + { + return (r, c) -> new SequentialRunner(r, c, Collections.singletonList(visitor), runtime, runtimeUnit); + } + + public static Runner sequential(Run run, + Configuration config, + List poolConfigurations, + long runtime, TimeUnit runtimeUnit) + { + return new SequentialRunner(run, config, poolConfigurations, runtime, runtimeUnit); + } + + /** + * Runs all visitors sequentially, in the loop, for a specified amount of time + */ + public static class SequentialRunner extends TimedRunner + { + protected final List visitors; + + public SequentialRunner(Run run, + Configuration config, + List visitorFactories, + long runtime, TimeUnit runtimeUnit) + { + super(run, config, runtime, runtimeUnit); + + this.visitors = visitorFactories.stream().map(factory -> factory.make(run)).collect(Collectors.toList()); + } + + @Override + public String type() + { + return "sequential"; + } + + @Override + public void runInternal() throws Throwable + { + long deadline = nanoTime() + runtimeUnit.toNanos(runtime);; + while (nanoTime() < deadline) + { + for (Visitor visitor : visitors) + visitor.visit(); + } + } + } + + public static Runner concurrent(Configuration config, + List poolConfigurations, + long runtime, TimeUnit runtimeUnit) + { + return new ConcurrentRunner(config.createRun(), config, poolConfigurations, runtime, runtimeUnit); + } + + /** + * Runs all visitors concurrently, each visitor in its own thread, looped, for a specified amount of time + */ + public static class ConcurrentRunner extends TimedRunner + { + private final List poolConfigurations; + + public ConcurrentRunner(Run run, + Configuration config, + List poolConfigurations, + long runtime, TimeUnit runtimeUnit) + { + super(run, config, runtime, runtimeUnit); + this.poolConfigurations = poolConfigurations; + } + + @Override + public String type() + { + return "concurrent"; + } + + public void runInternal() throws Throwable + { + List threads = new ArrayList<>(); + WaitQueue queue = WaitQueue.newWaitQueue(); + WaitQueue.Signal interrupt = queue.register(); + + for (Configuration.VisitorPoolConfiguration poolConfiguration : poolConfigurations) + { + for (int i = 0; i < poolConfiguration.concurrency; i++) + { + Visitor visitor = poolConfiguration.visitor.make(run); + String name = String.format("%s-%d", poolConfiguration.prefix, i + 1); + Interruptible thread = ExecutorFactory.Global.executorFactory().infiniteLoop(name, wrapInterrupt((state) -> { + if (state == Interruptible.State.NORMAL) + visitor.visit(); + }, interrupt::signal, errors::add), SAFE, NON_DAEMON, UNSYNCHRONIZED); + threads.add(thread); + } + } + + interrupt.await(runtime, runtimeUnit); + shutdown(threads::stream); + if (!errors.isEmpty()) + mergeAndThrow(errors); + } + } + + public static Runner chain(Configuration config, + Configuration.RunnerConfiguration... runners) + { + return new ChainRunner(config.createRun(), config, Arrays.asList(runners)); + } + + /** + * Chain runner is somewhat similar to StagedRunner in that it would execute multiple runners one after + * another, but unlike StagedRunner, it is single-shot: it will just run them once. + */ + public static class ChainRunner extends Runner + { + private final List stages; + + public ChainRunner(Run run, + Configuration config, + List runnerFactories) + { + super(run, config); + this.stages = new ArrayList<>(); + for (Configuration.RunnerConfiguration runner : runnerFactories) + stages.add(runner.make(run, config)); + } + + protected void runInternal() throws Throwable + { + for (Runner stage : stages) + stage.runInternal(); + } + + public String type() + { + return "chain"; + } + } + + public static void shutdown(Supplier> threads) + { + threads.get().forEach(Shutdownable::shutdown); + long deadline = nanoTime(); + threads.get().forEach((interruptible) -> { + try + { + if (!interruptible.awaitTermination(nanoTime() - deadline, TimeUnit.NANOSECONDS)) + logger.info("Could not terminate before the timeout: " + threads.get().map(Shutdownable::isTerminated).collect(Collectors.toList())); + } + catch (InterruptedException e) + { + throw new RuntimeException(e); + } + }); + + } + + public static Interruptible.Task wrapInterrupt(Interruptible.Task runnable, Runnable signalStop, Consumer registerException) + { + return (state) -> { + try + { + runnable.run(state); + } + catch (InterruptedException t) + { + signalStop.run(); + } + catch (Throwable t) + { + // Since some of the exceptions are thrown from inside instances + if (!t.getClass().toString().contains("InterruptedException")) + registerException.accept(t); + signalStop.run(); + } + }; + } + + public static void mergeAndThrow(List existingFail) + { + List skipped = existingFail.stream().filter(e -> e instanceof EarlyExitException).collect(Collectors.toList()); + for (Throwable throwable : skipped) + { + logger.warn("Skipping exit early exceptions", throwable); + return; + } + + List errors = existingFail.stream().filter(e -> !(e instanceof EarlyExitException)).collect(Collectors.toList()); + if (errors.size() == 1) + throw new RuntimeException("Interrupting run because of an exception", errors.get(0)); + + Throwable e = errors.get(0); + Throwable ret = e; + for (int i = 1; i < errors.size(); i++) + { + Throwable current = errors.get(i); + e.addSuppressed(current); + e = current; + } + + throw new RuntimeException("Interrupting run because of an exception", ret); + } + + private static void dumpExceptionToFile(BufferedWriter bw, Throwable throwable) throws IOException + { + if (throwable.getMessage() != null) + bw.write(throwable.getMessage()); + else + bw.write(""); + + bw.newLine(); + for (StackTraceElement line : throwable.getStackTrace()) + { + bw.write(line.toString()); + bw.newLine(); + } + bw.newLine(); + if (throwable.getCause() != null) + { + bw.write("Inner Exception: "); + dumpExceptionToFile(bw, throwable.getCause()); + } + + bw.newLine(); + bw.newLine(); + } + + public static void dumpStateToFile(Run run, Configuration config, List t) + { + try + { + File f = new File("failure.dump"); + logger.error("Dumping results into the file:" + f); + try (BufferedWriter bw = new BufferedWriter(new OutputStreamWriter(new FileOutputStream(f)))) + { + bw.write("Caught exception during the run: "); + for (Throwable throwable : t) + dumpExceptionToFile(bw, throwable); + + bw.flush(); + } + + File file = new File("run.yaml"); + Configuration.ConfigurationBuilder builder = config.unbuild(); + + // override stateful components + builder.setClock(run.clock.toConfig()); + builder.setDataTracker(run.tracker.toConfig()); + + Configuration.toFile(file, builder.build()); + } + + catch (Throwable e) + { + logger.error("Caught an error while trying to dump to file", e); + try + { + File f = new File("tmp.dump"); + + if (!f.createNewFile()) + logger.info("File {} already exists. Appending...", f); + + BufferedWriter tmp = new BufferedWriter(new OutputStreamWriter(new FileOutputStream(f))); + dumpExceptionToFile(tmp, e); + } + catch (IOException ex) + { + ex.printStackTrace(); + } + + throw new RuntimeException(e); + } + } +} \ No newline at end of file diff --git a/test/harry/main/org/apache/cassandra/harry/runner/StagedRunner.java b/test/harry/main/org/apache/cassandra/harry/runner/StagedRunner.java new file mode 100644 index 0000000000..bac36bfa25 --- /dev/null +++ b/test/harry/main/org/apache/cassandra/harry/runner/StagedRunner.java @@ -0,0 +1,92 @@ +/* + * 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.runner; + +import java.util.ArrayList; +import java.util.List; +import java.util.concurrent.TimeUnit; + +import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.annotation.JsonProperty; +import com.fasterxml.jackson.annotation.JsonTypeName; +import org.apache.cassandra.harry.core.Configuration; +import org.apache.cassandra.harry.core.Run; + +import static org.apache.cassandra.utils.Clock.Global.nanoTime; + +public class StagedRunner extends Runner.TimedRunner +{ + public static final String TYPE = "staged"; + + private final List stages; + + public StagedRunner(Run run, + Configuration config, + List runnerFactories, + long runtime, TimeUnit runtimeUnit) + { + super(run, config, runtime, runtimeUnit); + this.stages = new ArrayList<>(); + for (Configuration.RunnerConfiguration runner : runnerFactories) + stages.add(runner.make(run, config)); + } + + @Override + public String type() { + return TYPE; + } + + @Override + protected void runInternal() throws Throwable + { + long deadline = nanoTime() + runtimeUnit.toNanos(nanoTime()); + + while (nanoTime() < deadline) + { + for (Runner runner : stages) + runner.runInternal(); + } + } + + @JsonTypeName(TYPE) + public static class StagedRunnerConfig implements Configuration.RunnerConfiguration + { + @JsonProperty(value = "stages") + public final List runnerFactories; + + public final long run_time; + public final TimeUnit run_time_unit; + + @JsonCreator + public StagedRunnerConfig(@JsonProperty(value = "stages") List stages, + @JsonProperty(value = "run_time", defaultValue = "2") long runtime, + @JsonProperty(value = "run_time_unit", defaultValue = "HOURS") TimeUnit runtimeUnit) + { + this.runnerFactories = stages; + this.run_time = runtime; + this.run_time_unit = runtimeUnit; + } + + @Override + public Runner make(Run run, Configuration config) + { + return new StagedRunner(run, config, runnerFactories, run_time, run_time_unit); + } + } +} diff --git a/test/harry/main/org/apache/cassandra/harry/runner/TrivialShrinker.java b/test/harry/main/org/apache/cassandra/harry/runner/TrivialShrinker.java new file mode 100644 index 0000000000..a1bdc01af2 --- /dev/null +++ b/test/harry/main/org/apache/cassandra/harry/runner/TrivialShrinker.java @@ -0,0 +1,189 @@ +/* + * 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.runner; + +import java.io.File; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.HashSet; +import java.util.List; +import java.util.Set; +import java.util.concurrent.atomic.AtomicLong; +import java.util.function.Predicate; + +import org.apache.cassandra.harry.core.Configuration; +import org.apache.cassandra.harry.core.Run; +import org.apache.cassandra.harry.visitors.LtsVisitor; +import org.apache.cassandra.harry.visitors.SkippingVisitor; +import org.apache.cassandra.harry.visitors.Visitor; + +/** + * A most trivial imaginable shrinker: attempts to skip partitions and/or logical timestamps to see if the + * issue is still reproducible. + */ +public class TrivialShrinker +{ + public static void main(String[] args) throws Throwable + { + try + { + File configFile = HarryRunner.loadConfig(args); + Configuration configuration = Configuration.fromFile(configFile); + System.out.println(Configuration.toYamlString(configuration)); + + Set pdsToSkip = new HashSet<>(Arrays.asList( + // put pds you want to skip here, or Harry will find them for you + )); + + Set ltsToSkip = new HashSet<>(Arrays.asList( + // put lts you want to skip here, or Harry will find them for you + )); + + // Which LTS failure has occurred on + final long maxLts = 7000L; + + // Check if we've found exactly the exception that is causing the failure + Predicate check = (e) -> true; + + Run run = configuration.createRun(); + Configuration.SequentialRunnerConfig config = (Configuration.SequentialRunnerConfig) configuration.runner; + List visitors = new ArrayList<>(); + for (Configuration.VisitorConfiguration factory : config.visitorFactories) + { + Visitor visitor = factory.make(run); + if (visitor instanceof LtsVisitor) + { + AtomicLong counter = new AtomicLong(); + visitors.add(new SkippingVisitor((LtsVisitor) visitor, + counter::getAndIncrement, + (lts) -> run.pdSelector.pd(lts, run.schemaSpec), + ltsToSkip, + pdsToSkip)) ; + } + else + { + visitors.add(visitor); + } + } + + Set partitions = new HashSet<>(); + for (long i = 0; i < maxLts; i++) + partitions.add(run.pdSelector.pd(i, run.schemaSpec)); + + // Step one: figure out which partitions we can skip while still keeping it reproducible + for (Long pdToCheck : partitions) + { + if (pdsToSkip.contains(pdToCheck)) + continue; + pdsToSkip.add(pdToCheck); + Runner.init(configuration, run); + + try + { + runOnce(visitors, maxLts); + System.out.println("Can not skip " + pdToCheck + "\nCan only skip these: " + toString(pdsToSkip)); + pdsToSkip.remove(pdToCheck); + } + catch (RuntimeException t) + { + if (check.test(t)) + { + System.out.printf("Safe to skip: %d because without it we're still hitting an exception %s.\n%s\n", + pdToCheck, + t.getMessage(), + toString(pdsToSkip)); + } + else + { + System.out.println("Can not skip " + pdToCheck + "\n, since we seem to repro a different issue. Can only skip these: " + toString(pdsToSkip)); + pdsToSkip.remove(pdToCheck); + } + } + run.sut.schemaChange("DROP KEYSPACE " + run.schemaSpec.keyspace); + } + + // Step two: figure out which lts can be skipped within the remaining partitions + for (long lts = 0; lts < maxLts; lts++) + { + long ltsToCheck = lts; + if (ltsToSkip.contains(ltsToCheck) || pdsToSkip.contains(run.pdSelector.pd(lts, run.schemaSpec))) + continue; + ltsToSkip.add(ltsToCheck); + Runner.init(configuration, run); + + try + { + runOnce(visitors, maxLts); + System.out.println("Can not skip " + ltsToCheck + "\nCan only skip these: " + toString(ltsToSkip)); + ltsToSkip.remove(ltsToCheck); + } + catch (RuntimeException t) + { + if (check.test(t)) + { + System.out.printf("Safe to skip: %d because without it we're still hitting an exception %s.\n%s\n", + ltsToCheck, + t.getMessage(), + toString(ltsToSkip)); + } + else + { + System.out.println("Can not skip " + lts + "\n, since we seem to repro a different issue. Can only skip these: " + toString(ltsToSkip)); + ltsToSkip.remove(ltsToCheck); + } + + } + run.sut.schemaChange("DROP KEYSPACE " + run.schemaSpec.keyspace); + } + } + catch (Throwable t) + { + System.out.println(t.getMessage()); + t.printStackTrace(); + } + finally + { + System.exit(1); + } + } + + public static void runOnce(List visitors, long maxLts) + { + for (long lts = 0; lts <= maxLts; lts++) + { + for (Visitor visitor : visitors) + { + visitor.visit(); + } + } + } + + public static String toString(Set longs) + { + if (longs.isEmpty()) + return ""; + + String s = ""; + for (Long aLong : longs) + { + s += aLong + "L,"; + } + return s.substring(0, s.length() - 1); + } +} diff --git a/test/harry/main/org/apache/cassandra/harry/runner/UpToLtsRunner.java b/test/harry/main/org/apache/cassandra/harry/runner/UpToLtsRunner.java new file mode 100644 index 0000000000..9fc2cec5aa --- /dev/null +++ b/test/harry/main/org/apache/cassandra/harry/runner/UpToLtsRunner.java @@ -0,0 +1,111 @@ +/* + * 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.runner; + +import java.util.Collections; +import java.util.List; +import java.util.concurrent.TimeUnit; + +import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.annotation.JsonProperty; +import com.fasterxml.jackson.annotation.JsonTypeName; +import org.apache.cassandra.harry.core.Configuration; +import org.apache.cassandra.harry.core.Run; +import org.apache.cassandra.harry.visitors.Visitor; + +import static org.apache.cassandra.utils.Clock.Global.nanoTime; + +/** + * Runner that allows to run for a specific number of logical timestamps, rather + * than being configured by time. + */ +public class UpToLtsRunner extends Runner.SequentialRunner +{ + public static final String TYPE = "up_to_lts"; + + private final long maxLts; + + public static Configuration.RunnerConfiguration factory(List visitorFactories, + long maxLts, + long runtime, TimeUnit runtimeUnit) + { + return (r, c) -> new UpToLtsRunner(r, c, visitorFactories, maxLts, runtime, runtimeUnit); + } + + public static Configuration.RunnerConfiguration factory(Visitor.VisitorFactory visitorFactory, + long maxLts, + long runtime, TimeUnit runtimeUnit) + { + return (r, c) -> new UpToLtsRunner(r, c, Collections.singletonList(visitorFactory), maxLts, runtime, runtimeUnit); + } + + public UpToLtsRunner(Run run, + Configuration config, + List visitorFactories, + long maxLts, + long runtime, TimeUnit runtimeUnit) + { + super(run, config, visitorFactories, runtime, runtimeUnit); + this.maxLts = maxLts; + } + + @Override + public String type() + { + return TYPE; + } + + @Override + public void runInternal() + { + long deadline = nanoTime() + runtimeUnit.toNanos(runtime); + while (run.tracker.maxStarted() < maxLts && nanoTime() < deadline) + { + for (Visitor visitor : visitors) + visitor.visit(); + } + } + + @JsonTypeName(TYPE) + public static class UpToLtsRunnerConfig implements Configuration.RunnerConfiguration + { + public final List visitor_factories; + public final long max_lts; + public final long run_time; + public final TimeUnit run_time_unit; + + @JsonCreator + public UpToLtsRunnerConfig(@JsonProperty(value = "visitors") List visitors, + @JsonProperty(value = "max_lts") long maxLts, + @JsonProperty(value = "run_time", defaultValue = "2") long runtime, + @JsonProperty(value = "run_time_unit", defaultValue = "HOURS") TimeUnit runtimeUnit) + { + this.visitor_factories = visitors; + this.max_lts = maxLts; + this.run_time = runtime; + this.run_time_unit = runtimeUnit; + } + + @Override + public Runner make(Run run, Configuration config) + { + return new UpToLtsRunner(run, config, visitor_factories, max_lts, run_time, run_time_unit); + } + } +} diff --git a/test/harry/main/org/apache/cassandra/harry/sut/DoubleWritingSut.java b/test/harry/main/org/apache/cassandra/harry/sut/DoubleWritingSut.java new file mode 100644 index 0000000000..5c516f93ec --- /dev/null +++ b/test/harry/main/org/apache/cassandra/harry/sut/DoubleWritingSut.java @@ -0,0 +1,60 @@ +/* + * 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.sut; + +import java.util.concurrent.CompletableFuture; +import java.util.regex.Pattern; + +public class DoubleWritingSut implements SystemUnderTest +{ + private final SystemUnderTest primary; + private final SystemUnderTest secondary; + + public DoubleWritingSut(SystemUnderTest primary, + SystemUnderTest secondary) + { + this.primary = primary; + this.secondary = secondary; + } + public boolean isShutdown() + { + return primary.isShutdown(); + } + + public void shutdown() + { + primary.shutdown(); + } + + private static final Pattern pattern = Pattern.compile("^select", Pattern.CASE_INSENSITIVE); + + public Object[][] execute(String statement, ConsistencyLevel cl, Object... bindings) + { + if (pattern.matcher(statement).find()) + return primary.execute(statement, cl, bindings); + + secondary.execute(statement, cl, bindings); + return primary.execute(statement, cl, bindings); + } + + public CompletableFuture executeAsync(String statement, ConsistencyLevel cl, Object... bindings) + { + throw new UnsupportedOperationException("Not implemented (yet)"); + } +} diff --git a/test/harry/main/org/apache/cassandra/harry/sut/PrintlnSut.java b/test/harry/main/org/apache/cassandra/harry/sut/PrintlnSut.java new file mode 100644 index 0000000000..8d9196370a --- /dev/null +++ b/test/harry/main/org/apache/cassandra/harry/sut/PrintlnSut.java @@ -0,0 +1,67 @@ +/* + * 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.sut; + +import java.util.Arrays; +import java.util.concurrent.CompletableFuture; + +import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.annotation.JsonTypeName; +import org.apache.cassandra.harry.core.Configuration; + +public class PrintlnSut implements SystemUnderTest +{ + public boolean isShutdown() + { + return false; + } + + public void shutdown() + { + } + + public Object[][] execute(String statement, ConsistencyLevel cl, Object... bindings) + { + System.out.println(String.format("%s | %s | %s", + statement, + cl, + Arrays.toString(bindings))); + return new Object[0][]; + } + + public CompletableFuture executeAsync(String statement, ConsistencyLevel cl, Object... bindings) + { + return CompletableFuture.supplyAsync(() -> execute(statement, cl, bindings), + Runnable::run); + } + + @JsonTypeName("println") + public static class PrintlnSutConfiguration implements Configuration.SutConfiguration + { + @JsonCreator + public PrintlnSutConfiguration() + { + + } + public SystemUnderTest make() + { + return new PrintlnSut(); + } + } +} diff --git a/test/harry/main/org/apache/cassandra/harry/sut/QueryModifyingSut.java b/test/harry/main/org/apache/cassandra/harry/sut/QueryModifyingSut.java new file mode 100644 index 0000000000..aae913d8ed --- /dev/null +++ b/test/harry/main/org/apache/cassandra/harry/sut/QueryModifyingSut.java @@ -0,0 +1,80 @@ +/* + * 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.sut; + +import java.util.concurrent.CompletableFuture; + +import org.apache.cassandra.harry.model.AgainstSutChecker; + +/** + * Best thing for sanity checking of tricky range tombstone issues: + * write to memtable instead of writing to the usual system under test. + * + * Usually used in conjunction with {@link AgainstSutChecker}: + * + * SchemaSpec doubleWriteSchema = schema.cloneWithName(schema.keyspace, schema.keyspace + "_debug"); + * + * sut.schemaChange(doubleWriteSchema.compile().cql()); + * + * QueryModifyingSut sut = new QueryModifyingSut(this.sut, + * schema.table, + * doubleWriteSchema.table); + * + * + * Model model = new AgainstSutChecker(tracker, history.clock(), sut, schema, doubleWriteSchema); + */ +public class QueryModifyingSut implements SystemUnderTest +{ + private final SystemUnderTest delegate; + private final String toReplace; + private final String replacement; + + public QueryModifyingSut(SystemUnderTest delegate, + String toReplace, + String replacement) + { + this.delegate = delegate; + this.toReplace = toReplace; + this.replacement = replacement; + } + + public boolean isShutdown() + { + return delegate.isShutdown(); + } + + public void shutdown() + { + delegate.shutdown(); + } + + public Object[][] execute(String statement, ConsistencyLevel cl, Object... bindings) + { + return delegate.execute(statement.replaceAll(toReplace, replacement), + cl, + bindings); + } + + public CompletableFuture executeAsync(String statement, ConsistencyLevel cl, Object... bindings) + { + return delegate.executeAsync(statement.replaceAll(toReplace, replacement), + cl, + bindings); + } +} diff --git a/test/harry/main/org/apache/cassandra/harry/sut/SystemUnderTest.java b/test/harry/main/org/apache/cassandra/harry/sut/SystemUnderTest.java new file mode 100644 index 0000000000..bc96e07b72 --- /dev/null +++ b/test/harry/main/org/apache/cassandra/harry/sut/SystemUnderTest.java @@ -0,0 +1,105 @@ +/* + * 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.sut; + +import java.util.concurrent.CompletableFuture; + +import org.apache.cassandra.harry.operations.CompiledStatement; + +public interface SystemUnderTest +{ + public interface SUTFactory + { + public SystemUnderTest make(); + } + + public boolean isShutdown(); + + public void shutdown(); + + default void afterSchemaInit() + { + } + + default void schemaChange(String statement) + { + execute(statement, ConsistencyLevel.ALL, new Object[]{}); + } + + Object[][] execute(String statement, ConsistencyLevel cl, Object... bindings); + + default Object[][] execute(CompiledStatement statement, ConsistencyLevel cl) + { + return execute(statement.cql(), cl, statement.bindings()); + } + + default Object[][] executeIdempotent(String statement, ConsistencyLevel cl, Object... bindings) + { + return execute(statement, cl, bindings); + } + + CompletableFuture executeAsync(String statement, ConsistencyLevel cl, Object... bindings); + + default CompletableFuture executeIdempotentAsync(String statement, ConsistencyLevel cl, Object... bindings) + { + return executeAsync(statement, cl, bindings); + } + + interface SystemUnderTestFactory + { + SystemUnderTest create(); + } + + enum ConsistencyLevel { + ALL, QUORUM, NODE_LOCAL, ONE + } + + public static final SystemUnderTest NO_OP = new NoOpSut(); + + public class NoOpSut implements SystemUnderTest + { + private NoOpSut() {} + public boolean isShutdown() + { + return false; + } + + public void shutdown() + { + } + + public Object[][] execute(String statement, ConsistencyLevel cl, Object... bindings) + { + return new Object[0][]; + } + + public CompletableFuture executeAsync(String statement, ConsistencyLevel cl, Object... bindings) + { + return CompletableFuture.supplyAsync(() -> execute(statement, cl, bindings), + Runnable::run); + } + } + + public static interface FaultInjectingSut extends SystemUnderTest + { + public Object[][] executeWithWriteFailure(String statement, ConsistencyLevel cl, Object... bindings); + public CompletableFuture executeAsyncWithWriteFailure(String statement, ConsistencyLevel cl, Object... bindings); + } + +} \ No newline at end of file diff --git a/test/harry/main/org/apache/cassandra/harry/sut/TokenPlacementModel.java b/test/harry/main/org/apache/cassandra/harry/sut/TokenPlacementModel.java new file mode 100644 index 0000000000..32edffb3fe --- /dev/null +++ b/test/harry/main/org/apache/cassandra/harry/sut/TokenPlacementModel.java @@ -0,0 +1,891 @@ +/* + * 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.sut; + +import java.net.InetAddress; +import java.net.UnknownHostException; +import java.util.*; +import java.util.function.Function; + +import org.apache.cassandra.dht.Murmur3Partitioner; +import org.apache.cassandra.distributed.api.TokenSupplier; +import org.apache.cassandra.harry.gen.rng.PCGFastPure; +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; + +public class TokenPlacementModel +{ + 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 asMap(); + + public ReplicatedRanges replicate(List nodes) + { + return replicate(toRanges(nodes), nodes); + } + public abstract ReplicatedRanges replicate(Range[] ranges, List nodes); + } + + public static class ReplicatedRanges + { + public final Range[] ranges; + public final NavigableMap> placementsForRange; + + public ReplicatedRanges(Range[] ranges, NavigableMap> placementsForRange) + { + this.ranges = ranges; + this.placementsForRange = placementsForRange; + } + + public List 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> asMap() + { + return placementsForRange; + } + + private static int indexedBinarySearch(T[] arr, CompareTo 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 + { + int compareTo(V v); + } + + public static void addIfUnique(List nodes, Set 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 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 + */ + public static Range[] toRanges(List nodes) + { + List tokens = new ArrayList<>(); + for (Node node : nodes) + tokens.add(node.token()); + tokens.add(Long.MIN_VALUE); + tokens.sort(Long::compareTo); + + Range[] ranges = new Range[nodes.size() + 1]; + long prev = tokens.get(0); + int cnt = 0; + for (int i = 1; i < tokens.size(); i++) + { + long current = tokens.get(i); + ranges[cnt++] = new Range(prev, current); + prev = current; + } + ranges[ranges.length - 1] = new Range(prev, Long.MIN_VALUE); + return ranges; + + } + + public static List peerStateToNodes(Object[][] resultset) + { + List nodes = new ArrayList<>(); + for (Object[] row : resultset) + { + InetAddress address = (InetAddress) row[0]; + Set tokens = (Set) row[1]; + String dc = (String) row[2]; + String rack = (String) row[3]; + for (String token : tokens) + { + nodes.add(new Node(0, 0, 0, 0, + constantLookup(address.toString(), + Long.parseLong(token), + dc, + rack))); + } + } + return nodes; + } + + public static class NtsReplicationFactor extends ReplicationFactor + { + private final Lookup lookup = new DefaultLookup(); + private KeyspaceParams keyspaceParams; + private final Map map; + + public NtsReplicationFactor(int... nodesPerDc) + { + super(total(nodesPerDc)); + this.map = toMap(nodesPerDc, lookup); + } + + public NtsReplicationFactor(int dcs, int nodesPerDc) + { + super(dcs * nodesPerDc); + int[] counts = new int[dcs]; + Arrays.fill(counts, nodesPerDc); + this.map = toMap(counts, lookup); + } + + public NtsReplicationFactor(Map m) + { + super(m.values().stream().reduce(0, Integer::sum)); + this.map = m; + } + + private static int total(int... num) + { + int tmp = 0; + for (int i : num) + tmp += i; + return tmp; + } + + public int dcs() + { + return map.size(); + } + + public KeyspaceParams asKeyspaceParams() + { + if (this.keyspaceParams == null) + this.keyspaceParams = toKeyspaceParams(); + return this.keyspaceParams; + } + + public Map asMap() + { + return this.map; + } + + public ReplicatedRanges replicate(Range[] ranges, List nodes) + { + return replicate(ranges, nodes, asMap()); + } + + private static > void assertStrictlySorted(Collection coll) + { + if (coll.size() <= 1) return; + + Iterator 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 nodes, Map rfs) + { + assertStrictlySorted(nodes); + Map template = new HashMap<>(); + + Map> nodesByDC = nodesByDC(nodes); + Map> racksByDC = racksByDC(nodes); + + for (Map.Entry entry : rfs.entrySet()) + { + String dc = entry.getKey(); + int rf = entry.getValue(); + List nodesInThisDC = nodesByDC.get(dc); + Set 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>> replication = new TreeMap<>(); + + for (Range range : ranges) + { + final int idx = primaryReplica(nodes, range); + int cnt = 0; + if (idx >= 0) + { + int dcsToFill = template.size(); + + Map nodesInDCs = new HashMap<>(); + for (Map.Entry 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); + } + + /** + * Replicate ranges to rf nodes. + */ + private static ReplicatedRanges combine(NavigableMap>> orig) + { + + Range[] ranges = new Range[orig.size()]; + int idx = 0; + NavigableMap> flattened = new TreeMap<>(); + for (Map.Entry>> e : orig.entrySet()) + { + List placementsForRange = new ArrayList<>(); + for (List v : e.getValue().values()) + placementsForRange.addAll(v); + ranges[idx++] = e.getKey(); + flattened.put(e.getKey(), placementsForRange); + } + return new ReplicatedRanges(ranges, flattened); + } + + private KeyspaceParams toKeyspaceParams() + { + Object[] args = new Object[map.size() * 2]; + int i = 0; + for (Map.Entry e : map.entrySet()) + { + args[i * 2] = e.getKey(); + args[i * 2 + 1] = e.getValue(); + i++; + } + + return KeyspaceParams.nts(args); + } + + private Map toMap(int[] nodesPerDc, Lookup lookup) + { + Map 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() + + '}'; + } + } + + private static final class DatacenterNodes + { + private final List nodes = new ArrayList<>(); + private final Set 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; + } + + @Override + public String toString() + { + return "DatacenterNodes{" + + "nodes=" + nodes + + ", racks=" + racks + + ", rfLeft=" + rfLeft + + ", acceptableRackRepeats=" + acceptableRackRepeats + + '}'; + } + } + + private static , T1, T2> Map mapValues(Map allDCs, Function map) + { + NavigableMap res = new TreeMap<>(); + for (Map.Entry e : allDCs.entrySet()) + { + res.put(e.getKey(), map.apply(e.getValue())); + } + return res; + } + + public static Map> nodesByDC(List nodes) + { + Map> nodesByDC = new HashMap<>(); + for (Node node : nodes) + nodesByDC.computeIfAbsent(node.dc(), (k) -> new ArrayList<>()).add(node); + + return nodesByDC; + } + + public static Map dcLayout(List nodes) + { + Map> nodesByDC = nodesByDC(nodes); + Map layout = new HashMap<>(); + for (Map.Entry> e : nodesByDC.entrySet()) + layout.put(e.getKey(), e.getValue().size()); + + return layout; + } + + public static Map> racksByDC(List nodes) + { + Map> racksByDC = new HashMap<>(); + for (Node node : nodes) + racksByDC.computeIfAbsent(node.dc(), (k) -> new HashSet<>()).add(node.rack()); + + return racksByDC; + } + + + 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 asMap() + { + return Collections.singletonMap(lookup.dc(1), total()); + } + + public ReplicatedRanges replicate(Range[] ranges, List nodes) + { + return replicate(ranges, nodes, total()); + } + + public static ReplicatedRanges replicate(Range[] ranges, List nodes, int rf) + { + NavigableMap> replication = new TreeMap<>(); + for (Range range : ranges) + { + Set names = new HashSet<>(); + List 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() + + '}'; + } + } + + /** + * A Range is responsible for the tokens between (start, end]. + */ + public static class Range implements Comparable + { + 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); + long token(int tokenIdx); + Lookup forceToken(int tokenIdx, long token); + void reset(); + + default NodeId nodeId(int nodeIdx) + { + return ClusterMetadata.current().directory.peerId(addr(nodeIdx)); + } + + default InetAddressAndPort addr(int idx) + { + try + { + return InetAddressAndPort.getByName(id(idx)); + } + catch (UnknownHostException e) + { + throw new RuntimeException(e); + } + } + } + + public static Lookup constantLookup(String id, long token, String dc, String rack) + { + return new Lookup() + { + public String id(int nodeIdx) + { + return id; + } + + public String dc(int dcIdx) + { + return dc; + } + + public String rack(int rackIdx) + { + return rack; + } + + @Override + public org.apache.cassandra.tcm.membership.NodeId nodeId(int nodeIdx) + { + return null; + } + + public long token(int tokenIdx) + { + return token; + } + + public Lookup forceToken(int tokenIdx, long token) + { + throw new UnsupportedOperationException(); + } + + @Override + public InetAddressAndPort addr(int idx) + { + return null; + } + + public void reset() + { + throw new UnsupportedOperationException(); + } + }; + } + + public static class DefaultLookup implements Lookup + { + protected final Map overrides = new HashMap<>(2); + + public String id(int nodeIdx) + { + return String.format("127.0.%d.%d", nodeIdx / 256, nodeIdx % 256); + } + + 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 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 tokens(int i) + { + return Collections.singletonList(Long.toString(lookup.token(i))); + } + } + + public static class Node implements Comparable + { + private final int tokenIdx; + private final int nodeIdx; + private final int dcIdx; + private final int rackIdx; + private final Lookup lookup; + + public 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 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 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 Murmur3Partitioner.LongToken longToken() + { + return new Murmur3Partitioner.LongToken(token()); + } + + public NodeId nodeId() + { + return lookup.nodeId(idx()); + } + + public InetAddressAndPort addr() + { + return lookup.addr(idx()); + } + + 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()); + } + } +} diff --git a/test/distributed/org/apache/cassandra/distributed/harry/ClusterState.java b/test/harry/main/org/apache/cassandra/harry/sut/injvm/ClusterState.java similarity index 94% rename from test/distributed/org/apache/cassandra/distributed/harry/ClusterState.java rename to test/harry/main/org/apache/cassandra/harry/sut/injvm/ClusterState.java index 3cb3c7661b..97886edda8 100644 --- a/test/distributed/org/apache/cassandra/distributed/harry/ClusterState.java +++ b/test/harry/main/org/apache/cassandra/harry/sut/injvm/ClusterState.java @@ -16,7 +16,7 @@ * limitations under the License. */ -package org.apache.cassandra.distributed.harry; +package org.apache.cassandra.harry.sut.injvm; public interface ClusterState { diff --git a/test/distributed/org/apache/cassandra/distributed/harry/ExistingClusterSUT.java b/test/harry/main/org/apache/cassandra/harry/sut/injvm/ExistingClusterSUT.java similarity index 95% rename from test/distributed/org/apache/cassandra/distributed/harry/ExistingClusterSUT.java rename to test/harry/main/org/apache/cassandra/harry/sut/injvm/ExistingClusterSUT.java index 9037dd92cf..27d0c97ab6 100644 --- a/test/distributed/org/apache/cassandra/distributed/harry/ExistingClusterSUT.java +++ b/test/harry/main/org/apache/cassandra/harry/sut/injvm/ExistingClusterSUT.java @@ -16,16 +16,16 @@ * limitations under the License. */ -package org.apache.cassandra.distributed.harry; +package org.apache.cassandra.harry.sut.injvm; import java.util.concurrent.CompletableFuture; import java.util.concurrent.TimeUnit; import com.google.common.util.concurrent.Uninterruptibles; -import harry.core.Configuration; -import harry.model.sut.SystemUnderTest; import org.apache.cassandra.distributed.api.ICluster; +import org.apache.cassandra.harry.core.Configuration; +import org.apache.cassandra.harry.sut.SystemUnderTest; public class ExistingClusterSUT implements Configuration.SutConfiguration { diff --git a/test/harry/main/org/apache/cassandra/harry/sut/injvm/InJVMTokenAwareVisitExecutor.java b/test/harry/main/org/apache/cassandra/harry/sut/injvm/InJVMTokenAwareVisitExecutor.java new file mode 100644 index 0000000000..6d7f482e03 --- /dev/null +++ b/test/harry/main/org/apache/cassandra/harry/sut/injvm/InJVMTokenAwareVisitExecutor.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.harry.sut.injvm; + +import java.util.ArrayList; +import java.util.List; +import java.util.concurrent.TimeUnit; +import java.util.function.Function; + +import com.google.common.util.concurrent.Uninterruptibles; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.annotation.JsonProperty; +import com.fasterxml.jackson.annotation.JsonTypeName; +import org.apache.cassandra.harry.core.Run; +import org.apache.cassandra.harry.ddl.SchemaSpec; +import org.apache.cassandra.harry.sut.SystemUnderTest; +import org.apache.cassandra.harry.sut.TokenPlacementModel; +import org.apache.cassandra.harry.operations.CompiledStatement; +import org.apache.cassandra.harry.util.ByteUtils; +import org.apache.cassandra.harry.util.TokenUtil; +import org.apache.cassandra.harry.visitors.GeneratingVisitor; +import org.apache.cassandra.harry.visitors.LoggingVisitor; +import org.apache.cassandra.harry.visitors.OperationExecutor; +import org.apache.cassandra.harry.visitors.VisitExecutor; +import org.apache.cassandra.harry.visitors.Visitor; +import org.apache.cassandra.distributed.api.ConsistencyLevel; +import org.apache.cassandra.distributed.api.IInstance; + +import static org.apache.cassandra.harry.sut.TokenPlacementModel.peerStateToNodes; + +public class InJVMTokenAwareVisitExecutor extends LoggingVisitor.LoggingVisitorExecutor +{ + private static final Logger logger = LoggerFactory.getLogger(InJVMTokenAwareVisitExecutor.class); + + private final InJvmSut sut; + private final TokenPlacementModel.ReplicationFactor rf; + private final SystemUnderTest.ConsistencyLevel cl; + private final SchemaSpec schema; + private final int MAX_RETRIES = 10; + + public static Function factory(OperationExecutor.RowVisitorFactory rowVisitorFactory, + SystemUnderTest.ConsistencyLevel cl, + TokenPlacementModel.ReplicationFactor rf) + { + return (run) -> new InJVMTokenAwareVisitExecutor(run, rowVisitorFactory, cl, rf); + } + + public InJVMTokenAwareVisitExecutor(Run run, + OperationExecutor.RowVisitorFactory rowVisitorFactory, + SystemUnderTest.ConsistencyLevel cl, + TokenPlacementModel.ReplicationFactor rf) + { + super(run, rowVisitorFactory.make(run)); + this.sut = (InJvmSut) run.sut; + this.schema = run.schemaSpec; + this.cl = cl; + this.rf = rf; + } + + @Override + protected Object[][] executeWithRetries(long lts, long pd, CompiledStatement statement) + { + if (sut.isShutdown()) + throw new IllegalStateException("System under test is shut down"); + + int retries = 0; + + Object[] pk = schema.inflatePartitionKey(pd); + List replicas = getRing().replicasFor(TokenUtil.token(ByteUtils.compose(ByteUtils.objectsToBytes(pk)))); + while (retries++ < MAX_RETRIES) + { + try + { + TokenPlacementModel.Node replica = replicas.get((int) (lts % replicas.size())); + if (cl == SystemUnderTest.ConsistencyLevel.NODE_LOCAL) + { + return executeNodeLocal(statement.cql(), replica, statement.bindings()); + } + else + { + return sut.cluster + .stream() + .filter((n) -> n.config().broadcastAddress().toString().contains(replica.id())) + .findFirst() + .get() + .coordinator() + .execute(statement.cql(), InJvmSut.toApiCl(cl), statement.bindings()); + } + } + catch (Throwable t) + { + int delaySecs = 1; + logger.error(String.format("Caught message while trying to execute: %s. Scheduled to retry in %s seconds", statement, delaySecs), t); + Uninterruptibles.sleepUninterruptibly(delaySecs, TimeUnit.SECONDS); + } + } + throw new IllegalStateException(String.format("Can not execute statement %s after %d retries", statement, retries)); + } + + protected TokenPlacementModel.ReplicatedRanges getRing() + { + List other = peerStateToNodes(sut.cluster.coordinator(1).execute("select peer, tokens, data_center, rack from system.peers", ConsistencyLevel.ONE)); + List self = peerStateToNodes(sut.cluster.coordinator(1).execute("select broadcast_address, tokens, data_center, rack from system.local", ConsistencyLevel.ONE)); + List all = new ArrayList<>(); + all.addAll(self); + all.addAll(other); + all.sort(TokenPlacementModel.Node::compareTo); + return rf.replicate(all); + } + + protected Object[][] executeNodeLocal(String statement, TokenPlacementModel.Node node, Object... bindings) + { + IInstance instance = sut.cluster + .stream() + .filter((n) -> n.config().broadcastAddress().toString().contains(node.id())) + .findFirst() + .get(); + return instance.executeInternal(statement, bindings); + } + + + @JsonTypeName("in_jvm_token_aware") + public static class Configuration implements org.apache.cassandra.harry.core.Configuration.VisitorConfiguration + { + public final org.apache.cassandra.harry.core.Configuration.RowVisitorConfiguration row_visitor; + public final SystemUnderTest.ConsistencyLevel consistency_level; + public final int rf; + @JsonCreator + public Configuration(@JsonProperty("row_visitor") org.apache.cassandra.harry.core.Configuration.RowVisitorConfiguration rowVisitor, + @JsonProperty("consistency_level") SystemUnderTest.ConsistencyLevel consistencyLevel, + @JsonProperty("rf") int rf) + { + this.row_visitor = rowVisitor; + this.consistency_level = consistencyLevel; + this.rf = rf; + } + + @Override + public Visitor make(Run run) + { + return new GeneratingVisitor(run, new InJVMTokenAwareVisitExecutor(run, row_visitor, consistency_level, new TokenPlacementModel.SimpleReplicationFactor(rf))); + } + } +} \ No newline at end of file diff --git a/test/distributed/org/apache/cassandra/distributed/fuzz/InJvmSut.java b/test/harry/main/org/apache/cassandra/harry/sut/injvm/InJvmSut.java similarity index 91% rename from test/distributed/org/apache/cassandra/distributed/fuzz/InJvmSut.java rename to test/harry/main/org/apache/cassandra/harry/sut/injvm/InJvmSut.java index 0ba0206afd..16f5e13436 100644 --- a/test/distributed/org/apache/cassandra/distributed/fuzz/InJvmSut.java +++ b/test/harry/main/org/apache/cassandra/harry/sut/injvm/InJvmSut.java @@ -16,7 +16,7 @@ * limitations under the License. */ -package org.apache.cassandra.distributed.fuzz; +package org.apache.cassandra.harry.sut.injvm; import java.io.File; import java.io.IOException; @@ -32,7 +32,6 @@ import org.slf4j.LoggerFactory; import com.fasterxml.jackson.annotation.JsonCreator; import com.fasterxml.jackson.annotation.JsonProperty; import com.fasterxml.jackson.annotation.JsonTypeName; -import harry.core.Configuration; import org.apache.cassandra.config.DatabaseDescriptor; import org.apache.cassandra.db.Keyspace; import org.apache.cassandra.dht.Token; @@ -45,11 +44,6 @@ import org.apache.cassandra.tcm.ClusterMetadata; public class InJvmSut extends InJvmSutBase { - public static void init() - { - Configuration.registerSubtypes(InJvmSutConfiguration.class); - } - private static final Logger logger = LoggerFactory.getLogger(InJvmSut.class); public InJvmSut(Cluster cluster) @@ -57,9 +51,9 @@ public class InJvmSut extends InJvmSutBase super(cluster, roundRobin(cluster), retryOnTimeout(), 10); } - public InJvmSut(Cluster cluster, Supplier lbs, Function retryStrategy) + public InJvmSut(Cluster cluster, Supplier loadBalancingStrategy, Function retryStrategy) { - super(cluster, lbs, retryStrategy, 10); + super(cluster, loadBalancingStrategy, retryStrategy, 10); } @JsonTypeName("in_jvm") @@ -78,8 +72,8 @@ public class InJvmSut extends InJvmSutBase try { return Cluster.build().withConfig(cfg) - .withNodes(nodes) - .withRoot(root) + .withNodes(nodes) + .withRoot(root) .createWithoutStarting(); } catch (IOException e) diff --git a/test/distributed/org/apache/cassandra/distributed/fuzz/InJvmSutBase.java b/test/harry/main/org/apache/cassandra/harry/sut/injvm/InJvmSutBase.java similarity index 87% rename from test/distributed/org/apache/cassandra/distributed/fuzz/InJvmSutBase.java rename to test/harry/main/org/apache/cassandra/harry/sut/injvm/InJvmSutBase.java index 64a961e3a8..d024688253 100644 --- a/test/distributed/org/apache/cassandra/distributed/fuzz/InJvmSutBase.java +++ b/test/harry/main/org/apache/cassandra/harry/sut/injvm/InJvmSutBase.java @@ -16,7 +16,7 @@ * limitations under the License. */ -package org.apache.cassandra.distributed.fuzz; +package org.apache.cassandra.harry.sut.injvm; import java.io.File; import java.io.IOException; @@ -40,22 +40,16 @@ import org.slf4j.LoggerFactory; import com.fasterxml.jackson.annotation.JsonCreator; import com.fasterxml.jackson.annotation.JsonProperty; -import harry.core.Configuration; -import harry.model.sut.SystemUnderTest; -import org.apache.cassandra.distributed.api.Feature; import org.apache.cassandra.distributed.api.ICluster; import org.apache.cassandra.distributed.api.IInstance; import org.apache.cassandra.distributed.api.IInstanceConfig; import org.apache.cassandra.distributed.api.IMessage; import org.apache.cassandra.distributed.api.IMessageFilters; +import org.apache.cassandra.harry.core.Configuration; +import org.apache.cassandra.harry.sut.SystemUnderTest; public class InJvmSutBase> implements SystemUnderTest.FaultInjectingSut { - public static void init() - { - Configuration.registerSubtypes(InJvmSutBaseConfiguration.class); - } - private static final Logger logger = LoggerFactory.getLogger(InJvmSutBase.class); private final ExecutorService executor; @@ -275,23 +269,7 @@ public class InJvmSutBase CLUSTER cluster; - cluster = cluster((cfg) -> { - // TODO: make this configurable - cfg.with(Feature.NETWORK, Feature.GOSSIP, Feature.NATIVE_PROTOCOL) - .set("row_cache_size_in_mb", 10L) - .set("index_summary_capacity_in_mb", 10L) - .set("counter_cache_size_in_mb", 10L) - .set("key_cache_size_in_mb", 10L) - .set("file_cache_size_in_mb", 10) - .set("memtable_heap_space_in_mb", 128) - .set("memtable_offheap_space_in_mb", 128) - .set("memtable_flush_writers", 1) - .set("concurrent_compactors", 1) - .set("concurrent_reads", 5) - .set("concurrent_writes", 5) - .set("compaction_throughput_mb_per_sec", 10) - .set("hinted_handoff_enabled", false); - }, + cluster = cluster(defaultConfig(), nodes, new File(root)); @@ -300,6 +278,26 @@ public class InJvmSutBase } } + public static Consumer defaultConfig() + { + return (cfg) -> { + cfg.set("row_cache_size", "50MiB") + .set("index_summary_capacity", "50MiB") + .set("counter_cache_size", "50MiB") + .set("key_cache_size", "50MiB") + .set("file_cache_size", "50MiB") + .set("index_summary_capacity", "50MiB") + .set("memtable_heap_space", "128MiB") + .set("memtable_offheap_space", "128MiB") + .set("memtable_flush_writers", 1) + .set("concurrent_compactors", 1) + .set("concurrent_reads", 5) + .set("concurrent_writes", 5) + .set("compaction_throughput_mb_per_sec", 10) + .set("hinted_handoff_enabled", false); + }; + } + public static org.apache.cassandra.distributed.api.ConsistencyLevel toApiCl(ConsistencyLevel cl) { switch (cl) diff --git a/test/harry/main/org/apache/cassandra/harry/sut/injvm/QuiescentLocalStateChecker.java b/test/harry/main/org/apache/cassandra/harry/sut/injvm/QuiescentLocalStateChecker.java new file mode 100644 index 0000000000..c8d24f28a1 --- /dev/null +++ b/test/harry/main/org/apache/cassandra/harry/sut/injvm/QuiescentLocalStateChecker.java @@ -0,0 +1,87 @@ +/* + * 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.sut.injvm; + +import java.util.ArrayList; +import java.util.List; + +import org.apache.cassandra.harry.core.Run; +import org.apache.cassandra.harry.ddl.SchemaSpec; +import org.apache.cassandra.harry.model.OpSelectors; +import org.apache.cassandra.harry.model.QuiescentLocalStateCheckerBase; +import org.apache.cassandra.harry.model.reconciler.Reconciler; +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.util.ByteUtils; +import org.apache.cassandra.harry.util.TokenUtil; +import org.apache.cassandra.distributed.api.ConsistencyLevel; +import org.apache.cassandra.distributed.api.IInstance; + +public class QuiescentLocalStateChecker extends QuiescentLocalStateCheckerBase +{ + public static ModelFactory factory(TokenPlacementModel.ReplicationFactor rf) + { + return (run) -> new QuiescentLocalStateChecker(run, rf); + } + + public QuiescentLocalStateChecker(Run run, TokenPlacementModel.ReplicationFactor rf) + { + super(run, rf); + } + + public QuiescentLocalStateChecker(OpSelectors.Clock clock, + OpSelectors.PdSelector pdSelector, + SystemUnderTest sut, + DataTracker tracker, + SchemaSpec schema, + Reconciler reconciler, + TokenPlacementModel.ReplicationFactor rf) + { + super(clock, pdSelector, sut, tracker, schema, reconciler, rf); + } + + @Override + protected TokenPlacementModel.ReplicatedRanges getRing() + { + List other = TokenPlacementModel.peerStateToNodes(((InJvmSutBase) sut).cluster.coordinator(1).execute("select peer, tokens, data_center, rack from system.peers", ConsistencyLevel.ONE)); + List self = TokenPlacementModel.peerStateToNodes(((InJvmSutBase) sut).cluster.coordinator(1).execute("select broadcast_address, tokens, data_center, rack from system.local", ConsistencyLevel.ONE)); + List all = new ArrayList<>(); + all.addAll(self); + all.addAll(other); + all.sort(TokenPlacementModel.Node::compareTo); + return rf.replicate(all); + } + + @Override + protected Object[][] executeNodeLocal(String statement, TokenPlacementModel.Node node, Object... bindings) + { + IInstance instance = ((InJvmSutBase) sut).cluster + .stream() + .filter((n) -> n.config().broadcastAddress().toString().contains(node.id())) + .findFirst() + .get(); + return instance.executeInternal(statement, bindings); + } + + protected long token(long pd) + { + return TokenUtil.token(ByteUtils.compose(ByteUtils.objectsToBytes(schema.inflatePartitionKey(pd)))); + } +} diff --git a/test/harry/main/org/apache/cassandra/harry/tracker/DataTracker.java b/test/harry/main/org/apache/cassandra/harry/tracker/DataTracker.java new file mode 100644 index 0000000000..86c7bca613 --- /dev/null +++ b/test/harry/main/org/apache/cassandra/harry/tracker/DataTracker.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.harry.tracker; + +import java.util.function.LongConsumer; + +import org.apache.cassandra.harry.core.Configuration; +import org.apache.cassandra.harry.ddl.SchemaSpec; +import org.apache.cassandra.harry.model.OpSelectors; + +public interface DataTracker +{ + void onLtsStarted(LongConsumer onLts); + void onLtsFinished(LongConsumer onLts); + + void beginModification(long lts); + void endModification(long lts); + + default void beginValidation(long pd) {} + default void endValidation(long pd) {} + + long maxStarted(); + boolean isFinished(long lts); + + Configuration.DataTrackerConfiguration toConfig(); + + interface DataTrackerFactory + { + DataTracker make(OpSelectors.PdSelector pdSelector, SchemaSpec schemaSpec); + } + + DataTracker NO_OP = new NoOpDataTracker(); + + class NoOpDataTracker implements DataTracker + { + private NoOpDataTracker() {} + + public void onLtsStarted(LongConsumer onLts){} + public void onLtsFinished(LongConsumer onLts){} + + public void beginModification(long lts){} + public void endModification(long lts){} + + public long maxStarted() { return 0; } + public boolean isFinished(long lts) { return false; } + + public Configuration.DataTrackerConfiguration toConfig(){ return null; } + } + +} diff --git a/test/harry/main/org/apache/cassandra/harry/tracker/DefaultDataTracker.java b/test/harry/main/org/apache/cassandra/harry/tracker/DefaultDataTracker.java new file mode 100644 index 0000000000..2d6eae610a --- /dev/null +++ b/test/harry/main/org/apache/cassandra/harry/tracker/DefaultDataTracker.java @@ -0,0 +1,193 @@ +/* + * 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.tracker; + +import java.util.ArrayList; +import java.util.List; +import java.util.concurrent.PriorityBlockingQueue; +import java.util.concurrent.atomic.AtomicLong; +import java.util.function.LongConsumer; + +import org.apache.cassandra.harry.core.Configuration; +import com.google.common.annotations.VisibleForTesting; +import org.apache.cassandra.utils.concurrent.WaitQueue; + +public class DefaultDataTracker implements DataTracker +{ + protected final AtomicLong maxSeenLts; + protected final AtomicLong maxCompleteLts; + protected final PriorityBlockingQueue reorderBuffer; + protected final DrainReorderQueueTask reorderTask; + + protected List onStarted = new ArrayList<>(); + protected List onFinished = new ArrayList<>(); + + public DefaultDataTracker() + { + this.maxSeenLts = new AtomicLong(-1); + this.maxCompleteLts = new AtomicLong(-1); + this.reorderBuffer = new PriorityBlockingQueue<>(100); + this.reorderTask = new DrainReorderQueueTask(); + this.reorderTask.start(); + } + + @Override + public void onLtsStarted(LongConsumer onLts) + { + this.onStarted.add(onLts); + } + + @Override + public void onLtsFinished(LongConsumer onLts) + { + this.onFinished.add(onLts); + } + + @Override + public void beginModification(long lts) + { + assert lts >= 0; + startedInternal(lts); + for (LongConsumer consumer : onStarted) + consumer.accept(lts); + } + + @Override + public void endModification(long lts) + { + finishedInternal(lts); + for (LongConsumer consumer : onFinished) + consumer.accept(lts); + } + + void startedInternal(long lts) + { + recordEvent(lts, false); + } + + void finishedInternal(long lts) + { + recordEvent(lts, true); + } + + private void recordEvent(long lts, boolean finished) + { + // all seen LTS are allowed to be "in-flight" + maxSeenLts.getAndUpdate((old) -> Math.max(lts, old)); + + if (!finished) + return; + + if (!maxCompleteLts.compareAndSet(lts - 1, lts)) + reorderBuffer.offer(lts); + + reorderTask.notify.signalAll(); + } + + private class DrainReorderQueueTask extends Thread + { + private final WaitQueue notify; + + private DrainReorderQueueTask() + { + super("DrainReorderQueueTask"); + this.notify = WaitQueue.newWaitQueue(); + } + + public void run() + { + while (!Thread.interrupted()) + { + try + { + WaitQueue.Signal signal = notify.register(); + runOnce(); + signal.awaitUninterruptibly(); + } + catch (Throwable t) + { + t.printStackTrace(); + } + } + } + + public void runOnce() + { + long maxAchievedConsecutive = maxCompleteLts.get(); + + Long smallest = reorderBuffer.peek(); + while (smallest != null && smallest == maxAchievedConsecutive + 1) + { + boolean res = maxCompleteLts.compareAndSet(maxAchievedConsecutive, smallest); + assert res : String.format("Should have exclusive access to maxCompleteLts, but someone wrote %d, while %d was expected", maxCompleteLts.get(), maxAchievedConsecutive); + maxAchievedConsecutive = smallest; + long removed = reorderBuffer.remove(); + assert smallest == removed : String.format("Tried to remove %d but removed %d", smallest, removed); + smallest = reorderBuffer.peek(); + } + } + } + + + public long maxStarted() + { + return maxSeenLts.get(); + } + + public long maxConsecutiveFinished() + { + return maxCompleteLts.get(); + } + + public boolean isFinished(long lts) + { + // Since we _first_ add the item to maxConsecutive, and only then yank it from reorderBuffer, + // it may happen that we have checked for lts against maxConsecutive while it was still in reorderBuffer + // but then, by the time we check for it in the reorderBuffer, it is already removed; + return reorderBuffer.contains(lts) || lts <= maxConsecutiveFinished(); + } + + public Configuration.DataTrackerConfiguration toConfig() + { + return new Configuration.DefaultDataTrackerConfiguration(maxSeenLts.get(), maxCompleteLts.get(), new ArrayList<>(reorderBuffer)); + } + + @VisibleForTesting + public void forceLts(long maxSeen, long maxComplete, List reorderBuffer) + { + System.out.printf("Forcing maxSeen: %d, maxComplete: %d, reorderBuffer: %s%n", maxSeen, maxComplete, reorderBuffer); + this.maxSeenLts.set(maxSeen); + this.maxCompleteLts.set(maxComplete); + if (reorderBuffer != null) + { + reorderBuffer.sort(Long::compareTo); + this.reorderBuffer.addAll(reorderBuffer); + } + } + + public String toString() + { + List buf = new ArrayList<>(reorderBuffer); + return "DataTracker{" + + "maxSeenLts=" + maxSeenLts + + ", maxCompleteLts=" + maxCompleteLts + + ", reorderBuffer=" + buf + + '}'; + } +} diff --git a/test/harry/main/org/apache/cassandra/harry/tracker/LockingDataTracker.java b/test/harry/main/org/apache/cassandra/harry/tracker/LockingDataTracker.java new file mode 100644 index 0000000000..3edcf525f5 --- /dev/null +++ b/test/harry/main/org/apache/cassandra/harry/tracker/LockingDataTracker.java @@ -0,0 +1,286 @@ +/* + * 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.tracker; + +import java.util.ArrayList; +import java.util.Map; +import java.util.Set; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.ConcurrentSkipListSet; +import java.util.concurrent.atomic.AtomicLongFieldUpdater; + +import org.apache.cassandra.harry.core.Configuration; +import org.apache.cassandra.harry.ddl.SchemaSpec; +import org.apache.cassandra.harry.model.OpSelectors; +import org.apache.cassandra.utils.concurrent.WaitQueue; + +/** + * Locking data tracker, that can be used with a quiescent model checker while providing + * a high degree of concurrency. It works by isolating readers from writers. In other words, + * readers can intersect with other readers, and writers can coincide with other writers. + * + * We achieve quiescence on a partition level, not on LTS level, and we know for sure + * which operations have finished for a partition, even if their LTS are non-contiguous. + * + * We use a simple wait queue for queuing up waiters, and a compact long counter for + * tracking the number of concurrent readers and writers. Lower 32 bits hold a number of + * readers, and higher 32 bits - a number of writers. + */ +public class LockingDataTracker extends DefaultDataTracker +{ + private final Map locked = new ConcurrentHashMap<>(); + + private final WaitQueue readersQueue = WaitQueue.newWaitQueue(); + private final WaitQueue writersQueue = WaitQueue.newWaitQueue(); + private final OpSelectors.PdSelector pdSelector; + private final SchemaSpec schemaSpec; + + private final Set readingFrom = new ConcurrentSkipListSet<>(); + private final Set writingTo = new ConcurrentSkipListSet<>(); + + public LockingDataTracker(OpSelectors.PdSelector pdSelector, SchemaSpec schemaSpec) + { + this.pdSelector = pdSelector; + this.schemaSpec = schemaSpec; + } + + @Override + public void beginModification(long lts) + { + ReadersWritersLock partitionLock = getLockForLts(lts); + partitionLock.lockForWrite(); + assert !readingFrom.contains(partitionLock.descriptor) : String.format("Reading from should not have contained %d", partitionLock.descriptor); + writingTo.add(partitionLock.descriptor); + super.beginModification(lts); + } + + @Override + public void endModification(long lts) + { + super.endModification(lts); + ReadersWritersLock partitionLock = getLockForLts(lts); + assert !readingFrom.contains(partitionLock.descriptor) : String.format("Reading from should not have contained %d", partitionLock.descriptor); + writingTo.remove(partitionLock.descriptor); + partitionLock.unlockAfterWrite(); + } + + @Override + public void beginValidation(long pd) + { + ReadersWritersLock partitionLock = getLock(pd); + partitionLock.lockForRead(); + assert !writingTo.contains(pd) : String.format("Writing to should not have contained %d", pd); + readingFrom.add(pd); + super.beginValidation(pd); + } + + @Override + public void endValidation(long pd) + { + super.endValidation(pd); + ReadersWritersLock partitionLock = getLock(pd); + assert !writingTo.contains(pd) : String.format("Writing to should not have contained %d", pd); + readingFrom.remove(partitionLock.descriptor); + partitionLock.unlockAfterRead(); + } + + public void validate(long pd, Runnable runnable) + { + ReadersWritersLock partitionLock = getLockForLts(pd); + partitionLock.lockForRead(); + runnable.run(); + partitionLock.unlockAfterRead(); + } + + private ReadersWritersLock getLockForLts(long lts) + { + long pd = pdSelector.pd(lts, schemaSpec); + return getLock(pd); + } + + private ReadersWritersLock getLock(long pd) + { + return locked.computeIfAbsent(pd, (pd_) -> new ReadersWritersLock(readersQueue, writersQueue, pd)); + } + + /** + * Readers/writers lock. It was decided not to use signals here, and instead go for a + * busyspin instead, since we expect locks to be released briefly and contention to be minimal. + */ + public static class ReadersWritersLock + { + private static final AtomicLongFieldUpdater fieldUpdater = AtomicLongFieldUpdater.newUpdater(ReadersWritersLock.class, "lock"); + private volatile long lock; + + final long descriptor; + final WaitQueue readersQueue; + final WaitQueue writersQueue; + + public ReadersWritersLock(WaitQueue readersQueue, WaitQueue writersQueue, long descriptor) + { + this.readersQueue = readersQueue; + this.writersQueue = writersQueue; + this.lock = 0L; + this.descriptor = descriptor; + } + + @Override + public String toString() + { + long lock = this.lock; + return "PartitionLock{" + + "pd = " + descriptor + + ", readers = " + getReaders(lock) + + ", writers = " + getWriters(lock) + + '}'; + } + + public void lockForWrite() + { + while (true) + { + WaitQueue.Signal signal = writersQueue.register(); + long v = lock; + if (getReaders(v) == 0) + { + if (fieldUpdater.compareAndSet(this, v, incWriters(v))) + { + signal.cancel(); + return; + } + } + signal.awaitUninterruptibly(); + } + } + + public void unlockAfterWrite() + { + while (true) + { + long v = lock; + if (fieldUpdater.compareAndSet(this, v, decWriters(v))) + { + readersQueue.signalAll(); + writersQueue.signalAll(); + return; + } + } + } + + public void lockForRead() + { + while (true) + { + WaitQueue.Signal signal = readersQueue.register(); + long v = lock; + if (getWriters(v) == 0) + { + if (fieldUpdater.compareAndSet(this, v, incReaders(v))) + { + signal.cancel(); + return; + } + } + signal.awaitUninterruptibly(); + } + } + + public boolean tryLockForRead() + { + long v = lock; + if (getWriters(v) == 0 && fieldUpdater.compareAndSet(this, v, incReaders(v))) + return true; + + return false; + } + + public void unlockAfterRead() + { + while (true) + { + long v = lock; + if (fieldUpdater.compareAndSet(this, v, decReaders(v))) + { + writersQueue.signalAll(); + readersQueue.signalAll(); + return; + } + } + } + + private long incReaders(long v) + { + long readers = getReaders(v); + assert getWriters(v) == 0; + v &= ~0x00000000ffffffffL; // erase all readers + return v | (readers + 1L); + } + + private long decReaders(long v) + { + long readers = getReaders(v); + assert getWriters(v) == 0; + assert readers >= 1; + v &= ~0x00000000ffffffffL; // erase all readers + return v | (readers - 1L); + } + + private long incWriters(long v) + { + long writers = getWriters(v); + assert getReaders(v) == 0; + v &= ~0xffffffff00000000L; // erase all writers + return v | ((writers + 1L) << 32); + } + + private long decWriters(long v) + { + long writers = getWriters(v); + assert getReaders(v) == 0; + assert writers >= 1 : "Writers left " + writers; + v &= ~0xffffffff00000000L; // erase all writers + return v | ((writers - 1L) << 32); + } + + public int getReaders(long v) + { + v &= 0xffffffffL; + return (int) v; + } + + public int getWriters(long v) + { + v >>= 32; + v &= 0xffffffffL; + return (int) v; + } + } + + @Override + public Configuration.DataTrackerConfiguration toConfig() + { + return new Configuration.LockingDataTrackerConfiguration(maxSeenLts.get(), maxCompleteLts.get(), new ArrayList<>(reorderBuffer)); + } + + @Override + public String toString() + { + return "Locking" + super.toString(); + } +} diff --git a/test/harry/main/org/apache/cassandra/harry/util/BitSet.java b/test/harry/main/org/apache/cassandra/harry/util/BitSet.java new file mode 100644 index 0000000000..d4d364f020 --- /dev/null +++ b/test/harry/main/org/apache/cassandra/harry/util/BitSet.java @@ -0,0 +1,244 @@ +/* + * 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.util; + +import java.util.function.IntConsumer; + +import org.apache.cassandra.harry.gen.Generator; +import org.apache.cassandra.harry.gen.Surjections; + +public interface BitSet +{ + static long BITSET_STREAM_ID = 0x7670411362L; + + static long bitMask(int bits) + { + if (bits <= 0) + return 0L; + else if (bits < 64) + return (1L << bits) - 1; + else + return -1L; + } + + public static BitSet allSet(int bits) + { + return new BitSet64Bit(bitMask(bits), bits); + } + + public static BitSet allUnset(int bits) + { + return new BitSet64Bit(0, bits); + } + + public static BitSet create(long value, int bits) + { + return new BitSet64Bit(value, bits); + } + + public void eachBit(BitConsumer iter); + + public void eachSetBit(IntConsumer iter); + public void eachSetBit(IntConsumer iter, BitSet mask); + + public void eachUnsetBit(IntConsumer iter); + + public boolean allUnset(); + + public boolean allUnset(BitSet mask); + + public boolean allSet(); + + public void set(int idx); + + public void unset(int idx); + + public boolean isSet(int idx); + public boolean isSet(int idx, BitSet mask); + + public static boolean isSet(long bits, int idx) + { + return (bits & (1L << idx)) != 0; + } + + public static int setCount(long bits, int size) + { + int count = 0; + for (int i = 0; i < size; i++) + { + if (BitSet.isSet(bits, i)) + count++; + } + return count; + } + + public int setCount(); + public int setCount(BitSet mask); + + public int size(); + + public BitSet clone(boolean invert); + + public class BitSet64Bit implements BitSet + { + private long bits; + // TODO: use count to check out-of-bounds + private int count; + + public BitSet64Bit(int count) + { + this(0, count); + } + + public BitSet64Bit(long value, int count) + { + this.bits = value & bitMask(count); + this.count = count; + } + + public boolean allUnset() + { + return bits == 0; + } + + public boolean allUnset(BitSet mask) + { + assert mask instanceof BitSet64Bit; + return (((BitSet64Bit) mask).bits & bits) == 0; + } + + public boolean allSet() + { + return bits == bitMask(count); + } + + public void eachBit(BitConsumer iter) + { + for (int i = 0; i < count; i++) + iter.consume(i, isSet(i)); + } + + public void eachSetBit(IntConsumer iter) + { + for (int i = 0; i < count; i++) + { + boolean isSet = isSet(i); + if (isSet) + iter.accept(i); + } + } + + public void eachSetBit(IntConsumer iter, BitSet mask) + { + assert mask instanceof BitSet64Bit; + long bits = (((BitSet64Bit) mask).bits & this.bits); + for (int i = 0; i < count; i++) + { + boolean isSet = BitSet.isSet(bits, i); + if (isSet) + iter.accept(i); + } + } + + public void eachUnsetBit(IntConsumer iter) + { + for (int i = 0; i < count; i++) + { + boolean isSet = isSet(i); + if (!isSet) + iter.accept(i); + } + } + + public void set(int idx) + { + this.bits |= (1L << idx); + } + + public void unset(int idx) + { + this.bits &= ~(1L << idx); + } + + public boolean isSet(int idx) + { + assert idx < size() : String.format("Trying to query the bit (%s) outside the range of bitset (%s)", idx, size()); + return BitSet.isSet(bits, idx); + } + + public boolean isSet(int idx, BitSet mask) + { + assert idx < size(); + assert mask instanceof BitSet64Bit; + return BitSet.isSet(bits & ((BitSet64Bit) mask).bits, idx); + } + + public int setCount() + { + return BitSet.setCount(bits, size()); + } + + public int setCount(BitSet mask) + { + assert mask instanceof BitSet64Bit; + return BitSet.setCount(bits & ((BitSet64Bit) mask).bits, size()); + } + + public int size() + { + return count; + } + + public BitSet clone(boolean invert) + { + return new BitSet64Bit(invert ? ~bits & bitMask(count) : bits, + count); + } + + public String toString() + { + String s = ""; + for (int i = 0; i < size(); i++) + { + s += isSet(i) ? "set" : "unset"; + if (i < (size() - 1)) + s += ", "; + } + + return String.format("[%s]", s); + } + } + + public interface BitConsumer + { + public void consume(int idx, boolean isSet); + } + + public static Generator generator(int length) + { + return (rng) -> { + return BitSet.create(rng.next(), length); + }; + } + + public static Surjections.Surjection surjection(int length) + { + return generator(length).toSurjection(BITSET_STREAM_ID); + } +} diff --git a/test/harry/main/org/apache/cassandra/harry/util/ByteUtils.java b/test/harry/main/org/apache/cassandra/harry/util/ByteUtils.java new file mode 100644 index 0000000000..86195421d5 --- /dev/null +++ b/test/harry/main/org/apache/cassandra/harry/util/ByteUtils.java @@ -0,0 +1,162 @@ +/* + * 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.util; + +import java.net.InetAddress; +import java.nio.ByteBuffer; +import java.nio.charset.Charset; +import java.nio.charset.StandardCharsets; +import java.util.List; +import java.util.Set; +import java.util.UUID; + +public class ByteUtils +{ + public static ByteBuffer bytes(String s) + { + return ByteBuffer.wrap(s.getBytes(StandardCharsets.UTF_8)); + } + + public static ByteBuffer bytes(String s, Charset charset) + { + return ByteBuffer.wrap(s.getBytes(charset)); + } + + public static ByteBuffer bytes(byte b) + { + return ByteBuffer.allocate(1).put(0, b); + } + + public static ByteBuffer bytes(short s) + { + return ByteBuffer.allocate(2).putShort(0, s); + } + + public static ByteBuffer bytes(int i) + { + return ByteBuffer.allocate(4).putInt(0, i); + } + + public static ByteBuffer bytes(long n) + { + return ByteBuffer.allocate(8).putLong(0, n); + } + + public static ByteBuffer bytes(float f) + { + return ByteBuffer.allocate(4).putFloat(0, f); + } + + public static ByteBuffer bytes(double d) + { + return ByteBuffer.allocate(8).putDouble(0, d); + } + + public static ByteBuffer bytes(InetAddress address) + { + return ByteBuffer.wrap(address.getAddress()); + } + + public static ByteBuffer bytes(UUID uuid) + { + return ByteBuffer.wrap(decompose(uuid)); + } + + public static byte[] decompose(UUID uuid) + { + long most = uuid.getMostSignificantBits(); + long least = uuid.getLeastSignificantBits(); + byte[] b = new byte[16]; + for (int i = 0; i < 8; i++) + { + b[i] = (byte)(most >>> ((7-i) * 8)); + b[8+i] = (byte)(least >>> ((7-i) * 8)); + } + return b; + } + + public static ByteBuffer[] objectsToBytes(Object... objects) + { + ByteBuffer[] bytes = new ByteBuffer[objects.length]; + for (int i = 0; i < objects.length; i++) + bytes[i] = objectToBytes(objects[i]); + return bytes; + } + + public static ByteBuffer objectToBytes(Object obj) + { + if (obj instanceof Integer) + return bytes((int) obj); + else if (obj instanceof Byte) + return bytes((byte) obj); + else if (obj instanceof Boolean) + return bytes( (byte) (((boolean) obj) ? 1 : 0)); + else if (obj instanceof Short) + return bytes((short) obj); + else if (obj instanceof Long) + return bytes((long) obj); + else if (obj instanceof Float) + return bytes((float) obj); + else if (obj instanceof Double) + return bytes((double) obj); + else if (obj instanceof UUID) + return bytes((UUID) obj); + else if (obj instanceof InetAddress) + return bytes((InetAddress) obj); + else if (obj instanceof String) + return bytes((String) obj); + else if (obj instanceof List) + { + throw new UnsupportedOperationException("Please use ByteUtils from integration package"); + } + else if (obj instanceof Set) + { + throw new UnsupportedOperationException("Please use ByteUtils from integration package"); + } + else if (obj instanceof ByteBuffer) + return (ByteBuffer) obj; + else + throw new IllegalArgumentException(String.format("Cannot convert value %s of type %s", + obj, + obj.getClass())); + } + + public static ByteBuffer compose(ByteBuffer... buffers) { + if (buffers.length == 1) return buffers[0]; + + int totalLength = 0; + for (ByteBuffer bb : buffers) totalLength += 2 + bb.remaining() + 1; + + ByteBuffer out = ByteBuffer.allocate(totalLength); + for (ByteBuffer buffer : buffers) { + ByteBuffer bb = buffer.duplicate(); + putShortLength(out, bb.remaining()); + out.put(bb); + out.put((byte) 0); + } + out.flip(); + return out; + } + + public static void putShortLength(ByteBuffer bb, int length) { + bb.put((byte) ((length >> 8) & 0xFF)); + bb.put((byte) (length & 0xFF)); + } + +} diff --git a/test/harry/main/org/apache/cassandra/harry/util/DescriptorRanges.java b/test/harry/main/org/apache/cassandra/harry/util/DescriptorRanges.java new file mode 100644 index 0000000000..597d7e8bad --- /dev/null +++ b/test/harry/main/org/apache/cassandra/harry/util/DescriptorRanges.java @@ -0,0 +1,150 @@ +/* + * 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.util; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.Comparator; +import java.util.List; +import java.util.Objects; +import java.util.stream.Collectors; + +// TODO: this is not really an interval tree, just two sorted arrays. However, given that ExhaustiveChecker has +// to inflate a partition every time we execute the query, we always know all boundaries at any given point in time. +public class DescriptorRanges +{ + private List sortedByMin; + + public DescriptorRanges(List ranges) + { + this.sortedByMin = new ArrayList<>(ranges); + Collections.sort(sortedByMin, Comparator.comparingLong(a -> a.minBound)); + } + + public boolean isShadowed(long cd, long lts) + { + return !shadowedBy(cd, lts).isEmpty(); + } + + public List shadowedBy(long cd, long lts) + { + List shadowedBy = new ArrayList<>(); + for (DescriptorRange range : sortedByMin) + { + if (range.minBound > cd) + break; + + if (range.contains(cd, lts)) + shadowedBy.add(range); + } + + return shadowedBy; + } + + public List newerThan(long ts) + { + return sortedByMin.stream().filter((rt) -> { + return rt.timestamp >= ts; + }).collect(Collectors.toList()); + } + + + private static int toIdx(int idxOrIP) + { + if (idxOrIP >= 0) + return idxOrIP; + + return -1 * (idxOrIP + 1); + } + + public static class DescriptorRange + { + public final long minBound; + public final long maxBound; + public final boolean minInclusive; + public final boolean maxInclusive; + + public final long timestamp; + + public DescriptorRange(long minBound, long maxBound, boolean minInclusive, boolean maxInclusive, long timestamp) + { + this.minBound = minBound; + this.maxBound = maxBound; + this.minInclusive = minInclusive; + this.maxInclusive = maxInclusive; + + this.timestamp = timestamp; + } + + public boolean contains(long descriptor) + { + if (minInclusive && maxInclusive) + return descriptor >= minBound && descriptor <= maxBound; + + if (!minInclusive && !maxInclusive) + return descriptor > minBound && descriptor < maxBound; + + if (!minInclusive && maxInclusive) + return descriptor > minBound && descriptor <= maxBound; + + assert (minInclusive && !maxInclusive); + return descriptor >= minBound && descriptor < maxBound; + } + + public boolean contains(long descriptor, long ts) + { + return contains(descriptor) && timestamp >= ts; + } + + public boolean equals(Object o) + { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + DescriptorRange range = (DescriptorRange) o; + return minBound == range.minBound && + maxBound == range.maxBound && + minInclusive == range.minInclusive && + maxInclusive == range.maxInclusive && + timestamp == range.timestamp; + } + + public int hashCode() + { + return Objects.hash(minBound, maxBound, minInclusive, maxInclusive, timestamp); + } + + public String toString() + { + return "Range{" + + "minBound=" + minBound + + ", maxBound=" + maxBound + + ", minInclusive=" + minInclusive + + ", maxInclusive=" + maxInclusive + + ", timestamp=" + timestamp + + '}'; + } + } + + public String toString() + { + return "Ranges{" + + "sortedByMin=" + sortedByMin + + '}'; + } +} diff --git a/test/harry/main/org/apache/cassandra/harry/util/StringUtils.java b/test/harry/main/org/apache/cassandra/harry/util/StringUtils.java new file mode 100644 index 0000000000..956b3a34bf --- /dev/null +++ b/test/harry/main/org/apache/cassandra/harry/util/StringUtils.java @@ -0,0 +1,35 @@ +/* + * 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.util; + +public class StringUtils +{ + public static String toString(long[] arr) + { + String s = ""; + for (int i = 0; i < arr.length; i++) + { + s += arr[i]; + s += "L"; + if (i < (arr.length - 1)) + s += ','; + } + return s; + } +} diff --git a/test/harry/main/org/apache/cassandra/harry/util/TestRunner.java b/test/harry/main/org/apache/cassandra/harry/util/TestRunner.java new file mode 100644 index 0000000000..1941137436 --- /dev/null +++ b/test/harry/main/org/apache/cassandra/harry/util/TestRunner.java @@ -0,0 +1,106 @@ +/* + * 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.util; + +import java.util.function.BiConsumer; +import java.util.function.BiFunction; +import java.util.function.Consumer; +import java.util.function.Function; +import java.util.function.Supplier; + +import org.apache.cassandra.harry.gen.Generator; +import org.apache.cassandra.harry.gen.EntropySource; + +public class TestRunner +{ + private static final int CYCLES = 100; + + protected static final EntropySource rand = EntropySource.forTests(6371747244598697093L); + + public static void test(Generator gen1, + Generator gen2, + BiConsumer validate) + { + for (int i = 0; i < CYCLES; i++) + { + validate.accept(gen1.generate(rand), + gen2.generate(rand)); + } + } + + public static void test(Generator gen1, + Function> gen2, + ThrowingConsumer validate) throws Throwable + { + test(gen1, + (v1) -> test(gen2.apply(v1), validate)); + } + + public static void test(Generator gen1, + ThrowingConsumer validate) throws Throwable + { + for (int i = 0; i < CYCLES; i++) + { + validate.accept(gen1.generate(rand)); + } + } + + public static void test(Generator visitGenerator, + Supplier initializeModel, + Supplier initializeSUT, + BiFunction applyToModel, + BiFunction applyToSut, + ThrowingBiConsumer afterAll) throws Throwable + { + MODEL model = initializeModel.get(); + SUT sut = initializeSUT.get(); + for (int i = 0; i < CYCLES; i++) + { + VISIT v = visitGenerator.generate(rand); + model = applyToModel.apply(model, v); + sut = applyToSut.apply(sut, v); + } + afterAll.accept(model, sut); + } + + public static void test(Generator visitGenerator, + Supplier initializeSUT, + BiFunction applyToSut, + Consumer afterAll) throws Throwable + { + SUT sut = initializeSUT.get(); + for (int i = 0; i < CYCLES; i++) + { + VISIT v = visitGenerator.generate(rand); + sut = applyToSut.apply(sut, v); + } + afterAll.accept(sut); + } + + public static interface ThrowingConsumer + { + void accept(T t) throws Throwable; + } + + public static interface ThrowingBiConsumer + { + void accept(T1 t1, T2 t2) throws Throwable; + } +} + diff --git a/test/harry/main/org/apache/cassandra/harry/util/ThrowingRunnable.java b/test/harry/main/org/apache/cassandra/harry/util/ThrowingRunnable.java new file mode 100644 index 0000000000..0927217078 --- /dev/null +++ b/test/harry/main/org/apache/cassandra/harry/util/ThrowingRunnable.java @@ -0,0 +1,33 @@ +/* + * 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.util; + +public interface ThrowingRunnable { + void run() throws Throwable; + + static Runnable toRunnable(ThrowingRunnable runnable) { + return () -> { + try { + runnable.run(); + } catch (Throwable var2) { + throw new RuntimeException(var2); + } + }; + } +} \ No newline at end of file diff --git a/test/harry/main/org/apache/cassandra/harry/util/TokenUtil.java b/test/harry/main/org/apache/cassandra/harry/util/TokenUtil.java new file mode 100644 index 0000000000..844b18a967 --- /dev/null +++ b/test/harry/main/org/apache/cassandra/harry/util/TokenUtil.java @@ -0,0 +1,150 @@ +/* + * 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.util; + +import java.nio.ByteBuffer; + +/** + * Copied verbatim from Apache Cassandra codebase to avoid having a dependency on Cassandra. + */ +public class TokenUtil +{ + public static long token(ByteBuffer key) + { + if (key.remaining() == 0) + return Long.MIN_VALUE; + + return normalize(getHash(key)[0]); + } + + private static long normalize(long v) + { + // We exclude the MINIMUM value; see getToken() + return v == Long.MIN_VALUE ? Long.MAX_VALUE : v; + } + + private static long[] getHash(ByteBuffer key) + { + long[] hash = new long[2]; + hash3_x64_128(key, key.position(), key.remaining(), 0, hash); + return hash; + } + + public static void hash3_x64_128(ByteBuffer key, int offset, int length, long seed, long[] result) + { + final int nblocks = length >> 4; // Process as 128-bit blocks. + + long h1 = seed; + long h2 = seed; + + long c1 = 0x87c37b91114253d5L; + long c2 = 0x4cf5ad432745937fL; + + //---------- + // body + + for(int i = 0; i < nblocks; i++) + { + long k1 = getBlock(key, offset, i * 2 + 0); + long k2 = getBlock(key, offset, i * 2 + 1); + + k1 *= c1; k1 = rotl64(k1,31); k1 *= c2; h1 ^= k1; + + h1 = rotl64(h1,27); h1 += h2; h1 = h1*5+0x52dce729; + + k2 *= c2; k2 = rotl64(k2,33); k2 *= c1; h2 ^= k2; + + h2 = rotl64(h2,31); h2 += h1; h2 = h2*5+0x38495ab5; + } + + //---------- + // tail + + // Advance offset to the unprocessed tail of the data. + offset += nblocks * 16; + + long k1 = 0; + long k2 = 0; + + switch(length & 15) + { + case 15: k2 ^= ((long) key.get(offset+14)) << 48; + case 14: k2 ^= ((long) key.get(offset+13)) << 40; + case 13: k2 ^= ((long) key.get(offset+12)) << 32; + case 12: k2 ^= ((long) key.get(offset+11)) << 24; + case 11: k2 ^= ((long) key.get(offset+10)) << 16; + case 10: k2 ^= ((long) key.get(offset+9)) << 8; + case 9: k2 ^= ((long) key.get(offset+8)) << 0; + k2 *= c2; k2 = rotl64(k2,33); k2 *= c1; h2 ^= k2; + + case 8: k1 ^= ((long) key.get(offset+7)) << 56; + case 7: k1 ^= ((long) key.get(offset+6)) << 48; + case 6: k1 ^= ((long) key.get(offset+5)) << 40; + case 5: k1 ^= ((long) key.get(offset+4)) << 32; + case 4: k1 ^= ((long) key.get(offset+3)) << 24; + case 3: k1 ^= ((long) key.get(offset+2)) << 16; + case 2: k1 ^= ((long) key.get(offset+1)) << 8; + case 1: k1 ^= ((long) key.get(offset)); + k1 *= c1; k1 = rotl64(k1,31); k1 *= c2; h1 ^= k1; + }; + + //---------- + // finalization + + h1 ^= length; h2 ^= length; + + h1 += h2; + h2 += h1; + + h1 = fmix(h1); + h2 = fmix(h2); + + h1 += h2; + h2 += h1; + + result[0] = h1; + result[1] = h2; + } + + protected static long getBlock(ByteBuffer key, int offset, int index) + { + int i_8 = index << 3; + int blockOffset = offset + i_8; + return ((long) key.get(blockOffset + 0) & 0xff) + (((long) key.get(blockOffset + 1) & 0xff) << 8) + + (((long) key.get(blockOffset + 2) & 0xff) << 16) + (((long) key.get(blockOffset + 3) & 0xff) << 24) + + (((long) key.get(blockOffset + 4) & 0xff) << 32) + (((long) key.get(blockOffset + 5) & 0xff) << 40) + + (((long) key.get(blockOffset + 6) & 0xff) << 48) + (((long) key.get(blockOffset + 7) & 0xff) << 56); + } + + protected static long rotl64(long v, int n) + { + return ((v << n) | (v >>> (64 - n))); + } + + protected static long fmix(long k) + { + k ^= k >>> 33; + k *= 0xff51afd7ed558ccdL; + k ^= k >>> 33; + k *= 0xc4ceb9fe1a85ec53L; + k ^= k >>> 33; + + return k; + } +} diff --git a/test/harry/main/org/apache/cassandra/harry/visitors/AllPartitionsValidator.java b/test/harry/main/org/apache/cassandra/harry/visitors/AllPartitionsValidator.java new file mode 100644 index 0000000000..30037b67d9 --- /dev/null +++ b/test/harry/main/org/apache/cassandra/harry/visitors/AllPartitionsValidator.java @@ -0,0 +1,150 @@ +/* + * 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.visitors; + +import java.util.ArrayList; +import java.util.List; +import java.util.concurrent.CopyOnWriteArrayList; +import java.util.concurrent.atomic.AtomicLong; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import org.apache.cassandra.concurrent.ExecutorFactory; +import org.apache.cassandra.concurrent.Interruptible; +import org.apache.cassandra.harry.core.Configuration; +import org.apache.cassandra.harry.core.MetricReporter; +import org.apache.cassandra.harry.core.Run; +import org.apache.cassandra.harry.ddl.SchemaSpec; +import org.apache.cassandra.harry.model.Model; +import org.apache.cassandra.harry.model.OpSelectors; +import org.apache.cassandra.harry.sut.SystemUnderTest; +import org.apache.cassandra.harry.operations.Query; +import org.apache.cassandra.harry.tracker.DataTracker; +import org.apache.cassandra.harry.runner.Runner; +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; + +/** + * Concurrently validates all partitions that were visited during this run. + */ +public class AllPartitionsValidator implements Visitor +{ + private static final Logger logger = LoggerFactory.getLogger(AllPartitionsValidator.class); + + protected final Model model; + protected final SchemaSpec schema; + protected final QueryLogger queryLogger; + protected final OpSelectors.Clock clock; + protected final OpSelectors.PdSelector pdSelector; + protected final MetricReporter metricReporter; + protected final SystemUnderTest sut; + protected final DataTracker tracker; + + protected final int concurrency; + + public static Configuration.VisitorConfiguration factoryForTests(int concurrency, + Model.ModelFactory modelFactory) + { + return (r) -> new AllPartitionsValidator(r, concurrency, modelFactory, QueryLogger.NO_OP); + } + + public AllPartitionsValidator(Run run, + int concurrency, + Model.ModelFactory modelFactory) + { + this(run, concurrency, modelFactory, QueryLogger.NO_OP); + } + + public AllPartitionsValidator(Run run, + int concurrency, + Model.ModelFactory modelFactory, + QueryLogger logger) + { + this.metricReporter = run.metricReporter; + this.model = modelFactory.make(run); + this.schema = run.schemaSpec; + this.clock = run.clock; + this.sut = run.sut; + this.pdSelector = run.pdSelector; + this.concurrency = concurrency; + this.tracker = run.tracker; + this.queryLogger = logger; + } + + protected void validateAllPartitions() throws Throwable + { + List threads = new ArrayList<>(); + WaitQueue queue = WaitQueue.newWaitQueue(); + WaitQueue.Signal interrupt = queue.register(); + List errors = new CopyOnWriteArrayList<>(); + + final long maxPosition = pdSelector.maxPosition(tracker.maxStarted()); + + AtomicLong currentPosition = new AtomicLong(); + for (int i = 0; i < concurrency; i++) + { + Interruptible thread = ExecutorFactory.Global.executorFactory().infiniteLoop(String.format("AllPartitionsValidator-%d", i + 1), + Runner.wrapInterrupt((state) -> { + if (state == Interruptible.State.NORMAL) + { + metricReporter.validatePartition(); + long pos = currentPosition.getAndIncrement(); + if (pos < maxPosition) + { + for (boolean reverse : new boolean[]{ true, false }) + { + Query query = Query.selectPartition(schema, pdSelector.pd(pdSelector.minLtsAt(pos), schema), reverse); + model.validate(query); + queryLogger.logSelectQuery((int)pos, query); + } + } + else + { + interrupt.signalAll(); + } + } + }, interrupt::signal, errors::add), SAFE, NON_DAEMON, UNSYNCHRONIZED); + threads.add(thread); + } + + interrupt.awaitUninterruptibly(); + + Runner.shutdown(threads::stream); + if (!errors.isEmpty()) + Runner.mergeAndThrow(errors); + } + + public void visit() + { + try + { + validateAllPartitions(); + } + catch (Throwable e) + { + throw new RuntimeException(e); + } + } + + +} \ No newline at end of file diff --git a/test/harry/main/org/apache/cassandra/harry/visitors/CorruptingVisitor.java b/test/harry/main/org/apache/cassandra/harry/visitors/CorruptingVisitor.java new file mode 100644 index 0000000000..1fcd93943e --- /dev/null +++ b/test/harry/main/org/apache/cassandra/harry/visitors/CorruptingVisitor.java @@ -0,0 +1,89 @@ +/* + * 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.visitors; + +import java.util.Random; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +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.runner.HarryRunner; +import org.apache.cassandra.harry.operations.Query; + +public class CorruptingVisitor implements Visitor +{ + public static final Logger logger = LoggerFactory.getLogger(HarryRunner.class); + + private final Run run; + private final QueryResponseCorruptor[] corruptors; + private final int triggerAfter; + + public CorruptingVisitor(int triggerAfter, + Run run) + { + this.run = run; + this.triggerAfter = triggerAfter; + + this.corruptors = new QueryResponseCorruptor[]{ + new QueryResponseCorruptor.SimpleQueryResponseCorruptor(run.schemaSpec, + run.clock, + HideRowCorruptor::new), + new AddExtraRowCorruptor(run.schemaSpec, + run.clock, + run.tracker, + run.descriptorSelector), + new QueryResponseCorruptor.SimpleQueryResponseCorruptor(run.schemaSpec, + run.clock, + HideValueCorruptor::new), + new QueryResponseCorruptor.SimpleQueryResponseCorruptor(run.schemaSpec, + run.clock, + ChangeValueCorruptor::new) + }; + } + + public void visit() + { + long lts = run.clock.peek(); + + if (lts > triggerAfter) + return; + + // TODO: switch to a better entropy source + Random random = new Random(1); + + QueryResponseCorruptor corruptor = corruptors[random.nextInt(corruptors.length)]; + long maxPos = run.pdSelector.maxPosition(run.tracker.maxStarted()); + long pd = run.pdSelector.pd(random.nextInt((int) maxPos), run.schemaSpec); + try + { + boolean success = corruptor.maybeCorrupt(Query.selectPartition(run.schemaSpec, pd, false), + run.sut); + logger.info("{} tried to corrupt a partition with a pd {}@{} my means of {}", success ? "Successfully" : "Unsuccessfully", pd, lts, corruptor.getClass()); + } + catch (Throwable t) + { + logger.error("Caught an exception while trying to corrupt a partition.", t); + } + } +} diff --git a/test/harry/main/org/apache/cassandra/harry/visitors/DoubleWriteVisitExecutor.java b/test/harry/main/org/apache/cassandra/harry/visitors/DoubleWriteVisitExecutor.java new file mode 100644 index 0000000000..06bc1d7458 --- /dev/null +++ b/test/harry/main/org/apache/cassandra/harry/visitors/DoubleWriteVisitExecutor.java @@ -0,0 +1,56 @@ +/* + * 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.visitors; + +public class DoubleWriteVisitExecutor extends VisitExecutor +{ + private final VisitExecutor delegate1; + private final VisitExecutor delegate2; + + public DoubleWriteVisitExecutor(VisitExecutor delegate1, + VisitExecutor delegate2) + { + this.delegate1 = delegate1; + this.delegate2 = delegate2; + } + + protected void beforeLts(long lts, long pd) + { + delegate1.beforeLts(lts, pd); + delegate2.beforeLts(lts, pd); + } + + protected void afterLts(long lts, long pd) + { + delegate1.afterLts(lts, pd); + delegate2.afterLts(lts, pd); + } + + protected void operation(Operation operation) + { + delegate1.operation(operation); + delegate2.operation(operation); + } + + public void shutdown() throws InterruptedException + { + delegate1.shutdown(); + delegate2.shutdown(); + } +} diff --git a/test/harry/main/org/apache/cassandra/harry/visitors/FaultInjectingVisitor.java b/test/harry/main/org/apache/cassandra/harry/visitors/FaultInjectingVisitor.java new file mode 100644 index 0000000000..428c699266 --- /dev/null +++ b/test/harry/main/org/apache/cassandra/harry/visitors/FaultInjectingVisitor.java @@ -0,0 +1,97 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.cassandra.harry.visitors; + +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.Executors; +import java.util.concurrent.ScheduledExecutorService; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicInteger; + +import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.annotation.JsonProperty; +import com.fasterxml.jackson.annotation.JsonTypeName; +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.operations.CompiledStatement; + +/** + * Fault injecting visitor: randomly fails some of the queries. + * + * Requires {@code FaultInjectingSut} to function. + */ +public class FaultInjectingVisitor extends LoggingVisitor +{ + private final AtomicInteger cnt = new AtomicInteger(); + + private final SystemUnderTest.FaultInjectingSut sut; + protected final ScheduledExecutorService executor = Executors.newScheduledThreadPool(2); + + public FaultInjectingVisitor(Run run, OperationExecutor.RowVisitorFactory rowVisitorFactory) + { + super(run, rowVisitorFactory); + this.sut = (SystemUnderTest.FaultInjectingSut) run.sut; + } + + void executeAsyncWithRetries(CompletableFuture originator, CompiledStatement statement) + { + executeAsyncWithRetries(originator, statement, true); + } + + void executeAsyncWithRetries(CompletableFuture originator, CompiledStatement statement, boolean allowFailures) + { + if (sut.isShutdown()) + throw new IllegalStateException("System under test is shut down"); + + CompletableFuture future; + if (allowFailures && cnt.getAndIncrement() % 2 == 0) + { + future = sut.executeAsyncWithWriteFailure(statement.cql(), SystemUnderTest.ConsistencyLevel.QUORUM, statement.bindings()); + } + else + { + future = sut.executeAsync(statement.cql(), SystemUnderTest.ConsistencyLevel.QUORUM, statement.bindings()); + } + + future.whenComplete((res, t) -> { + if (t != null) + executor.schedule(() -> executeAsyncWithRetries(originator, statement, false), 1, TimeUnit.SECONDS); + else + originator.complete(res); + }); + } + + @JsonTypeName("fault_injecting") + public static class FaultInjectingVisitorConfiguration extends Configuration.MutatingVisitorConfiguation + { + @JsonCreator + public FaultInjectingVisitorConfiguration(@JsonProperty("row_visitor") Configuration.RowVisitorConfiguration row_visitor) + { + super(row_visitor); + } + + @Override + public Visitor make(Run run) + { + return new FaultInjectingVisitor(run, row_visitor); + } + } + +} diff --git a/test/harry/main/org/apache/cassandra/harry/visitors/GeneratingVisitor.java b/test/harry/main/org/apache/cassandra/harry/visitors/GeneratingVisitor.java new file mode 100644 index 0000000000..3cda4179d5 --- /dev/null +++ b/test/harry/main/org/apache/cassandra/harry/visitors/GeneratingVisitor.java @@ -0,0 +1,232 @@ +/* + * 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.visitors; + +import org.apache.cassandra.harry.core.Run; +import org.apache.cassandra.harry.ddl.SchemaSpec; +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 GeneratingVisitor extends LtsVisitor +{ + private final OpSelectors.PdSelector pdSelector; + private final OpSelectors.DescriptorSelector descriptorSelector; + private final QueryGenerator rangeSelector; + private final SchemaSpec schema; + + public GeneratingVisitor(Run run, + VisitExecutor delegate) + { + super(delegate, run.clock::nextLts); + + this.pdSelector = run.pdSelector; + this.descriptorSelector = run.descriptorSelector; + this.schema = run.schemaSpec; + this.rangeSelector = run.rangeSelector; + } + + @Override + public void visit(long lts) + { + generate(lts, pdSelector.pd(lts, schema)); + } + + private void generate(long lts, long pd) + { + beforeLts(lts, pd); + int opsPerLts = descriptorSelector.operationsPerLts(lts); + for (long opId = 0; opId < opsPerLts; opId++) + { + OpSelectors.OperationKind kind = descriptorSelector.operationType(pd, lts, opId); + BaseOperation operation; + switch (kind) + { + case INSERT: + case UPDATE: + operation = writeOp(lts, pd, opId, kind); + break; + case INSERT_WITH_STATICS: + case UPDATE_WITH_STATICS: + operation = writeRegularAndStatic(lts, pd, opId, kind); + break; + case DELETE_ROW: + { + long cd = descriptorSelector.cd(pd, lts, opId, schema); + operation = new GeneratedDeleteRowOp(lts, pd, cd, opId, kind); + break; + } + case DELETE_COLUMN: + case DELETE_COLUMN_WITH_STATICS: + { + long cd = descriptorSelector.cd(pd, lts, opId, schema); + BitSet columns = descriptorSelector.columnMask(pd, lts, opId, kind); + operation = new GeneratedDeleteColumnsOp(lts, pd, cd, opId, kind, columns); + break; + } + case DELETE_PARTITION: + case DELETE_RANGE: + case DELETE_SLICE: + operation = new GeneratedDeleteOp(lts, pd, opId, kind, rangeSelector); + break; + default: + throw new IllegalStateException("All cases are covered but not " + kind); + } + operation(operation); + } + afterLts(lts, pd); + } + + public BaseOperation writeOp(long lts, long pd, long opId, OpSelectors.OperationKind kind) + { + long cd = descriptorSelector.cd(pd, lts, opId, schema); + + return new GeneratedWriteOp(lts, pd, cd, opId, kind) + { + @Override + public long[] vds() + { + return descriptorSelector.vds(pd, cd(), lts, opId, kind(), schema); + } + }; + } + + public BaseOperation writeRegularAndStatic(long lts, long pd, long opId, OpSelectors.OperationKind kind) + { + long cd = descriptorSelector.cd(pd, lts, opId, schema); + + return new GeneratedWriteWithStaticOp(lts, pd, cd, opId, kind) + { + public long[] sds() + { + return descriptorSelector.sds(pd, cd(), lts, opId, kind(), schema); + } + + @Override + public long[] vds() + { + return descriptorSelector.vds(pd, cd(), lts, opId, kind(), schema); + } + }; + } + + public abstract static class GeneratedWriteOp extends BaseOperation implements ReplayingVisitor.WriteOp + { + protected final long cd; + public GeneratedWriteOp(long lts, long pd, long cd, long opId, OpSelectors.OperationKind kind) + { + super(lts, pd, opId, kind); + this.cd = cd; + } + + @Override + public long cd() + { + return cd; + } + } + + public abstract static class GeneratedWriteWithStaticOp extends GeneratedWriteOp implements ReplayingVisitor.WriteStaticOp + { + public GeneratedWriteWithStaticOp(long lts, long pd, long cd, long opId, OpSelectors.OperationKind kind) + { + super(lts, pd, cd, opId, kind); + } + } + + public static class GeneratedDeleteRowOp extends BaseOperation implements ReplayingVisitor.DeleteRowOp + { + private final long cd; + public GeneratedDeleteRowOp(long lts, long pd, long cd, long opId, OpSelectors.OperationKind kind) + { + super(lts, pd, opId, kind); + this.cd = cd; + } + + @Override + public long cd() + { + return cd; + } + } + + public static class GeneratedDeleteOp extends BaseOperation implements ReplayingVisitor.DeleteOp + { + private final Query relations; + + public GeneratedDeleteOp(long lts, long pd, long opId, OpSelectors.OperationKind kind, QueryGenerator queryGenerator) + { + this(lts, pd, opId, kind, queryGenerator.inflate( lts, opId, queryKind(kind))); + } + + public GeneratedDeleteOp(long lts, long pd, long opId, OpSelectors.OperationKind kind, Query relations) + { + super(lts, pd, opId, kind); + this.relations = relations; + } + + @Override + public Query relations() + { + return relations; + } + + protected static Query.QueryKind queryKind(OpSelectors.OperationKind kind) + { + switch (kind) + { + case DELETE_PARTITION: + return Query.QueryKind.SINGLE_PARTITION; + case DELETE_ROW: + return Query.QueryKind.SINGLE_CLUSTERING; + case DELETE_RANGE: + return Query.QueryKind.CLUSTERING_RANGE; + case DELETE_SLICE: + return Query.QueryKind.CLUSTERING_SLICE; + default: + throw new IllegalStateException(String.format("Can not transform %s into delete", kind)); + } + } + } + + public static class GeneratedDeleteColumnsOp extends BaseOperation implements ReplayingVisitor.DeleteColumnsOp + { + private final long cd; + private final BitSet columnMask; + + public GeneratedDeleteColumnsOp(long lts, long pd, long cd, long opId, OpSelectors.OperationKind kind, BitSet columnMask) + { + super(lts, pd, opId, kind); + this.cd = cd; + this.columnMask = columnMask; + } + + public long cd() + { + return cd; + } + + @Override + public BitSet columns() + { + return columnMask; + } + } +} diff --git a/test/harry/main/org/apache/cassandra/harry/visitors/LoggingVisitor.java b/test/harry/main/org/apache/cassandra/harry/visitors/LoggingVisitor.java new file mode 100644 index 0000000000..eee0996db9 --- /dev/null +++ b/test/harry/main/org/apache/cassandra/harry/visitors/LoggingVisitor.java @@ -0,0 +1,85 @@ +/* + * 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.visitors; + +import java.io.BufferedWriter; +import java.io.File; +import java.io.FileNotFoundException; +import java.io.FileOutputStream; +import java.io.IOException; +import java.io.OutputStreamWriter; + +import org.apache.cassandra.harry.core.Run; +import org.apache.cassandra.harry.operations.CompiledStatement; + +public class LoggingVisitor extends GeneratingVisitor +{ + public LoggingVisitor(Run run, + OperationExecutor.RowVisitorFactory rowVisitorFactory) + { + super(run, new LoggingVisitorExecutor(run, rowVisitorFactory.make(run))); + } + + public static class LoggingVisitorExecutor extends MutatingVisitor.MutatingVisitExecutor + { + private final BufferedWriter operationLog; + + public LoggingVisitorExecutor(Run run, OperationExecutor rowVisitor) + { + super(run, rowVisitor); + + File f = new File("operation.log"); + try + { + operationLog = new BufferedWriter(new OutputStreamWriter(new FileOutputStream(f))); + } + catch (FileNotFoundException e) + { + throw new RuntimeException(e); + } + } + + public void afterLts(long lts, long pd) + { + super.afterLts(lts, pd); + log("LTS: %d. Pd %d. Finished\n", lts, pd); + } + + @Override + protected void operationInternal(Operation operation, CompiledStatement statement) + { + super.operationInternal(operation, statement); + log(String.format("LTS: %d. Pd %d. Operation: %s Statement %s\n", + operation.lts(), operation.pd(), operation, statement)); + } + + private void log(String format, Object... objects) + { + try + { + operationLog.write(String.format(format, objects)); + operationLog.flush(); + } + catch (IOException e) + { + // ignore + } + } + } +} diff --git a/test/harry/main/org/apache/cassandra/harry/visitors/LtsVisitor.java b/test/harry/main/org/apache/cassandra/harry/visitors/LtsVisitor.java new file mode 100644 index 0000000000..f34b5df5a5 --- /dev/null +++ b/test/harry/main/org/apache/cassandra/harry/visitors/LtsVisitor.java @@ -0,0 +1,83 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.cassandra.harry.visitors; + +import java.util.function.LongSupplier; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * Common class for all visitors that support visits at a specific logical timestamp. + * + * Classes inheriting from LTS Visitor have to visit drawn LTS: every LTS that has been received + * from the consumer _has to_ actually be visited. If this is not done, during model checking + * drawn LTS will be considered visited, which will lead to data inconsistencies. + * + * This class and its implementations such as Mutating visitor are NOT thread safe. If you'd like + * to have several threads generating data, use multiple copies of delegating visitor, since + */ +public abstract class LtsVisitor extends VisitExecutor implements Visitor +{ + private static final Logger logger = LoggerFactory.getLogger(LtsVisitor.class); + protected final VisitExecutor delegate; + private final LongSupplier ltsSource; + + public LtsVisitor(VisitExecutor delegate, + LongSupplier ltsSource) + { + this.delegate = delegate; + this.ltsSource = ltsSource; + } + + public final void visit() + { + long lts = ltsSource.getAsLong(); + if (lts > 0 && lts % 10_000 == 0) + logger.info("Visiting lts {}...", lts); + visit(lts); + } + + public abstract void visit(long lts); + + @Override + protected void beforeLts(long lts, long pd) + { + delegate.beforeLts(lts, pd); + } + + @Override + protected void afterLts(long lts, long pd) + { + delegate.afterLts(lts, pd); + } + + @Override + protected void operation(Operation operation) + { + delegate.operation(operation); + } + + @Override + public void shutdown() throws InterruptedException + { + delegate.shutdown(); + } + +} diff --git a/test/harry/main/org/apache/cassandra/harry/visitors/MutatingRowVisitor.java b/test/harry/main/org/apache/cassandra/harry/visitors/MutatingRowVisitor.java new file mode 100644 index 0000000000..d394693c21 --- /dev/null +++ b/test/harry/main/org/apache/cassandra/harry/visitors/MutatingRowVisitor.java @@ -0,0 +1,115 @@ +/* + * 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.visitors; + +import org.apache.cassandra.harry.core.MetricReporter; +import org.apache.cassandra.harry.core.Run; +import com.google.common.annotations.VisibleForTesting; +import org.apache.cassandra.harry.ddl.SchemaSpec; +import org.apache.cassandra.harry.model.OpSelectors; +import org.apache.cassandra.harry.operations.CompiledStatement; +import org.apache.cassandra.harry.operations.DeleteHelper; +import org.apache.cassandra.harry.operations.WriteHelper; +import org.apache.cassandra.harry.util.BitSet; + +public class MutatingRowVisitor implements OperationExecutor +{ + protected final SchemaSpec schema; + protected final OpSelectors.Clock clock; + protected final MetricReporter metricReporter; + + public MutatingRowVisitor(Run run) + { + this(run.schemaSpec, + run.clock, + run.metricReporter); + } + + @VisibleForTesting + public MutatingRowVisitor(SchemaSpec schema, + OpSelectors.Clock clock, + MetricReporter metricReporter) + { + this.metricReporter = metricReporter; + this.schema = schema; + this.clock = clock; + } + + public CompiledStatement insert(VisitExecutor.WriteOp op) + { + metricReporter.insert(); + return WriteHelper.inflateInsert(schema, op.pd(), op.cd(), op.vds(), null, clock.rts(op.lts())); + } + + public CompiledStatement insertWithStatics(VisitExecutor.WriteStaticOp op) + { + metricReporter.insert(); + return WriteHelper.inflateInsert(schema, op.pd(), op.cd(), op.vds(), op.sds(), clock.rts(op.lts())); + } + + public CompiledStatement update(VisitExecutor.WriteOp op) + { + metricReporter.insert(); + return WriteHelper.inflateUpdate(schema, op.pd(), op.cd(), op.vds(), null, clock.rts(op.lts())); + } + + public CompiledStatement updateWithStatics(VisitExecutor.WriteStaticOp op) + { + metricReporter.insert(); + return WriteHelper.inflateUpdate(schema, op.pd(), op.cd(), op.vds(), op.sds(), clock.rts(op.lts())); + } + + public CompiledStatement deleteColumn(VisitExecutor.DeleteColumnsOp op) + { + metricReporter.columnDelete(); + BitSet mask = schema.regularColumnsMask(); + return DeleteHelper.deleteColumn(schema, op.pd(), op.cd(), op.columns(), mask, clock.rts(op.lts())); + } + + public CompiledStatement deleteColumnWithStatics(VisitExecutor.DeleteColumnsOp op) + { + metricReporter.columnDelete(); + BitSet mask = schema.regularAndStaticColumnsMask(); + return DeleteHelper.deleteColumn(schema, op.pd(), op.cd(), op.columns(), mask, clock.rts(op.lts())); + } + + public CompiledStatement deleteRow(VisitExecutor.DeleteRowOp op) + { + metricReporter.rowDelete(); + return DeleteHelper.deleteRow(schema, op.pd(), op.cd(), clock.rts(op.lts())); + } + + public CompiledStatement deletePartition(VisitExecutor.DeleteOp op) + { + metricReporter.partitionDelete(); + return DeleteHelper.delete(schema, op.pd(), clock.rts(op.lts())); + } + + public CompiledStatement deleteRange(VisitExecutor.DeleteOp op) + { + metricReporter.rangeDelete(); + return op.relations().toDeleteStatement(clock.rts(op.lts())); + } + + public CompiledStatement deleteSlice(VisitExecutor.DeleteOp op) + { + metricReporter.rangeDelete(); + return op.relations().toDeleteStatement(clock.rts(op.lts())); + } +} \ No newline at end of file diff --git a/test/harry/main/org/apache/cassandra/harry/visitors/MutatingVisitor.java b/test/harry/main/org/apache/cassandra/harry/visitors/MutatingVisitor.java new file mode 100644 index 0000000000..4380fd1de9 --- /dev/null +++ b/test/harry/main/org/apache/cassandra/harry/visitors/MutatingVisitor.java @@ -0,0 +1,220 @@ +/* + * 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.visitors; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import java.util.concurrent.TimeUnit; +import java.util.function.Function; + +import com.google.common.util.concurrent.Uninterruptibles; +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.SchemaSpec; +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; + +public class MutatingVisitor extends GeneratingVisitor +{ + private static final Logger logger = LoggerFactory.getLogger(MutatingVisitor.class); + + public MutatingVisitor(Run run) + { + this(run, MutatingRowVisitor::new); + } + + public MutatingVisitor(Run run, + OperationExecutor.RowVisitorFactory rowVisitorFactory) + { + this(run, new MutatingVisitExecutor(run, rowVisitorFactory.make(run))); + } + + public static Configuration.VisitorConfiguration factory() + { + return MutatingVisitor::new; + } + + public static Configuration.VisitorConfiguration factory(Function rowVisitorFactory) + { + return (r) -> new MutatingVisitor(r, rowVisitorFactory.apply(r)); + } + + public MutatingVisitor(Run run, + VisitExecutor visitExecutor) + { + super(run, visitExecutor); + } + + public static class LtsTrackingVisitExecutor extends MutatingVisitExecutor + { + public LtsTrackingVisitExecutor(OpSelectors.DescriptorSelector descriptorSelector, DataTracker tracker, SystemUnderTest sut, SchemaSpec schema, OperationExecutor rowVisitor, SystemUnderTest.ConsistencyLevel consistencyLevel) + { + super(descriptorSelector, tracker, sut, schema, rowVisitor, consistencyLevel); + assert schema.trackLts : "LTS Tracking visitor can only be used when tracking LTS in schema"; + } + + @Override + public void afterLts(long lts, long pd) + { + if (hadVisibleVisit) + { + StringBuilder sb = new StringBuilder(); + sb.append("UPDATE ").append(schema.keyspace).append(".").append(schema.table) + .append(" SET visited_lts = visited_lts + [").append(lts).append("] ") + .append("WHERE "); + Object[] pk = schema.inflatePartitionKey(pd); + for (int i = 0; i < pk.length; i++) + { + if (i > 0) + sb.append(" AND "); + sb.append(schema.partitionKeys.get(i).name + " = ?"); + bindings.add(pk[i]); + } + sb.append(";"); + statements.add(sb.toString()); + } + + super.afterLts(lts, pd); + } + } + + public static class MutatingVisitExecutor extends VisitExecutor + { + protected final List statements = new ArrayList<>(); + protected final List bindings = new ArrayList<>(); + + protected final OpSelectors.DescriptorSelector descriptorSelector; + protected final DataTracker tracker; + protected final SystemUnderTest sut; + protected final OperationExecutor rowVisitor; + protected final SystemUnderTest.ConsistencyLevel consistencyLevel; + protected final SchemaSpec schema; + private final int maxRetries = 10; + // Apart from partition deletion, we register all operations on partition level + protected Boolean hadVisibleVisit = null; + + public MutatingVisitExecutor(Run run, OperationExecutor rowVisitor) + { + this(run, rowVisitor, SystemUnderTest.ConsistencyLevel.QUORUM); + } + + public MutatingVisitExecutor(Run run, OperationExecutor rowVisitor, SystemUnderTest.ConsistencyLevel consistencyLevel) + { + this(run.descriptorSelector, run.tracker, run.sut, run.schemaSpec, rowVisitor, consistencyLevel); + } + + public MutatingVisitExecutor(OpSelectors.DescriptorSelector descriptorSelector, + DataTracker tracker, + SystemUnderTest sut, + SchemaSpec schema, + OperationExecutor rowVisitor, + SystemUnderTest.ConsistencyLevel consistencyLevel) + { + this.descriptorSelector = descriptorSelector; + this.tracker = tracker; + this.sut = sut; + this.schema = schema; + this.rowVisitor = rowVisitor; + this.consistencyLevel = consistencyLevel; + } + + @Override + public void beforeLts(long lts, long pd) + { + tracker.beginModification(lts); + } + + @Override + public void afterLts(long lts, long pd) + { + if (statements.isEmpty()) + { + logger.warn("Encountered an empty batch on {}", lts); + return; + } + + String query = String.join(" ", statements); + if (statements.size() > 1) + query = String.format("BEGIN UNLOGGED BATCH\n%s\nAPPLY BATCH;", query); + + Object[] bindingsArray = new Object[bindings.size()]; + bindings.toArray(bindingsArray); + statements.clear(); + bindings.clear(); + + CompiledStatement compiledStatement = new CompiledStatement(query, bindingsArray); + executeWithRetries(lts, pd, compiledStatement); + tracker.endModification(lts); + hadVisibleVisit = null; + } + + @Override + public void operation(Operation operation) + { + // Partition deletions have highest precedence even in batches, so we have to make + // a distinction between "we have not seen any operations yet" and "there was a partition deletion" + if (hadVisibleVisit == null) + hadVisibleVisit = operation.kind().hasVisibleVisit(); + else + hadVisibleVisit &= operation.kind().hasVisibleVisit(); + + operationInternal(operation, rowVisitor.perform(operation)); + } + + protected void operationInternal(Operation operation, CompiledStatement statement) + { + statements.add(statement.cql()); + Collections.addAll(bindings, statement.bindings()); + } + + protected Object[][] executeWithRetries(long lts, long pd, CompiledStatement statement) + { + if (sut.isShutdown()) + throw new IllegalStateException("System under test is shut down"); + + int retries = 0; + + while (retries++ < maxRetries) + { + try + { + return sut.execute(statement.cql(), consistencyLevel, statement.bindings()); + } + catch (Throwable t) + { + int delaySecs = 1; + logger.error(String.format("Caught message while trying to execute: %s. Scheduled to retry in %s seconds", statement, delaySecs), t); + Uninterruptibles.sleepUninterruptibly(delaySecs, TimeUnit.SECONDS); + } + } + + throw new IllegalStateException(String.format("Can not execute statement %s after %d retries", statement, retries)); + } + + public void shutdown() throws InterruptedException + { + } + } +} diff --git a/test/harry/main/org/apache/cassandra/harry/visitors/OperationExecutor.java b/test/harry/main/org/apache/cassandra/harry/visitors/OperationExecutor.java new file mode 100644 index 0000000000..a5997a2a1d --- /dev/null +++ b/test/harry/main/org/apache/cassandra/harry/visitors/OperationExecutor.java @@ -0,0 +1,78 @@ +/* + * 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.visitors; + +import org.apache.cassandra.harry.core.Run; +import org.apache.cassandra.harry.operations.CompiledStatement; + +public interface OperationExecutor +{ + interface RowVisitorFactory + { + OperationExecutor make(Run run); + } + + default CompiledStatement perform(VisitExecutor.Operation operation) + { + switch (operation.kind()) + { + // TODO: switch to EnumMap + // TODO: pluggable capabilities; OperationKind can/should bear its own logic + case INSERT: + return insert((VisitExecutor.WriteOp) operation); + case UPDATE: + return update((VisitExecutor.WriteOp) operation); + case DELETE_ROW: + return deleteRow((VisitExecutor.DeleteRowOp) operation); + case INSERT_WITH_STATICS: + return insertWithStatics((VisitExecutor.WriteStaticOp) operation); + case UPDATE_WITH_STATICS: + return updateWithStatics((VisitExecutor.WriteStaticOp) operation); + case DELETE_PARTITION: + return deletePartition((VisitExecutor.DeleteOp) operation); + case DELETE_COLUMN: + return deleteColumn((VisitExecutor.DeleteColumnsOp) operation); + case DELETE_COLUMN_WITH_STATICS: + return deleteColumnWithStatics((VisitExecutor.DeleteColumnsOp) operation); + case DELETE_RANGE: + return deleteRange((VisitExecutor.DeleteOp) operation); + case DELETE_SLICE: + return deleteSlice((VisitExecutor.DeleteOp) operation); + default: + throw new IllegalStateException(); + } + } + + CompiledStatement insert(VisitExecutor.WriteOp operation); + CompiledStatement update(VisitExecutor.WriteOp operation); + + CompiledStatement insertWithStatics(VisitExecutor.WriteStaticOp operation); + CompiledStatement updateWithStatics(VisitExecutor.WriteStaticOp operation); + + CompiledStatement deleteColumn(VisitExecutor.DeleteColumnsOp operation); + CompiledStatement deleteColumnWithStatics(VisitExecutor.DeleteColumnsOp operation); + + CompiledStatement deleteRow(VisitExecutor.DeleteRowOp operation); + + CompiledStatement deletePartition(VisitExecutor.DeleteOp deleteOp); + CompiledStatement deleteRange(VisitExecutor.DeleteOp deleteOp); + CompiledStatement deleteSlice(VisitExecutor.DeleteOp deleteOp); + + +} \ No newline at end of file diff --git a/test/harry/main/org/apache/cassandra/harry/visitors/ParallelRecentValidator.java b/test/harry/main/org/apache/cassandra/harry/visitors/ParallelRecentValidator.java new file mode 100644 index 0000000000..61cd1b2122 --- /dev/null +++ b/test/harry/main/org/apache/cassandra/harry/visitors/ParallelRecentValidator.java @@ -0,0 +1,149 @@ +/* + * 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.visitors; + +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.atomic.AtomicLong; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.annotation.JsonProperty; +import com.fasterxml.jackson.annotation.JsonTypeName; +import org.apache.cassandra.harry.core.Configuration; +import org.apache.cassandra.harry.core.Run; +import org.apache.cassandra.harry.gen.Surjections; +import org.apache.cassandra.harry.model.Model; +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.tracker.DataTracker; + +public class ParallelRecentValidator extends ParallelValidator +{ + private static final Logger logger = LoggerFactory.getLogger(ParallelRecentValidator.class); + + private final int partitionCount; + private final int queries; + private final QueryGenerator.TypedQueryGenerator querySelector; + private final Model model; + private final QueryLogger queryLogger; + private final OpSelectors.PdSelector pdSelector; + private final DataTracker tracker; + + public ParallelRecentValidator(int partitionCount, int concurrency, int queries, + Run run, + Model.ModelFactory modelFactory, + QueryLogger queryLogger) + { + super(concurrency, run); + this.pdSelector = run.pdSelector; + this.tracker = run.tracker; + this.partitionCount = partitionCount; + this.queries = Math.max(queries, 1); + this.querySelector = new QueryGenerator.TypedQueryGenerator(run.rng, + // TODO: make query kind configurable + Surjections.enumValues(Query.QueryKind.class), + run.rangeSelector); + this.model = modelFactory.make(run); + this.queryLogger = queryLogger; + } + + protected void doOne(State state) + { + long claim = state.claim(); + if (claim < 0) + return; + + long visitLts = run.pdSelector.minLtsAt(state.position - claim); + for (int i = 0; i < queries; i++) + { + run.metricReporter.validateRandomQuery(); + Query query = querySelector.inflate(visitLts, i); + model.validate(query); + queryLogger.logSelectQuery(i, query); + } + } + + protected CompletableFuture startThreads(ExecutorService executor, int parallelism) + { + logger.info("Validating {} recent partitions", partitionCount); + return super.startThreads(executor, parallelism); + } + + protected State initialState() + { + return new State(pdSelector.maxPosition(tracker.maxStarted())); + } + + public class State extends ParallelValidator.State + { + private final long position; + private final AtomicLong counter; + + public State(long maxPos) + { + this.position = maxPos; + this.counter = new AtomicLong(partitionCount); + } + + public long claim() + { + long v = counter.getAndDecrement(); + if (v <= 0) + signal(); + + return v; + } + } + + @JsonTypeName("parallel_validate_recent_partitions") + public static class ParallelRecentValidatorConfig implements Configuration.VisitorConfiguration + { + public final int partition_count; + public final int queries; + public final int concurrency; + public final Configuration.ModelConfiguration modelConfiguration; + public final Configuration.QueryLoggerConfiguration query_logger; + + // TODO: make query selector configurable + @JsonCreator + public ParallelRecentValidatorConfig(@JsonProperty("partition_count") int partition_count, + @JsonProperty("concurrency") int concurrency, + @JsonProperty("queries_per_partition") int queries, + @JsonProperty("model") Configuration.ModelConfiguration model, + @JsonProperty("query_logger") Configuration.QueryLoggerConfiguration query_logger) + { + this.partition_count = partition_count; + this.concurrency = concurrency; + this.queries = Math.max(queries, 1); + this.modelConfiguration = model; + this.query_logger = QueryLogger.thisOrDefault(query_logger); + } + + @Override + public Visitor make(Run run) + { + return new ParallelRecentValidator(partition_count, concurrency, queries, run, modelConfiguration, query_logger.make()); + } + } + +} diff --git a/test/harry/main/org/apache/cassandra/harry/visitors/ParallelValidator.java b/test/harry/main/org/apache/cassandra/harry/visitors/ParallelValidator.java new file mode 100644 index 0000000000..4c2dea20fc --- /dev/null +++ b/test/harry/main/org/apache/cassandra/harry/visitors/ParallelValidator.java @@ -0,0 +1,103 @@ +/* + * 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.visitors; + +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicBoolean; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import org.apache.cassandra.harry.core.Run; + +public abstract class ParallelValidator implements Visitor +{ + private static final Logger logger = LoggerFactory.getLogger(AllPartitionsValidator.class); + + protected final Run run; + protected final int parallelism; + protected final ExecutorService executor; + + public ParallelValidator(int parallelism, + Run run) + { + this.run = run; + this.parallelism = parallelism; + this.executor = Executors.newFixedThreadPool(parallelism); + } + + protected abstract void doOne(T state); + protected abstract T initialState(); + + protected CompletableFuture startThreads(ExecutorService executor, int parallelism) + { + CompletableFuture[] futures = new CompletableFuture[parallelism]; + T shared = initialState(); + + for (int i = 0; i < parallelism; i++) + { + futures[i] = CompletableFuture.supplyAsync(() -> { + while (!shared.signalled()) + doOne(shared); + + return null; + }, executor); + } + + return CompletableFuture.allOf(futures); + } + + public abstract static class State + { + private final AtomicBoolean isDone = new AtomicBoolean(false); + + public void signal() + { + isDone.set(true); + } + + public boolean signalled() + { + return isDone.get(); + } + } + + public void visit() + { + try + { + startThreads(executor, parallelism).get(); + } + catch (Throwable e) + { + throw new RuntimeException(e); + } + logger.info("Finished validations"); + } + + @Override + public void shutdown() throws InterruptedException + { + executor.shutdown(); + executor.awaitTermination(60, TimeUnit.SECONDS); + } +} \ No newline at end of file diff --git a/test/harry/main/org/apache/cassandra/harry/visitors/QueryLogger.java b/test/harry/main/org/apache/cassandra/harry/visitors/QueryLogger.java new file mode 100644 index 0000000000..3eb60df742 --- /dev/null +++ b/test/harry/main/org/apache/cassandra/harry/visitors/QueryLogger.java @@ -0,0 +1,97 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.cassandra.harry.visitors; + +import java.io.BufferedWriter; +import java.io.File; +import java.io.FileNotFoundException; +import java.io.FileOutputStream; +import java.io.IOException; +import java.io.OutputStreamWriter; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import org.apache.cassandra.harry.core.Configuration; +import org.apache.cassandra.harry.operations.Query; + +public interface QueryLogger +{ + static Configuration.QueryLoggerConfiguration thisOrDefault(Configuration.QueryLoggerConfiguration config) + { + if (config == null) + return () -> NO_OP; + return config; + } + + QueryLogger NO_OP = new NoOpQueryLogger(); + + void println(String a, Object... interpolate); + default void logSelectQuery(int modifier, Query query) + { + println(String.format("PD: %d. Modifier: %d.\t%s", query.pd, modifier, query.toSelectStatement())); + } + interface QueryLoggerFactory + { + QueryLogger make(); + } + + class FileQueryLogger implements QueryLogger + { + private static final Logger logger = LoggerFactory.getLogger(FileQueryLogger.class); + private final BufferedWriter log; + + public FileQueryLogger(String filename) + { + File f = new File(filename); + try + { + log = new BufferedWriter(new OutputStreamWriter(new FileOutputStream(f))); + } + catch (FileNotFoundException e) + { + throw new RuntimeException(e); + } + } + + public void println(String s, Object... interpolate) + { + try + { + log.write(String.format(s, interpolate)); + } + catch (IOException e) + { + logger.error("Could not log line", e); + } + } + } + + class NoOpQueryLogger implements QueryLogger + { + public void println(String a, Object... interpolate) + { + } + + public void logSelectQuery(int modifier, Query query) + { + + } + } +} diff --git a/test/harry/main/org/apache/cassandra/harry/visitors/RandomPartitionValidator.java b/test/harry/main/org/apache/cassandra/harry/visitors/RandomPartitionValidator.java new file mode 100644 index 0000000000..d1bd1d931e --- /dev/null +++ b/test/harry/main/org/apache/cassandra/harry/visitors/RandomPartitionValidator.java @@ -0,0 +1,58 @@ +/* + * 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.visitors; + +import java.util.concurrent.atomic.AtomicLong; + +import org.apache.cassandra.harry.core.Run; +import org.apache.cassandra.harry.model.Model; +import org.apache.cassandra.harry.model.OpSelectors; +import org.apache.cassandra.harry.operations.QueryGenerator; + +public class RandomPartitionValidator implements Visitor +{ + protected final Model model; + protected final QueryGenerator.TypedQueryGenerator queryGenerator; + protected final Run run; + protected final AtomicLong modifier; + protected final QueryLogger logger; + + public RandomPartitionValidator(Run run, + Model.ModelFactory modelFactory, + QueryLogger logger) + { + this.model = modelFactory.make(run); + this.queryGenerator = new QueryGenerator.TypedQueryGenerator(run); + this.run = run; + this.modifier = new AtomicLong(); + this.logger = logger; + } + + public void visit() + { + if (run.tracker.maxStarted() == 0) + return; + + long modifier = this.modifier.incrementAndGet(); + long pd = ((OpSelectors.DefaultPdSelector)run.pdSelector).randomVisitedPd(run.tracker.maxStarted(), + modifier, + run.schemaSpec); + model.validate(queryGenerator.inflate(run.pdSelector.maxLtsFor(pd), modifier)); + } +} diff --git a/test/harry/main/org/apache/cassandra/harry/visitors/RandomValidator.java b/test/harry/main/org/apache/cassandra/harry/visitors/RandomValidator.java new file mode 100644 index 0000000000..f159e1475d --- /dev/null +++ b/test/harry/main/org/apache/cassandra/harry/visitors/RandomValidator.java @@ -0,0 +1,93 @@ +/* + * 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.visitors; + +import java.util.concurrent.atomic.AtomicLong; + +import org.apache.cassandra.harry.core.MetricReporter; +import org.apache.cassandra.harry.core.Run; +import org.apache.cassandra.harry.ddl.SchemaSpec; +import org.apache.cassandra.harry.gen.Surjections; +import org.apache.cassandra.harry.model.Model; +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.tracker.DataTracker; + +public class RandomValidator implements Visitor +{ + private final QueryLogger logger; + private final Model model; + + private final OpSelectors.DefaultPdSelector pdSelector; + private final QueryGenerator.TypedQueryGenerator querySelector; + private final MetricReporter metricReporter; + private final DataTracker tracker; + private final AtomicLong modifier; + private final SchemaSpec schemaSpec; + + private final int partitionCount; + private final int queries; + + public RandomValidator(int partitionCount, + int queries, + Run run, + Model.ModelFactory modelFactory, + QueryLogger logger) + { + this.partitionCount = partitionCount; + this.queries = Math.max(queries, 1); + this.metricReporter = run.metricReporter; + this.pdSelector = (OpSelectors.DefaultPdSelector) run.pdSelector; + this.querySelector = new QueryGenerator.TypedQueryGenerator(run.rng, + Surjections.pick(Query.QueryKind.SINGLE_PARTITION), + run.rangeSelector); + this.model = modelFactory.make(run); + this.logger = logger; + this.tracker = run.tracker; + this.schemaSpec = run.schemaSpec; + + this.modifier = new AtomicLong(); + } + + // TODO: expose metric, how many times validated recent partitions + private int validateRandomPartitions() + { + for (int i = 0; i < partitionCount && !Thread.currentThread().isInterrupted(); i++) + { + metricReporter.validateRandomQuery(); + long modifier = this.modifier.incrementAndGet(); + long pd = pdSelector.randomVisitedPd(tracker.maxStarted(), modifier, schemaSpec); + for (int j = 0; j < queries && !Thread.currentThread().isInterrupted(); j++) + { + Query query = querySelector.inflate(pdSelector.maxLtsFor(pd), j); + logger.logSelectQuery(j, query); + model.validate(query); + } + } + + return partitionCount; + } + + @Override + public void visit() + { + validateRandomPartitions(); + } +} \ No newline at end of file diff --git a/test/harry/main/org/apache/cassandra/harry/visitors/RecentValidator.java b/test/harry/main/org/apache/cassandra/harry/visitors/RecentValidator.java new file mode 100644 index 0000000000..30f34c56c8 --- /dev/null +++ b/test/harry/main/org/apache/cassandra/harry/visitors/RecentValidator.java @@ -0,0 +1,96 @@ +/* + * 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.visitors; + +import org.apache.cassandra.harry.core.MetricReporter; +import org.apache.cassandra.harry.core.Run; +import org.apache.cassandra.harry.gen.Surjections; +import org.apache.cassandra.harry.model.Model; +import org.apache.cassandra.harry.model.OpSelectors; +import org.apache.cassandra.harry.operations.Query; +import org.apache.cassandra.harry.operations.QueryGenerator; + +public class RecentValidator implements Visitor +{ + private final QueryLogger queryLogger; + private final Model model; + + private final OpSelectors.PdSelector pdSelector; + private final QueryGenerator.TypedQueryGenerator querySelector; + private final MetricReporter metricReporter; + private final OpSelectors.Clock clock; + + private final int partitionCount; + private final int queries; + + public RecentValidator(int partitionCount, + int queries, + Run run, + Model.ModelFactory modelFactory, + QueryLogger queryLogger) + { + this.partitionCount = partitionCount; + this.queries = Math.max(queries, 1); + this.metricReporter = run.metricReporter; + this.pdSelector = run.pdSelector; + this.clock = run.clock; + this.querySelector = new QueryGenerator.TypedQueryGenerator(run.rng, + // TODO: make query kind configurable + Surjections.enumValues(Query.QueryKind.class), + run.rangeSelector); + this.model = modelFactory.make(run); + this.queryLogger = queryLogger; + } + + // TODO: expose metric, how many times validated recent partitions + private int validateRecentPartitions() + { + long pos = pdSelector.maxPosition(clock.peek()); + + int maxPartitions = partitionCount; + while (pos >= 0 && maxPartitions > 0 && !Thread.currentThread().isInterrupted()) + { + long visitLts = pdSelector.minLtsAt(pos); + for (int i = 0; i < queries; i++) + { + metricReporter.validateRandomQuery(); + Query query = querySelector.inflate(visitLts, i); + // TODO: add pd skipping from shrinker here, too + log(i, query); + model.validate(query); + } + + pos--; + maxPartitions--; + } + + return partitionCount - maxPartitions; + } + + @Override + public void visit() + { + validateRecentPartitions(); + } + + private void log(int modifier, Query query) + { + queryLogger.println(String.format("PD: %d. Modifier: %d.\t%s", query.pd, modifier, query.toSelectStatement())); + } +} \ No newline at end of file diff --git a/test/harry/main/org/apache/cassandra/harry/visitors/ReplayingVisitor.java b/test/harry/main/org/apache/cassandra/harry/visitors/ReplayingVisitor.java new file mode 100644 index 0000000000..65fcdb87d9 --- /dev/null +++ b/test/harry/main/org/apache/cassandra/harry/visitors/ReplayingVisitor.java @@ -0,0 +1,73 @@ +/* + * 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.visitors; + +import java.util.Arrays; +import java.util.function.LongSupplier; + +public abstract class ReplayingVisitor extends LtsVisitor +{ + public ReplayingVisitor(VisitExecutor delegate, LongSupplier ltsSource) + { + super(delegate, ltsSource); + } + + @Override + public void visit(long lts) + { + replay(getVisit(lts)); + } + + public abstract Visit getVisit(long lts); + + public abstract void replayAll(); + + private void replay(Visit visit) + { + beforeLts(visit.lts, visit.pd); + for (Operation operation : visit.operations) + operation(operation); + afterLts(visit.lts, visit.pd); + } + + public static class Visit + { + public final long lts; + public final long pd; + public final Operation[] operations; + + public Visit(long lts, long pd, Operation[] operations) + { + this.lts = lts; + this.pd = pd; + this.operations = operations; + } + + public String toString() + { + return "Visit{" + + "lts=" + lts + + ", pd=" + pd + + ", operations=[" + Arrays.toString(operations) + + "]}"; + } + + + } +} \ No newline at end of file diff --git a/test/harry/main/org/apache/cassandra/harry/visitors/Sampler.java b/test/harry/main/org/apache/cassandra/harry/visitors/Sampler.java new file mode 100644 index 0000000000..0b2dced728 --- /dev/null +++ b/test/harry/main/org/apache/cassandra/harry/visitors/Sampler.java @@ -0,0 +1,97 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.cassandra.harry.visitors; + +import org.apache.commons.math3.stat.descriptive.DescriptiveStatistics; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.annotation.JsonProperty; +import com.fasterxml.jackson.annotation.JsonTypeName; +import org.apache.cassandra.harry.core.Configuration; +import org.apache.cassandra.harry.core.Run; +import org.apache.cassandra.harry.ddl.SchemaSpec; +import org.apache.cassandra.harry.gen.rng.RngUtils; +import org.apache.cassandra.harry.model.OpSelectors; +import org.apache.cassandra.harry.model.SelectHelper; +import org.apache.cassandra.harry.sut.SystemUnderTest; +import org.apache.cassandra.harry.tracker.DataTracker; + +public class Sampler implements Visitor +{ + private static final Logger logger = LoggerFactory.getLogger(AllPartitionsValidator.class); + + private final SystemUnderTest sut; + private final OpSelectors.PdSelector pdSelector; + private final OpSelectors.Clock clock; + private final DataTracker tracker; + private final SchemaSpec schema; + private final int samplePartitions; + + public Sampler(Run run, int samplePartitions) + { + this.sut = run.sut; + this.pdSelector = run.pdSelector; + this.clock = run.clock; + this.tracker = run.tracker; + this.schema = run.schemaSpec; + this.samplePartitions = samplePartitions; + } + + public void visit() + { + long lts = clock.peek(); + long max = pdSelector.maxPosition(tracker.maxStarted()); + DescriptiveStatistics ds = new DescriptiveStatistics(); + int empty = 0; + + long n = RngUtils.next(lts); + for (long i = 0; i < this.samplePartitions; i++) + { + long posLts = pdSelector.minLtsAt(RngUtils.asInt(n, (int) max)); + n = RngUtils.next(n); + // TODO: why not just pd at pos? + long pd = pdSelector.pd(posLts, schema); + long count = (long) sut.execute(SelectHelper.count(schema, pd), SystemUnderTest.ConsistencyLevel.ONE)[0][0]; + if (count == 0) + empty++; + ds.addValue(count); + } + logger.info("Visited {} partitions (sampled {} empty out of {}), with mean size of {}. Median: {}. Min: {}. Max: {}", + max, empty, samplePartitions, ds.getMean(), ds.getPercentile(0.5), ds.getMin(), ds.getMax()); + } + + @JsonTypeName("sampler") + public static class SamplerConfiguration implements Configuration.VisitorConfiguration + { + public final int sample_partitions; + + @JsonCreator + public SamplerConfiguration(@JsonProperty(value = "sample_partitions", defaultValue = "10") int sample_partitions) + { + this.sample_partitions = sample_partitions; + } + + public Visitor make(Run run) + { + return new Sampler(run, sample_partitions); + } + } +} diff --git a/test/harry/main/org/apache/cassandra/harry/visitors/SingleValidator.java b/test/harry/main/org/apache/cassandra/harry/visitors/SingleValidator.java new file mode 100644 index 0000000000..0186e991e3 --- /dev/null +++ b/test/harry/main/org/apache/cassandra/harry/visitors/SingleValidator.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.harry.visitors; + +import org.apache.cassandra.harry.core.Run; +import org.apache.cassandra.harry.model.Model; +import org.apache.cassandra.harry.operations.Query; +import org.apache.cassandra.harry.operations.QueryGenerator; + +public class SingleValidator implements Visitor +{ + protected final int iterations; + protected final Model model; + protected final QueryGenerator queryGenerator; + protected final Run run; + + public SingleValidator(int iterations, + Run run, + Model.ModelFactory modelFactory) + { + this.iterations = iterations; + this.model = modelFactory.make(run); + this.queryGenerator = new QueryGenerator(run); + this.run = run; + } + + @Override + public void visit() + { + visit(run.clock.peek()); + } + + public void visit(long lts) + { + model.validate(queryGenerator.inflate(lts, 0, Query.QueryKind.SINGLE_PARTITION)); + + for (boolean reverse : new boolean[]{ true, false }) + { + model.validate(Query.selectPartition(run.schemaSpec, run.pdSelector.pd(lts, run.schemaSpec), reverse)); + } + + for (Query.QueryKind queryKind : new Query.QueryKind[]{ Query.QueryKind.CLUSTERING_RANGE, Query.QueryKind.CLUSTERING_SLICE, Query.QueryKind.SINGLE_CLUSTERING }) + { + for (int i = 0; i < iterations; i++) + { + model.validate(queryGenerator.inflate(lts, i, queryKind)); + } + } + } +} diff --git a/test/harry/main/org/apache/cassandra/harry/visitors/SkippingVisitor.java b/test/harry/main/org/apache/cassandra/harry/visitors/SkippingVisitor.java new file mode 100644 index 0000000000..a8c5fb6ccb --- /dev/null +++ b/test/harry/main/org/apache/cassandra/harry/visitors/SkippingVisitor.java @@ -0,0 +1,56 @@ +/* + * 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.visitors; + +import java.util.Set; +import java.util.function.LongSupplier; + +public class SkippingVisitor extends LtsVisitor +{ + private final Set ltsToSkip; + private final Set pdsToSkip; + private final LtsToPd ltsToPd; + // Use DelegatingVisitor class instead of VisitExecutor available via protected field + private LtsVisitor delegateShadow; + + public SkippingVisitor(LtsVisitor delegate, + LongSupplier ltsSupplier, + LtsToPd ltsToPd, + Set ltsToSkip, + Set pdsToSkip) + { + super(delegate, ltsSupplier); + this.ltsToSkip = ltsToSkip; + this.pdsToSkip = pdsToSkip; + this.ltsToPd = ltsToPd; + } + + public void visit(long lts) + { + if (ltsToSkip.contains(lts) || pdsToSkip.contains(ltsToPd.convert(lts))) + return; + + delegateShadow.visit(lts); + } + + public static interface LtsToPd + { + public long convert(long lts); + } +} diff --git a/test/harry/main/org/apache/cassandra/harry/visitors/VisitExecutor.java b/test/harry/main/org/apache/cassandra/harry/visitors/VisitExecutor.java new file mode 100644 index 0000000000..a0e9c8e73c --- /dev/null +++ b/test/harry/main/org/apache/cassandra/harry/visitors/VisitExecutor.java @@ -0,0 +1,122 @@ +/* + * 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.visitors; + +import org.apache.cassandra.harry.model.OpSelectors; +import org.apache.cassandra.harry.operations.Query; +import org.apache.cassandra.harry.util.BitSet; + +public abstract class VisitExecutor +{ + protected abstract void beforeLts(long lts, long pd); + + protected abstract void afterLts(long lts, long pd); + + protected abstract void operation(Operation operation); + + public abstract void shutdown() throws InterruptedException; + + public interface WriteOp extends Operation + { + long cd(); + long[] vds(); + } + + public interface WriteStaticOp extends WriteOp + { + long[] sds(); + } + + public interface DeleteRowOp extends Operation + { + long cd(); + } + + public interface DeleteOp extends Operation + { + Query relations(); + } + + public interface DeleteColumnsOp extends Operation + { + long cd(); + BitSet columns(); + } + + public interface Operation + { + long pd(); + long lts(); + long opId(); + OpSelectors.OperationKind kind(); + } + + public static abstract class BaseOperation implements Operation + { + public final long lts; + public final long opId; + public final long pd; + public final OpSelectors.OperationKind kind; + + public BaseOperation(long lts, long pd, long opId, OpSelectors.OperationKind kind) + { + assert opId >= 0; + this.pd = pd; + this.lts = lts; + this.opId = opId; + this.kind = kind; + } + + @Override + public final long pd() + { + return pd; + + } + + @Override + public final long lts() + { + return lts; + } + + @Override + public final long opId() + { + return opId; + } + + @Override + public final OpSelectors.OperationKind kind() + { + return kind; + + } + + public String toString() + { + return "Operation{" + + " lts=" + lts + + " pd=" + pd + + " opId=" + opId + + ", kind=" + kind + + '}'; + } + } +} diff --git a/test/harry/main/org/apache/cassandra/harry/visitors/Visitor.java b/test/harry/main/org/apache/cassandra/harry/visitors/Visitor.java new file mode 100644 index 0000000000..ccac2ded57 --- /dev/null +++ b/test/harry/main/org/apache/cassandra/harry/visitors/Visitor.java @@ -0,0 +1,33 @@ +/* + * 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.visitors; + +import org.apache.cassandra.harry.core.Run; + +public interface Visitor +{ + void visit(); + + default void shutdown() throws InterruptedException {} + + interface VisitorFactory + { + Visitor make(Run run); + } +} \ No newline at end of file diff --git a/test/simulator/main/org/apache/cassandra/simulator/AlwaysDeliverNetworkScheduler.java b/test/simulator/main/org/apache/cassandra/simulator/AlwaysDeliverNetworkScheduler.java index f5721f5df6..ef5bdb1fd3 100644 --- a/test/simulator/main/org/apache/cassandra/simulator/AlwaysDeliverNetworkScheduler.java +++ b/test/simulator/main/org/apache/cassandra/simulator/AlwaysDeliverNetworkScheduler.java @@ -32,11 +32,11 @@ public class AlwaysDeliverNetworkScheduler implements FutureActionScheduler private final long delayNanos; - public AlwaysDeliverNetworkScheduler(SimulatedTime time, RandomSource randomSource) + public AlwaysDeliverNetworkScheduler(SimulatedTime time) { - this(time, randomSource, TimeUnit.MILLISECONDS.toNanos(10)); + this(time, TimeUnit.MILLISECONDS.toNanos(10)); } - public AlwaysDeliverNetworkScheduler(SimulatedTime time, RandomSource randomSource, long dealayNanos) + public AlwaysDeliverNetworkScheduler(SimulatedTime time, long dealayNanos) { this.time = time; this.delayNanos = dealayNanos; diff --git a/test/simulator/main/org/apache/cassandra/simulator/cluster/KeyspaceActions.java b/test/simulator/main/org/apache/cassandra/simulator/cluster/KeyspaceActions.java index 7551d31bbf..a6a78682a3 100644 --- a/test/simulator/main/org/apache/cassandra/simulator/cluster/KeyspaceActions.java +++ b/test/simulator/main/org/apache/cassandra/simulator/cluster/KeyspaceActions.java @@ -33,7 +33,7 @@ import org.apache.cassandra.dht.Murmur3Partitioner; import org.apache.cassandra.dht.Murmur3Partitioner.LongToken; import org.apache.cassandra.distributed.Cluster; import org.apache.cassandra.distributed.api.ConsistencyLevel; -import org.apache.cassandra.distributed.test.log.PlacementSimulator; +import org.apache.cassandra.harry.sut.TokenPlacementModel; import org.apache.cassandra.simulator.Action; import org.apache.cassandra.simulator.ActionList; import org.apache.cassandra.simulator.ActionListener; @@ -64,7 +64,7 @@ public class KeyspaceActions extends ClusterActions final EnumSet ops = EnumSet.noneOf(TopologyChange.class); final NodeLookup nodeLookup; - final PlacementSimulator.NodeFactory factory; + final TokenPlacementModel.NodeFactory factory; final int[] minRf, initialRf, maxRf; final int[] membersOfQuorumDcs; @@ -97,7 +97,7 @@ public class KeyspaceActions extends ClusterActions this.nodeLookup = simulated.snitch; - this.factory = new PlacementSimulator.NodeFactory(new SimulationLookup()); + this.factory = new TokenPlacementModel.NodeFactory(new SimulationLookup()); int[] dcSizes = new int[options.initialRf.length]; for (int dc : nodeLookup.nodeToDc) ++dcSizes[dc]; @@ -183,16 +183,16 @@ public class KeyspaceActions extends ClusterActions })); } - private PlacementSimulator.ReplicatedRanges placements(NodesByDc nodesByDc, int[] rfs) + private TokenPlacementModel.ReplicatedRanges placements(NodesByDc nodesByDc, int[] rfs) { - List nodes = new ArrayList<>(); + List nodes = new ArrayList<>(); for (int dcIdx = 0; dcIdx < nodesByDc.dcs.length; dcIdx++) { int[] nodesInDc = nodesByDc.dcs[dcIdx]; for (int i = 0; i < nodesByDc.dcSizes[dcIdx]; i++) { int nodeIdx = nodesInDc[i]; - PlacementSimulator.Node node = factory.make(nodeIdx,nodeIdx, 1); + TokenPlacementModel.Node node = factory.make(nodeIdx,nodeIdx, 1); nodes.add(node); assert node.token() == tokenOf(nodeIdx); } @@ -202,12 +202,12 @@ public class KeyspaceActions extends ClusterActions for (int i = 0; i < rfs.length; i++) rf.put(factory.lookup().dc(i + 1), rfs[i]); - nodes.sort(PlacementSimulator.Node::compareTo); - return new PlacementSimulator.NtsReplicationFactor(rfs).replicate(nodes); + nodes.sort(TokenPlacementModel.Node::compareTo); + return new TokenPlacementModel.NtsReplicationFactor(rfs).replicate(nodes); } - private Topology recomputeTopology(PlacementSimulator.ReplicatedRanges readPlacements, - PlacementSimulator.ReplicatedRanges writePlacements) + private Topology recomputeTopology(TokenPlacementModel.ReplicatedRanges readPlacements, + TokenPlacementModel.ReplicatedRanges writePlacements) { int[][] replicasForKey = new int[primaryKeys.length][]; int[][] pendingReplicasForKey = new int[primaryKeys.length][]; @@ -215,14 +215,14 @@ public class KeyspaceActions extends ClusterActions { int primaryKey = primaryKeys[i]; LongToken token = new Murmur3Partitioner().getToken(Int32Type.instance.decompose(primaryKey)); - List readReplicas = readPlacements.replicasFor(token.token); - List writeReplicas = writePlacements.replicasFor(token.token); + List readReplicas = readPlacements.replicasFor(token.token); + List writeReplicas = writePlacements.replicasFor(token.token); - replicasForKey[i] = readReplicas.stream().mapToInt(PlacementSimulator.Node::idx).toArray(); - Set pendingReplicas = new HashSet<>(writeReplicas); + replicasForKey[i] = readReplicas.stream().mapToInt(TokenPlacementModel.Node::idx).toArray(); + Set pendingReplicas = new HashSet<>(writeReplicas); pendingReplicas.removeAll(readReplicas); - replicasForKey[i] = readReplicas.stream().mapToInt(PlacementSimulator.Node::idx).toArray(); - pendingReplicasForKey[i] = pendingReplicas.stream().mapToInt(PlacementSimulator.Node::idx).toArray(); + replicasForKey[i] = readReplicas.stream().mapToInt(TokenPlacementModel.Node::idx).toArray(); + pendingReplicasForKey[i] = pendingReplicas.stream().mapToInt(TokenPlacementModel.Node::idx).toArray(); } int[] membersOfRing = joined.toArray(); @@ -262,10 +262,10 @@ public class KeyspaceActions extends ClusterActions case JOIN: { Topology before = topology; - PlacementSimulator.ReplicatedRanges placementsBefore = placements(joined, currentRf); + TokenPlacementModel.ReplicatedRanges placementsBefore = placements(joined, currentRf); int join = registered.removeRandom(random, dc); joined.add(join); - PlacementSimulator.ReplicatedRanges placementsAfter = placements(joined, currentRf); + TokenPlacementModel.ReplicatedRanges placementsAfter = placements(joined, currentRf); Topology during = recomputeTopology(placementsBefore, placementsAfter); updateTopology(during); Topology after = recomputeTopology(placementsAfter, placementsAfter); @@ -275,13 +275,13 @@ public class KeyspaceActions extends ClusterActions case REPLACE: { Topology before = topology; - PlacementSimulator.ReplicatedRanges placementsBefore = placements(joined, currentRf); + TokenPlacementModel.ReplicatedRanges placementsBefore = placements(joined, currentRf); int join = registered.removeRandom(random, dc); int leave = joined.selectRandom(random, dc); joined.add(join); joined.remove(leave); left.add(leave); - PlacementSimulator.ReplicatedRanges placementsAfter = placements(joined, currentRf); + TokenPlacementModel.ReplicatedRanges placementsAfter = placements(joined, currentRf); nodeLookup.setTokenOf(join, nodeLookup.tokenOf(leave)); Topology during = recomputeTopology(placementsBefore, placementsAfter); updateTopology(during); @@ -296,10 +296,10 @@ public class KeyspaceActions extends ClusterActions case LEAVE: { Topology before = topology; - PlacementSimulator.ReplicatedRanges placementsBefore = placements(joined, currentRf); + TokenPlacementModel.ReplicatedRanges placementsBefore = placements(joined, currentRf); int leave = joined.removeRandom(random, dc); left.add(leave); - PlacementSimulator.ReplicatedRanges placementsAfter = placements(joined, currentRf); + TokenPlacementModel.ReplicatedRanges placementsAfter = placements(joined, currentRf); Topology during = recomputeTopology(placementsBefore, placementsAfter); updateTopology(during); Topology after = recomputeTopology(placementsAfter, placementsAfter); @@ -375,7 +375,7 @@ public class KeyspaceActions extends ClusterActions private Topology recomputeTopology() { - PlacementSimulator.ReplicatedRanges ranges = placements(joined, currentRf); + TokenPlacementModel.ReplicatedRanges ranges = placements(joined, currentRf); return recomputeTopology(ranges, ranges); } @@ -408,7 +408,7 @@ public class KeyspaceActions extends ClusterActions return Long.parseLong(cluster.get(nodeLookup.tokenOf(node)).config().getString("initial_token")); } - public class SimulationLookup extends PlacementSimulator.DefaultLookup + public class SimulationLookup extends TokenPlacementModel.DefaultLookup { public String dc(int dcIdx) { @@ -425,7 +425,7 @@ public class KeyspaceActions extends ClusterActions return Long.parseLong(cluster.get(nodeLookup.tokenOf(tokenIdx)).config().getString("initial_token")); } - public PlacementSimulator.Lookup forceToken(int tokenIdx, long token) + public TokenPlacementModel.Lookup forceToken(int tokenIdx, long token) { SimulationLookup newLookup = new SimulationLookup(); newLookup.overrides.putAll(overrides); diff --git a/test/simulator/main/org/apache/cassandra/simulator/harry/HarryValidatingQuery.java b/test/simulator/main/org/apache/cassandra/simulator/harry/HarryValidatingQuery.java index 871f58d429..3309bc87dd 100644 --- a/test/simulator/main/org/apache/cassandra/simulator/harry/HarryValidatingQuery.java +++ b/test/simulator/main/org/apache/cassandra/simulator/harry/HarryValidatingQuery.java @@ -24,16 +24,16 @@ import java.util.List; import org.slf4j.Logger; import org.slf4j.LoggerFactory; -import harry.core.Run; -import harry.data.ResultSetRow; -import harry.model.Model; -import harry.model.SelectHelper; -import harry.model.sut.TokenPlacementModel; -import harry.model.sut.injvm.QuiescentLocalStateChecker; -import harry.operations.CompiledStatement; -import harry.operations.Query; +import org.apache.cassandra.harry.core.Run; +import org.apache.cassandra.harry.data.ResultSetRow; +import org.apache.cassandra.harry.model.Model; +import org.apache.cassandra.harry.model.SelectHelper; +import org.apache.cassandra.harry.operations.CompiledStatement; +import org.apache.cassandra.harry.operations.Query; import org.apache.cassandra.distributed.Cluster; import org.apache.cassandra.distributed.api.IInstance; +import org.apache.cassandra.harry.sut.TokenPlacementModel; +import org.apache.cassandra.harry.sut.injvm.QuiescentLocalStateChecker; import org.apache.cassandra.simulator.systems.InterceptedExecution; import org.apache.cassandra.simulator.systems.InterceptingExecutor; import org.apache.cassandra.simulator.systems.SimulatedAction; @@ -124,7 +124,7 @@ public class HarryValidatingQuery extends SimulatedAction { IInstance instance = cluster .stream() - .filter((n) -> n.config().broadcastAddress().toString().equals(node.id)) + .filter((n) -> n.config().broadcastAddress().toString().equals(node.id())) .findFirst() .get(); return instance.executeInternal(statement, bindings); diff --git a/test/simulator/test/org/apache/cassandra/simulator/test/HarrySimulatorTest.java b/test/simulator/test/org/apache/cassandra/simulator/test/HarrySimulatorTest.java index d138b96f1c..81e70079aa 100644 --- a/test/simulator/test/org/apache/cassandra/simulator/test/HarrySimulatorTest.java +++ b/test/simulator/test/org/apache/cassandra/simulator/test/HarrySimulatorTest.java @@ -39,18 +39,18 @@ import org.junit.Test; import org.slf4j.Logger; import org.slf4j.LoggerFactory; -import harry.core.Configuration; -import harry.core.Run; -import harry.ddl.ColumnSpec; -import harry.ddl.SchemaGenerators; -import harry.ddl.SchemaSpec; -import harry.generators.Surjections; -import harry.model.clock.OffsetClock; -import harry.model.sut.SystemUnderTest; -import harry.model.sut.TokenPlacementModel; -import harry.operations.Query; -import harry.runner.DefaultDataTracker; -import harry.visitors.GeneratingVisitor; +import org.apache.cassandra.harry.clock.OffsetClock; +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.operations.Query; +import org.apache.cassandra.harry.sut.SystemUnderTest; +import org.apache.cassandra.harry.sut.injvm.InJvmSut; +import org.apache.cassandra.harry.tracker.DefaultDataTracker; +import org.apache.cassandra.harry.visitors.GeneratingVisitor; import io.airlift.airline.Command; import io.airlift.airline.HelpOption; import io.airlift.airline.Option; @@ -61,9 +61,9 @@ import org.apache.cassandra.distributed.api.ConsistencyLevel; import org.apache.cassandra.distributed.api.IInstanceConfig; import org.apache.cassandra.distributed.api.IInvokableInstance; 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.harry.HarryHelper; import org.apache.cassandra.distributed.shared.WithProperties; +import org.apache.cassandra.harry.sut.TokenPlacementModel; import org.apache.cassandra.locator.InetAddressAndPort; import org.apache.cassandra.net.Verb; import org.apache.cassandra.schema.ReplicationParams; @@ -103,6 +103,7 @@ import org.apache.cassandra.utils.CloseableIterator; import static java.util.concurrent.TimeUnit.SECONDS; import static org.apache.cassandra.distributed.api.ConsistencyLevel.ALL; +import static org.apache.cassandra.harry.sut.TokenPlacementModel.constantLookup; import static org.apache.cassandra.simulator.ActionSchedule.Mode.UNLIMITED; import static org.apache.cassandra.simulator.cluster.ClusterActions.Options.noActions; @@ -465,7 +466,7 @@ public class HarrySimulatorTest @Override public FutureActionScheduler futureActionScheduler(int nodeCount, SimulatedTime time, RandomSource random) { - return new AlwaysDeliverNetworkScheduler(time, random); + return new AlwaysDeliverNetworkScheduler(time); } @Override @@ -574,7 +575,7 @@ public class HarrySimulatorTest else if (somewhatLossy.contains(verb)) schedulers.put(verb, new FixedLossNetworkScheduler(nodes, random, time, KindOfSequence.UNIFORM, .1f, .15f)); else if (somewhatSlow.contains(verb)) - schedulers.put(verb, new AlwaysDeliverNetworkScheduler(time, random, TimeUnit.MILLISECONDS.toNanos(100))); + schedulers.put(verb, new AlwaysDeliverNetworkScheduler(time, TimeUnit.MILLISECONDS.toNanos(100))); } return schedulers; } @@ -834,12 +835,14 @@ public class HarrySimulatorTest IInstanceConfig config = simulation.cluster.get(nodeId).config(); InetAddressAndPort addr = InetAddressAndPort.getByAddress(config.broadcastAddress()); - TokenPlacementModel.Node node = new TokenPlacementModel.Node(Long.parseLong(config.getString("initial_token")), - addr.toString(), - new TokenPlacementModel.Location(simulation.clusterActions.snitch.get().getDatacenter(addr), - simulation.clusterActions.snitch.get().getRack(addr))); + + TokenPlacementModel.Node node = new TokenPlacementModel.Node(0, 0, 0, 0, + constantLookup(addr.toString(), + Long.parseLong(config.getString("initial_token")), + simulation.clusterActions.snitch.get().getDatacenter(addr), + simulation.clusterActions.snitch.get().getRack(addr))); nodesLookup[i] = node; - nodesByDc.computeIfAbsent(node.location.dc, (k) -> new ArrayList<>()).add(node); + nodesByDc.computeIfAbsent(node.dc(), (k) -> new ArrayList<>()).add(node); idByAddr.put(addr.toString(), config.num()); } this.ring = new ArrayList<>(); @@ -850,7 +853,7 @@ public class HarrySimulatorTest int[] joined = new int[ring.size()]; for (int i = 0; i < ring.size(); i++) { - joined[i] = idByAddr.get(ring.get(i).id); + joined[i] = idByAddr.get(ring.get(i).id()); } return joined; } @@ -894,7 +897,7 @@ public class HarrySimulatorTest for (TokenPlacementModel.Node node : ring) { if (prev != null) - assert node.token > prev.token : "Ring doesn't seem to be sorted: " + ring; + assert node.token() > prev.token() : "Ring doesn't seem to be sorted: " + ring; prev = node; } } @@ -915,14 +918,18 @@ public class HarrySimulatorTest for (TokenPlacementModel.Node node : ring) { if (prev != null) - assert node.token > prev.token : "Ring doesn't seem to be sorted: " + ring; + assert node.token() > prev.token() : "Ring doesn't seem to be sorted: " + ring; prev = node; } } private static TokenPlacementModel.Node withToken(TokenPlacementModel.Node node, long token) { - return new TokenPlacementModel.Node(token, node.id, node.location); + return new TokenPlacementModel.Node(0, 0, 0, 0, + constantLookup(node.id(), + token, + node.dc(), + node.rack())); } public String toString() diff --git a/test/simulator/test/org/apache/cassandra/simulator/test/SimulatedVisitExectuor.java b/test/simulator/test/org/apache/cassandra/simulator/test/SimulatedVisitExectuor.java index 2938bb1822..d676ec8409 100644 --- a/test/simulator/test/org/apache/cassandra/simulator/test/SimulatedVisitExectuor.java +++ b/test/simulator/test/org/apache/cassandra/simulator/test/SimulatedVisitExectuor.java @@ -25,12 +25,11 @@ import java.util.List; import org.slf4j.Logger; import org.slf4j.LoggerFactory; -import harry.core.Run; -import harry.model.OpSelectors; -import harry.operations.CompiledStatement; -import harry.runner.DataTracker; -import harry.visitors.MutatingRowVisitor; -import harry.visitors.VisitExecutor; +import org.apache.cassandra.harry.core.Run; +import org.apache.cassandra.harry.operations.CompiledStatement; +import org.apache.cassandra.harry.tracker.DataTracker; +import org.apache.cassandra.harry.visitors.MutatingRowVisitor; +import org.apache.cassandra.harry.visitors.VisitExecutor; import org.apache.cassandra.distributed.api.ConsistencyLevel; import org.apache.cassandra.distributed.impl.Query; import org.apache.cassandra.simulator.Action; @@ -123,29 +122,23 @@ public class SimulatedVisitExectuor extends VisitExecutor return current; } + @Override protected void beforeLts(long lts, long pd) { this.lts.add(lts); } - protected void afterBatch(long lts, long pd, long m) - { - } - - protected void beforeBatch(long lts, long pd, long m) - { - } - - public void operation(long lts, long pd, long cd, long m, long opId, OpSelectors.OperationKind opType) - { - CompiledStatement statement = rowVisitor.perform(opType, lts, pd, cd, opId); - statements.add(statement.cql()); - Collections.addAll(bindings, statement.bindings()); - } - + @Override protected void afterLts(long lts, long pd) { + } + @Override + protected void operation(Operation operation) + { + CompiledStatement statement = rowVisitor.perform(operation); + statements.add(statement.cql()); + Collections.addAll(bindings, statement.bindings()); } public void shutdown() throws InterruptedException diff --git a/test/unit/org/apache/cassandra/tcm/ClusterMetadataTest.java b/test/unit/org/apache/cassandra/tcm/ClusterMetadataTest.java index 9896167a50..249ea32551 100644 --- a/test/unit/org/apache/cassandra/tcm/ClusterMetadataTest.java +++ b/test/unit/org/apache/cassandra/tcm/ClusterMetadataTest.java @@ -27,7 +27,7 @@ import org.junit.Test; import org.apache.cassandra.ServerTestUtils; import org.apache.cassandra.distributed.test.log.ClusterMetadataTestHelper; import org.apache.cassandra.distributed.test.log.CMSTestBase; -import org.apache.cassandra.distributed.test.log.PlacementSimulator; +import org.apache.cassandra.harry.sut.TokenPlacementModel; import org.apache.cassandra.tcm.sequences.BootstrapAndJoin; import org.apache.cassandra.tcm.sequences.UnbootstrapAndLeave; import org.apache.cassandra.schema.KeyspaceMetadata; @@ -50,7 +50,7 @@ public class ClusterMetadataTest public void before() throws ExecutionException, InterruptedException { ClusterMetadataService.unsetInstance(); - new CMSTestBase.CMSSut(AtomicLongBackedProcessor::new, false, new PlacementSimulator.SimpleReplicationFactor(3)); + new CMSTestBase.CMSSut(AtomicLongBackedProcessor::new, false, new TokenPlacementModel.SimpleReplicationFactor(3)); } @Test diff --git a/test/unit/org/apache/cassandra/tcm/ownership/UniformRangePlacementIntegrationTest.java b/test/unit/org/apache/cassandra/tcm/ownership/UniformRangePlacementIntegrationTest.java index bf6b84fdd7..322acdc4ed 100644 --- a/test/unit/org/apache/cassandra/tcm/ownership/UniformRangePlacementIntegrationTest.java +++ b/test/unit/org/apache/cassandra/tcm/ownership/UniformRangePlacementIntegrationTest.java @@ -32,7 +32,7 @@ import org.apache.cassandra.ServerTestUtils; import org.apache.cassandra.distributed.test.log.CMSTestBase; import org.apache.cassandra.distributed.test.log.ClusterMetadataTestHelper; import org.apache.cassandra.distributed.test.log.MetadataChangeSimulationTest; -import org.apache.cassandra.distributed.test.log.PlacementSimulator; +import org.apache.cassandra.harry.sut.TokenPlacementModel; import org.apache.cassandra.schema.Keyspaces; import org.apache.cassandra.schema.ReplicationParams; import org.apache.cassandra.tcm.AtomicLongBackedProcessor; @@ -41,7 +41,7 @@ import org.apache.cassandra.tcm.ClusterMetadataService; public class UniformRangePlacementIntegrationTest { - private static final PlacementSimulator.ReplicationFactor RF = new PlacementSimulator.NtsReplicationFactor(3, 3); + private static final TokenPlacementModel.ReplicationFactor RF = new TokenPlacementModel.NtsReplicationFactor(3, 3); private CMSTestBase.CMSSut sut; @BeforeClass public static void beforeClass() @@ -68,29 +68,29 @@ public class UniformRangePlacementIntegrationTest UniformRangePlacement rangePlacement = new UniformRangePlacement(); Random rng = new Random(1); int idx = 1; - PlacementSimulator.NodeFactory factory = PlacementSimulator.nodeFactory(); - List nodes = new ArrayList<>(); + TokenPlacementModel.NodeFactory factory = TokenPlacementModel.nodeFactory(); + List nodes = new ArrayList<>(); for (int i = 0; i < 5; i++) { for (int j = 1; j <= 3; j++) { int dc = j; int rack = (rng.nextInt(3) + 1); - PlacementSimulator.Node node = factory.make(idx, dc, rack); + TokenPlacementModel.Node node = factory.make(idx, dc, rack); ClusterMetadataTestHelper.register(idx, node.dc(), node.rack()); ClusterMetadataTestHelper.join(idx, node.token()); nodes.add(node); idx++; } } - nodes.sort(PlacementSimulator.Node::compareTo); + nodes.sort(TokenPlacementModel.Node::compareTo); ClusterMetadataService.instance().processor().fetchLogAndWait(); DataPlacements placements = rangePlacement.calculatePlacements(ClusterMetadata.current().epoch, ClusterMetadata.current(), Keyspaces.of(ClusterMetadata.current().schema.getKeyspaces().get("test").get())); - PlacementSimulator.ReplicatedRanges predicted = RF.replicate(nodes); + TokenPlacementModel.ReplicatedRanges predicted = RF.replicate(nodes); ReplicationParams replicationParams = ClusterMetadata.current().schema.getKeyspaces().get("test").get().params.replication; MetadataChangeSimulationTest.match(placements.get(replicationParams).reads, diff --git a/test/unit/org/apache/cassandra/tcm/sequences/ProgressBarrierTest.java b/test/unit/org/apache/cassandra/tcm/sequences/ProgressBarrierTest.java index e5c5ab4145..e3aaf69eab 100644 --- a/test/unit/org/apache/cassandra/tcm/sequences/ProgressBarrierTest.java +++ b/test/unit/org/apache/cassandra/tcm/sequences/ProgressBarrierTest.java @@ -33,17 +33,17 @@ import java.util.stream.Collectors; import org.junit.Assert; import org.junit.Test; -import harry.generators.PCGFastPure; -import harry.generators.PcgRSUFast; -import harry.generators.RandomGenerator; -import harry.generators.Surjections; import org.apache.cassandra.config.DatabaseDescriptor; import org.apache.cassandra.db.ConsistencyLevel; import org.apache.cassandra.distributed.api.IIsolatedExecutor; import org.apache.cassandra.distributed.test.log.CMSTestBase; -import org.apache.cassandra.distributed.test.log.PlacementSimulator; import org.apache.cassandra.distributed.test.log.RngUtils; import org.apache.cassandra.exceptions.RequestFailureReason; +import org.apache.cassandra.harry.gen.EntropySource; +import org.apache.cassandra.harry.gen.Surjections; +import org.apache.cassandra.harry.gen.rng.PCGFastPure; +import org.apache.cassandra.harry.gen.rng.PcgRSUFast; +import org.apache.cassandra.harry.sut.TokenPlacementModel; import org.apache.cassandra.locator.InetAddressAndPort; import org.apache.cassandra.net.ConnectionType; import org.apache.cassandra.net.Message; @@ -74,25 +74,25 @@ public class ProgressBarrierTest extends CMSTestBase @Test public void testProgressBarrier() throws Throwable { - RandomGenerator rng = new PcgRSUFast(1L, 1l); + EntropySource rng = new PcgRSUFast(1L, 1l); Supplier respond = bools().toGenerator().bind(rng); - Supplier rfs = combine(ints(0, 3), - ints(1, 5), - bools(), - ints(1, 5), - (Integer dcs, Integer nodesPerDc, Boolean addAlternate, Integer nodesPerDcAlt) -> { + Supplier rfs = combine(ints(0, 3), + ints(1, 5), + bools(), + ints(1, 5), + (Integer dcs, Integer nodesPerDc, Boolean addAlternate, Integer nodesPerDcAlt) -> { if (dcs == 0) - return new PlacementSimulator.SimpleReplicationFactor(nodesPerDc); + return new TokenPlacementModel.SimpleReplicationFactor(nodesPerDc); else if (addAlternate && nodesPerDcAlt.intValue() != nodesPerDc.intValue()) { int[] perDc = new int[dcs + 1]; Arrays.fill(perDc, nodesPerDc); perDc[perDc.length - 1] = nodesPerDcAlt; - return new PlacementSimulator.NtsReplicationFactor(perDc); + return new TokenPlacementModel.NtsReplicationFactor(perDc); } else { - return new PlacementSimulator.NtsReplicationFactor(dcs, nodesPerDc); + return new TokenPlacementModel.NtsReplicationFactor(dcs, nodesPerDc); } }) .toGenerator() @@ -106,14 +106,14 @@ public class ProgressBarrierTest extends CMSTestBase ConsistencyLevel.ONE).toGenerator() .bind(rng); - PlacementSimulator.NodeFactory nodeFactory = PlacementSimulator.nodeFactory(); + TokenPlacementModel.NodeFactory nodeFactory = TokenPlacementModel.nodeFactory(); for (int run = 0; run < 100; run++) { - PlacementSimulator.ReplicationFactor rf = rfs.get(); + TokenPlacementModel.ReplicationFactor rf = rfs.get(); try (CMSTestBase.CMSSut sut = new CMSTestBase.CMSSut(AtomicLongBackedProcessor::new, false, rf)) { - List allNodes = new ArrayList<>(); - PlacementSimulator.Node node = null; + List allNodes = new ArrayList<>(); + TokenPlacementModel.Node node = null; int nodesInCluster = Math.max(rf.total(), nodes.get()); for (int i = 1; i <= nodesInCluster; i++) { @@ -210,7 +210,7 @@ public class ProgressBarrierTest extends CMSTestBase replicas.sort(InetAddressAndPort::compareTo); Set collected = responded.stream().filter(replicas::contains).collect(Collectors.toSet()); int expected; - if (rf instanceof PlacementSimulator.SimpleReplicationFactor) + if (rf instanceof TokenPlacementModel.SimpleReplicationFactor) expected = rf.total() / 2 + 1; else expected = rf.asMap().get(dc) / 2 + 1; @@ -230,7 +230,7 @@ public class ProgressBarrierTest extends CMSTestBase .forEach(n -> byDc.compute(metadata.directory.location(n).datacenter, (k, v) -> v == null ? 1 : v + 1)); - if (rf instanceof PlacementSimulator.SimpleReplicationFactor) + if (rf instanceof TokenPlacementModel.SimpleReplicationFactor) { int actual = byDc.get(dc); int expected = rf.asMap().get(dc) / 2 + 1; @@ -275,13 +275,13 @@ public class ProgressBarrierTest extends CMSTestBase @Test public void testProgressBarrierDegradingConsistency() throws Throwable { - PlacementSimulator.ReplicationFactor rf = new PlacementSimulator.NtsReplicationFactor(5, 5); - PlacementSimulator.NodeFactory nodeFactory = PlacementSimulator.nodeFactory(); + TokenPlacementModel.ReplicationFactor rf = new TokenPlacementModel.NtsReplicationFactor(5, 5); + TokenPlacementModel.NodeFactory nodeFactory = TokenPlacementModel.nodeFactory(); DatabaseDescriptor.setProgressBarrierMinConsistencyLevel(ConsistencyLevel.ONE); try (CMSTestBase.CMSSut sut = new CMSTestBase.CMSSut(AtomicLongBackedProcessor::new, false, rf)) { - PlacementSimulator.Node node = null; + TokenPlacementModel.Node node = null; for (int i = 1; i <= 4; i++) { node = nodeFactory.make(i, (i % rf.dcs()) + 1, 1);