Bring Harry into C* Tree

Patch by Alex Petrov; reviewed by Caleb Rackliffe and Marcus Eriksson for CASSANDRA-19210.
This commit is contained in:
Alex Petrov 2023-12-10 18:30:26 +01:00
parent b757dad43f
commit 439d1b122a
180 changed files with 25070 additions and 2578 deletions

View File

@ -106,10 +106,6 @@
<groupId>org.apache.ant</groupId>
<artifactId>ant-junit</artifactId>
</dependency>
<dependency>
<groupId>org.apache.cassandra</groupId>
<artifactId>harry-core</artifactId>
</dependency>
<dependency>
<groupId>org.junit</groupId>
<artifactId>junit-bom</artifactId>

View File

@ -513,12 +513,6 @@
</exclusion>
</exclusions>
</dependency>
<dependency>
<groupId>org.apache.cassandra</groupId>
<artifactId>harry-core</artifactId>
<version>0.0.2</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.reflections</groupId>
<artifactId>reflections</artifactId>

View File

@ -87,6 +87,7 @@
<property name="test.simulator-asm.src" value="${test.dir}/simulator/asm"/>
<property name="test.simulator-bootstrap.src" value="${test.dir}/simulator/bootstrap"/>
<property name="test.simulator-test.src" value="${test.dir}/simulator/test"/>
<property name="test.harry.src" value="${test.dir}/harry/main"/>
<property name="test.driver.connection_timeout_ms" value="10000"/>
<property name="test.driver.read_timeout_ms" value="24000"/>
<property name="test.jvm.args" value="" />
@ -1043,6 +1044,7 @@
<src path="${test.simulator-asm.src}"/>
<src path="${test.simulator-bootstrap.src}"/>
<src path="${test.simulator-test.src}"/>
<src path="${test.harry.src}"/>
<compilerarg line="${jdk11plus-javac-exports}"/>
</javac>

View File

@ -41,6 +41,7 @@
<sourceFolder url="file://$MODULE_DIR$/test/simulator/bootstrap" isTestSource="true" />
<sourceFolder url="file://$MODULE_DIR$/test/simulator/main" isTestSource="true" />
<sourceFolder url="file://$MODULE_DIR$/test/simulator/test" isTestSource="true" />
<sourceFolder url="file://$MODULE_DIR$/test/harry/main" isTestSource="true" />
<sourceFolder url="file://$MODULE_DIR$/test/resources" type="java-test-resource" />
<sourceFolder url="file://$MODULE_DIR$/test/conf" type="java-test-resource" />
<excludeFolder url="file://$MODULE_DIR$/.idea" />

File diff suppressed because one or more lines are too long

View File

@ -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"),

View File

@ -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: {}

View File

@ -1,123 +0,0 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.apache.cassandra.distributed.fuzz;
import java.io.File;
import java.util.concurrent.TimeUnit;
import java.util.function.Supplier;
import org.junit.Test;
import harry.core.Configuration;
import harry.core.Run;
import harry.ddl.SchemaSpec;
import harry.generators.Surjections;
import harry.model.sut.injvm.InJvmSut;
import harry.runner.LockingDataTracker;
import harry.runner.Runner;
import harry.visitors.MutatingVisitor;
import harry.visitors.QueryLogger;
import harry.visitors.RandomPartitionValidator;
import org.apache.cassandra.distributed.api.ICluster;
import org.apache.cassandra.distributed.api.IInvokableInstance;
import org.apache.cassandra.distributed.test.TestBaseImpl;
import static harry.core.Configuration.VisitorPoolConfiguration.pool;
import static java.util.Arrays.asList;
import static org.apache.cassandra.distributed.api.Feature.GOSSIP;
import static org.apache.cassandra.distributed.api.Feature.NATIVE_PROTOCOL;
import static org.apache.cassandra.distributed.api.Feature.NETWORK;
public class ConcurrentQuiescentCheckerIntegrationTest extends TestBaseImpl
{
private static Supplier<SchemaSpec> schemaSupplier()
{
Surjections.Surjection<SchemaSpec> schemaGen = HarryHelper.schemaSpecGen("harry", "table_");
return new Supplier<SchemaSpec>()
{
int schemaIdx = 0;
public SchemaSpec get()
{
while (true)
{
SchemaSpec schema = schemaGen.inflate(schemaIdx++);
try
{
schema.validate();
return schema;
}
catch (AssertionError e)
{
continue;
}
}
}
};
}
@Test
public void testWithConcurrentQuiescentChecker() throws Throwable
{
final int writeThreads = 2;
final int readThreads = 2;
try (ICluster<IInvokableInstance> cluster = builder().withNodes(3)
.withConfig(config -> config.with(GOSSIP, NETWORK, NATIVE_PROTOCOL))
.start())
{
Supplier<SchemaSpec> schemaSupplier = schemaSupplier();
for (int i = 0; i < 3; i++)
{
SchemaSpec schema = schemaSupplier.get();
Configuration config = HarryHelper
.defaultConfiguration()
.setSeed(1L)
.setClock(new Configuration.ApproximateMonotonicClockConfiguration((int) TimeUnit.MINUTES.toSeconds(20), 1, TimeUnit.SECONDS))
.setSUT(() -> new InJvmSut(cluster))
.setKeyspaceDdl("CREATE KEYSPACE IF NOT EXISTS " + schema.keyspace + " WITH replication = {'class': 'SimpleStrategy', 'replication_factor': 3};")
.setSchemaProvider((seed, sut) -> schema)
.setCreateSchema(true)
.setDropSchema(true)
.setDataTracker(LockingDataTracker::new)
.build();
Run run = config.createRun();
try
{
new Runner.ConcurrentRunner(config.createRun(), config,
asList(pool("Writer", writeThreads, MutatingVisitor::new),
pool("Reader", readThreads, (run_) -> new RandomPartitionValidator(run_, new Configuration.QuiescentCheckerConfig(), QueryLogger.NO_OP))),
1, TimeUnit.MINUTES)
.run();
}
catch (Throwable e)
{
Configuration.toFile(new File(String.format("failure-%d.yaml", config.seed)),
config.unbuild()
.setClock(run.clock.toConfig())
.setDataTracker(run.tracker.toConfig())
.build());
throw e;
}
}
}
}
}

View File

@ -1,132 +0,0 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.apache.cassandra.distributed.fuzz;
import java.util.Collection;
import org.junit.AfterClass;
import org.junit.BeforeClass;
import harry.core.Configuration;
import harry.core.Run;
import harry.ddl.SchemaSpec;
import harry.model.clock.OffsetClock;
import org.apache.cassandra.Util;
import org.apache.cassandra.db.ColumnFamilyStore;
import org.apache.cassandra.db.Keyspace;
import org.apache.cassandra.distributed.Cluster;
import org.apache.cassandra.distributed.test.TestBaseImpl;
import org.apache.cassandra.io.sstable.format.SSTableReader;
public abstract class FuzzTestBase extends TestBaseImpl
{
protected static Configuration configuration;
public static final int RF = 2;
static
{
try
{
HarryHelper.init();
configuration = HarryHelper.defaultConfiguration().build();
}
catch (Exception e)
{
throw new RuntimeException(e);
}
}
protected static Cluster cluster;
@BeforeClass
public static void beforeClassOverride() throws Throwable
{
cluster = Cluster.build(2)
.start();
init(cluster, RF);
}
@AfterClass
public static void afterClass()
{
if (cluster != null)
cluster.close();
}
public void reset()
{
cluster.schemaChange("DROP KEYSPACE IF EXISTS " + KEYSPACE);
init(cluster, RF);
}
/**
* Helped method to generate a {@code number} of sstables for the given {@code schemaSpec}.
*/
@SuppressWarnings("unused")
public static void generateTables(SchemaSpec schemaSpec, int number)
{
Run run = configuration.unbuild()
.setSeed(1)
.setSchemaProvider(new FixedSchemaProviderConfiguration(schemaSpec))
.setClock(() -> new OffsetClock(10000L))
.setDropSchema(false)
.setCreateSchema(false)
.build()
.createRun();
ColumnFamilyStore store = Keyspace.open(schemaSpec.keyspace).getColumnFamilyStore(schemaSpec.table);
store.disableAutoCompaction();
SSTableGenerator gen = new SSTableGenerator(run, store);
SSTableGenerator largePartitionGen = new SSTableWithLargePartition(run, store);
for (int i = 0; i < number; i++)
{
if (i % 3 == 0)
largePartitionGen.gen(1_000);
else
gen.gen(1_000);
}
}
/**
* Helper class to force generation of a fixed partition size.
*/
private static class SSTableWithLargePartition extends SSTableGenerator
{
public SSTableWithLargePartition(Run run, ColumnFamilyStore store)
{
super(run, store);
}
@Override
public Collection<SSTableReader> gen(int rows)
{
long lts = 0;
for (int i = 0; i < rows; i++)
{
long current = lts++;
write(current, current, current, current, true).applyUnsafe();
if (schema.staticColumns != null)
writeStatic(current, 0, current, current, true).applyUnsafe();
}
Util.flush(store);
return null;
}
}
}

View File

@ -1,111 +0,0 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.apache.cassandra.distributed.fuzz;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.TimeUnit;
import com.fasterxml.jackson.annotation.JsonCreator;
import com.fasterxml.jackson.annotation.JsonProperty;
import com.fasterxml.jackson.annotation.JsonTypeName;
import harry.core.Run;
import harry.ddl.SchemaSpec;
import harry.model.sut.SystemUnderTest;
import harry.operations.CompiledStatement;
import harry.visitors.*;
import org.apache.cassandra.distributed.test.log.RngUtils;
public class InJVMTokenAwareVisitorExecutor extends LoggingVisitor.LoggingVisitorExecutor
{
public static void init()
{
harry.core.Configuration.registerSubtypes(Configuration.class);
}
private final InJvmSut sut;
private final SystemUnderTest.ConsistencyLevel cl;
private final SchemaSpec schema;
private final int maxRetries = 10;
// TODO: as of now, token aware executor only performs operations against natrual, but not pending replicas.
public InJVMTokenAwareVisitorExecutor(Run run,
OperationExecutor.RowVisitorFactory rowVisitorFactory,
SystemUnderTest.ConsistencyLevel cl)
{
super(run, rowVisitorFactory.make(run));
this.sut = (InJvmSut) run.sut;
this.schema = run.schemaSpec;
this.cl = cl;
}
@Override
protected void executeAsyncWithRetries(long lts, long pd, CompletableFuture<Object[][]> future, CompiledStatement statement)
{
executeAsyncWithRetries(lts, pd, future, statement, 0);
}
private void executeAsyncWithRetries(long lts, long pd, CompletableFuture<Object[][]> future, CompiledStatement statement, int retries)
{
if (sut.isShutdown())
throw new IllegalStateException("System under test is shut down");
if (retries > this.maxRetries)
throw new IllegalStateException(String.format("Can not execute statement %s after %d retries", statement, retries));
Object[] partitionKey = schema.inflatePartitionKey(pd);
int[] replicas = sut.getReadReplicasFor(partitionKey, schema.keyspace, schema.table);
int replica = replicas[RngUtils.asInt(lts, 0, replicas.length - 1)];
if (cl == SystemUnderTest.ConsistencyLevel.NODE_LOCAL)
{
future.complete(sut.cluster.get(replica).executeInternal(statement.cql(), statement.bindings()));
}
else
{
CompletableFuture.supplyAsync(() -> sut.cluster.coordinator(replica).execute(statement.cql(), InJvmSut.toApiCl(cl), statement.bindings()), executor)
.whenComplete((res, t) ->
{
if (t != null)
executor.schedule(() -> executeAsyncWithRetries(lts, pd, future, statement, retries + 1), 1, TimeUnit.SECONDS);
else
future.complete(res);
});
}
}
@JsonTypeName("in_jvm_token_aware")
public static class Configuration implements harry.core.Configuration.VisitorConfiguration
{
public final harry.core.Configuration.RowVisitorConfiguration row_visitor;
public final SystemUnderTest.ConsistencyLevel consistency_level;
@JsonCreator
public Configuration(@JsonProperty("row_visitor") harry.core.Configuration.RowVisitorConfiguration rowVisitor,
@JsonProperty("consistency_level") SystemUnderTest.ConsistencyLevel consistencyLevel)
{
this.row_visitor = rowVisitor;
this.consistency_level = consistencyLevel;
}
@Override
public Visitor make(Run run)
{
return new GeneratingVisitor(run, new InJVMTokenAwareVisitorExecutor(run, row_visitor, consistency_level));
}
}
}

View File

@ -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);
}
}
}

View File

@ -1,363 +0,0 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.apache.cassandra.distributed.fuzz;
import java.nio.ByteBuffer;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
import java.util.SortedSet;
import harry.core.Run;
import harry.ddl.SchemaSpec;
import harry.model.OpSelectors;
import harry.operations.Relation;
import harry.operations.Query;
import harry.operations.QueryGenerator;
import harry.util.BitSet;
import org.apache.cassandra.Util;
import org.apache.cassandra.cql3.AbstractMarker;
import org.apache.cassandra.cql3.ColumnIdentifier;
import org.apache.cassandra.cql3.Operator;
import org.apache.cassandra.cql3.QueryOptions;
import org.apache.cassandra.cql3.SingleColumnRelation;
import org.apache.cassandra.cql3.VariableSpecifications;
import org.apache.cassandra.cql3.WhereClause;
import org.apache.cassandra.cql3.restrictions.StatementRestrictions;
import org.apache.cassandra.cql3.statements.Bound;
import org.apache.cassandra.cql3.statements.DeleteStatement;
import org.apache.cassandra.cql3.statements.StatementType;
import org.apache.cassandra.db.ClusteringBound;
import org.apache.cassandra.db.ColumnFamilyStore;
import org.apache.cassandra.db.ConsistencyLevel;
import org.apache.cassandra.db.DecoratedKey;
import org.apache.cassandra.db.DeletionTime;
import org.apache.cassandra.db.Mutation;
import org.apache.cassandra.db.RangeTombstone;
import org.apache.cassandra.db.RowUpdateBuilder;
import org.apache.cassandra.db.Slices;
import org.apache.cassandra.db.marshal.ByteBufferAccessor;
import org.apache.cassandra.db.marshal.CompositeType;
import org.apache.cassandra.db.partitions.PartitionUpdate;
import org.apache.cassandra.io.sstable.format.SSTableReader;
import org.apache.cassandra.schema.ColumnMetadata;
import org.apache.cassandra.schema.TableMetadata;
import org.apache.cassandra.utils.ByteBufferUtil;
import org.apache.cassandra.utils.FBUtilities;
import static harry.generators.DataGenerators.UNSET_VALUE;
public class SSTableGenerator
{
protected final SchemaSpec schema;
protected final OpSelectors.DescriptorSelector descriptorSelector;
protected final OpSelectors.MonotonicClock clock;
protected final ColumnFamilyStore store;
protected final TableMetadata metadata;
protected final QueryGenerator rangeSelector;
private final Set<SSTableReader> sstables = new HashSet<>();
private long lts = 0;
public SSTableGenerator(Run run,
ColumnFamilyStore store)
{
this.schema = run.schemaSpec;
this.descriptorSelector = run.descriptorSelector;
this.clock = run.clock;
this.store = store;
// We assume metadata can not change over the lifetime of sstable generator
this.metadata = store.metadata.get();
this.rangeSelector = new QueryGenerator(schema, run.pdSelector, descriptorSelector, run.rng);
store.disableAutoCompaction();
}
public Collection<SSTableReader> gen(int rows)
{
mark();
for (int i = 0; i < rows; i++)
{
long current = lts++;
write(current, current, current, current, true).applyUnsafe();
if (schema.staticColumns != null)
writeStatic(current, current, current, current, true).applyUnsafe();
}
return flush();
}
public void mark()
{
sstables.clear();
sstables.addAll(store.getLiveSSTables());
}
public Collection<SSTableReader> flush()
{
Util.flush(store);
sstables.removeAll(store.getLiveSSTables());
Set<SSTableReader> ret = new HashSet<>(sstables);
mark();
return ret;
}
public Mutation write(long lts, long pd, long cd, long opId, boolean withRowMarker)
{
long[] vds = descriptorSelector.vds(pd, cd, lts, opId, OpSelectors.OperationKind.INSERT, schema);
Object[] partitionKey = schema.inflatePartitionKey(pd);
Object[] clusteringKey = schema.inflateClusteringKey(cd);
Object[] regularColumns = schema.inflateRegularColumns(vds);
RowUpdateBuilder builder = new RowUpdateBuilder(metadata,
FBUtilities.nowInSeconds(),
clock.rts(lts),
metadata.params.defaultTimeToLive,
serializePartitionKey(store, partitionKey))
.clustering(clusteringKey);
if (!withRowMarker)
builder.noRowMarker();
for (int i = 0; i < regularColumns.length; i++)
{
Object value = regularColumns[i];
if (value == UNSET_VALUE)
continue;
ColumnMetadata def = metadata.getColumn(new ColumnIdentifier(schema.regularColumns.get(i).name, false));
builder.add(def, value);
}
return builder.build();
}
public Mutation writeStatic(long lts, long pd, long cd, long opId, boolean withRowMarker)
{
long[] sds = descriptorSelector.sds(pd, cd, lts, opId, OpSelectors.OperationKind.INSERT_WITH_STATICS, schema);
Object[] partitionKey = schema.inflatePartitionKey(pd);
Object[] staticColumns = schema.inflateStaticColumns(sds);
RowUpdateBuilder builder = new RowUpdateBuilder(metadata,
FBUtilities.nowInSeconds(),
clock.rts(lts),
metadata.params.defaultTimeToLive,
serializePartitionKey(store, partitionKey));
if (!withRowMarker)
builder.noRowMarker();
for (int i = 0; i < staticColumns.length; i++)
{
Object value = staticColumns[i];
if (value == UNSET_VALUE)
continue;
ColumnMetadata def = metadata.getColumn(new ColumnIdentifier(schema.staticColumns.get(i).name, false));
builder.add(def, staticColumns[i]);
}
return builder.build();
}
public Mutation deleteColumn(long lts, long pd, long cd, long opId)
{
Object[] partitionKey = schema.inflatePartitionKey(pd);
Object[] clusteringKey = schema.inflateClusteringKey(cd);
RowUpdateBuilder builder = new RowUpdateBuilder(metadata,
FBUtilities.nowInSeconds(),
clock.rts(lts),
metadata.params.defaultTimeToLive,
serializePartitionKey(store, partitionKey))
.noRowMarker()
.clustering(clusteringKey);
BitSet columns = descriptorSelector.columnMask(pd, lts, opId, OpSelectors.OperationKind.DELETE_COLUMN);
BitSet mask = schema.regularColumnsMask();
if (columns == null || columns.allUnset(mask))
throw new IllegalArgumentException("Can't have a delete column query with no columns set. Column mask: " + columns);
columns.eachSetBit((idx) -> {
if (idx < schema.regularColumnsOffset)
throw new RuntimeException("Can't delete parts of partition or clustering key");
if (idx > schema.allColumns.size())
throw new IndexOutOfBoundsException(String.format("Index %d is out of bounds. Max index: %d", idx, schema.allColumns.size()));
builder.delete(schema.allColumns.get(idx).name);
}, mask);
return builder.build();
}
public Mutation deleteStatic(long lts, long pd, long opId)
{
Object[] partitionKey = schema.inflatePartitionKey(pd);
RowUpdateBuilder builder = new RowUpdateBuilder(metadata,
FBUtilities.nowInSeconds(),
clock.rts(lts),
metadata.params.defaultTimeToLive,
serializePartitionKey(store, partitionKey))
.noRowMarker();
BitSet columns = descriptorSelector.columnMask(pd, lts, opId, OpSelectors.OperationKind.DELETE_COLUMN_WITH_STATICS);
BitSet mask = schema.staticColumnsMask();
if (columns == null || columns.allUnset(mask))
throw new IllegalArgumentException("Can't have a delete column query with no columns set. Column mask: " + columns);
columns.eachSetBit((idx) -> {
if (idx < schema.staticColumnsOffset)
throw new RuntimeException(String.format("Can't delete parts of partition or clustering key %d (%s)",
idx, schema.allColumns.get(idx)));
if (idx > schema.allColumns.size())
throw new IndexOutOfBoundsException(String.format("Index %d is out of bounds. Max index: %d", idx, schema.allColumns.size()));
builder.delete(schema.allColumns.get(idx).name);
}, mask);
return builder.build();
}
public Mutation deletePartition(long lts, long pd)
{
Object[] partitionKey = schema.inflatePartitionKey(pd);
PartitionUpdate update = PartitionUpdate.fullPartitionDelete(metadata,
serializePartitionKey(store, partitionKey),
clock.rts(lts),
FBUtilities.nowInSeconds());
return new Mutation(update);
}
public Mutation deleteRow(long lts, long pd, long cd)
{
Object[] partitionKey = schema.inflatePartitionKey(pd);
Object[] clusteringKey = schema.inflateClusteringKey(cd);
return RowUpdateBuilder.deleteRow(metadata,
clock.rts(lts),
serializePartitionKey(store, partitionKey),
clusteringKey);
}
public Mutation deleteSlice(long lts, long pd, long opId)
{
return delete(lts, pd, rangeSelector.inflate(lts, opId, Query.QueryKind.CLUSTERING_SLICE));
}
public Mutation deleteRange(long lts, long pd, long opId)
{
return delete(lts, pd, rangeSelector.inflate(lts, opId, Query.QueryKind.CLUSTERING_RANGE));
}
Mutation delete(long lts, long pd, Query query)
{
Object[] partitionKey = schema.inflatePartitionKey(pd);
WhereClause.Builder builder = new WhereClause.Builder();
List<ColumnIdentifier> variableNames = new ArrayList<>();
List<ByteBuffer> values = new ArrayList<>();
for (int i = 0; i < partitionKey.length; i++)
{
String name = schema.partitionKeys.get(i).name;
ColumnMetadata columnDef = metadata.getColumn(ByteBufferUtil.bytes(name));
variableNames.add(columnDef.name);
values.add(ByteBufferUtil.objectToBytes(partitionKey[i]));
builder.add(new SingleColumnRelation(ColumnIdentifier.getInterned(name, true),
toOperator(Relation.RelationKind.EQ),
new AbstractMarker.Raw(values.size() - 1)));
}
for (Relation relation : query.relations)
{
String name = relation.column();
ColumnMetadata columnDef = metadata.getColumn(ByteBufferUtil.bytes(relation.column()));
variableNames.add(columnDef.name);
values.add(ByteBufferUtil.objectToBytes(relation.value()));
builder.add(new SingleColumnRelation(ColumnIdentifier.getInterned(name, false),
toOperator(relation.kind),
new AbstractMarker.Raw(values.size() - 1)));
}
StatementRestrictions restrictions = new StatementRestrictions(null,
StatementType.DELETE,
metadata,
builder.build(),
new VariableSpecifications(variableNames),
Collections.emptyList(),
false,
false,
false,
false);
QueryOptions options = QueryOptions.forInternalCalls(ConsistencyLevel.QUORUM, values);
SortedSet<ClusteringBound<?>> startBounds = restrictions.getClusteringColumnsBounds(Bound.START, options);
SortedSet<ClusteringBound<?>> endBounds = restrictions.getClusteringColumnsBounds(Bound.END, options);
Slices slices = DeleteStatement.toSlices(metadata, startBounds, endBounds);
assert slices.size() == 1;
long deletionTime = FBUtilities.nowInSeconds();
long rts = clock.rts(lts);
return new RowUpdateBuilder(metadata,
deletionTime,
rts,
metadata.params.defaultTimeToLive,
serializePartitionKey(store, partitionKey))
.noRowMarker()
.addRangeTombstone(new RangeTombstone(slices.get(0), DeletionTime.build(rts, deletionTime)))
.build();
}
public static Operator toOperator(Relation.RelationKind kind)
{
switch (kind)
{
case LT: return Operator.LT;
case GT: return Operator.GT;
case LTE: return Operator.LTE;
case GTE: return Operator.GTE;
case EQ: return Operator.EQ;
default: throw new IllegalArgumentException("Unsupported " + kind);
}
}
public static DecoratedKey serializePartitionKey(ColumnFamilyStore store, Object... pk)
{
if (pk.length == 1)
return store.getPartitioner().decorateKey(ByteBufferUtil.objectToBytes(pk[0]));
ByteBuffer[] values = new ByteBuffer[pk.length];
for (int i = 0; i < pk.length; i++)
values[i] = ByteBufferUtil.objectToBytes(pk[i]);
return store.getPartitioner().decorateKey(CompositeType.build(ByteBufferAccessor.instance, values));
}
}

View File

@ -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
{
}
}

View File

@ -1,131 +0,0 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.apache.cassandra.distributed.fuzz.test;
import java.util.Arrays;
import java.util.HashSet;
import java.util.Set;
import java.util.concurrent.CompletableFuture;
import com.google.common.collect.Iterators;
import org.junit.Test;
import harry.core.Configuration;
import harry.core.Run;
import harry.ddl.ColumnSpec;
import harry.ddl.SchemaSpec;
import harry.model.Model;
import harry.model.QuiescentChecker;
import harry.model.clock.OffsetClock;
import harry.model.sut.SystemUnderTest;
import harry.operations.Query;
import harry.visitors.GeneratingVisitor;
import harry.visitors.LtsVisitor;
import org.apache.cassandra.cql3.CQLTester;
import org.apache.cassandra.distributed.fuzz.FixedSchemaProviderConfiguration;
import org.apache.cassandra.distributed.fuzz.HarryHelper;
import org.apache.cassandra.distributed.fuzz.SSTableLoadingVisitor;
import org.apache.cassandra.distributed.impl.RowUtil;
public class SSTableGeneratorTest extends CQLTester
{
private static final Configuration configuration;
static
{
try
{
HarryHelper.init();
configuration = HarryHelper.defaultConfiguration()
.setClock(() -> new OffsetClock(10000L))
.build();
}
catch (Exception e)
{
throw new RuntimeException(e);
}
}
private static SchemaSpec schemaSpec = new SchemaSpec(KEYSPACE, "tbl1",
Arrays.asList(ColumnSpec.pk("pk1", ColumnSpec.asciiType),
ColumnSpec.pk("pk2", ColumnSpec.int64Type)),
Arrays.asList(ColumnSpec.ck("ck1", ColumnSpec.asciiType, false),
ColumnSpec.ck("ck2", ColumnSpec.int64Type, false)),
Arrays.asList(ColumnSpec.regularColumn("v1", ColumnSpec.int32Type),
ColumnSpec.regularColumn("v2", ColumnSpec.int64Type),
ColumnSpec.regularColumn("v3", ColumnSpec.int32Type),
ColumnSpec.regularColumn("v4", ColumnSpec.asciiType)),
Arrays.asList(ColumnSpec.staticColumn("s1", ColumnSpec.asciiType),
ColumnSpec.staticColumn("s2", ColumnSpec.int64Type)));
@Test
public void testSSTableGenerator()
{
createTable(schemaSpec.compile().cql());
Run run = configuration.unbuild()
.setSchemaProvider(new FixedSchemaProviderConfiguration(schemaSpec))
.setSUT(CqlTesterSUT::new)
.build()
.createRun();
SSTableLoadingVisitor sstableVisitor = new SSTableLoadingVisitor(run, 1000);
LtsVisitor visitor = new GeneratingVisitor(run, sstableVisitor);
Set<Long> pds = new HashSet<>();
run.tracker.onLtsStarted((lts) -> pds.add(run.pdSelector.pd(lts, run.schemaSpec)));
for (int i = 0; i < 1000; i++)
visitor.visit();
sstableVisitor.forceFlush();
Model checker = new QuiescentChecker(run);
for (Long pd : pds)
checker.validate(Query.selectPartition(run.schemaSpec, pd, false));
}
public class CqlTesterSUT implements SystemUnderTest
{
public boolean isShutdown()
{
return false;
}
public void shutdown()
{
}
public CompletableFuture<Object[][]> executeAsync(String s, ConsistencyLevel consistencyLevel, Object... objects)
{
throw new RuntimeException("Not implemented");
}
public Object[][] execute(String s, ConsistencyLevel consistencyLevel, Object... objects)
{
try
{
return Iterators.toArray(RowUtil.toIter(SSTableGeneratorTest.this.execute(s, objects)),
Object[].class);
}
catch (Throwable throwable)
{
throw new RuntimeException();
}
}
}
}

View File

@ -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;

View File

@ -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(() -> {

View File

@ -24,6 +24,7 @@ import org.apache.cassandra.config.DatabaseDescriptor;
import org.apache.cassandra.db.guardrails.Guardrails;
import org.apache.cassandra.dht.Murmur3Partitioner;
import org.apache.cassandra.distributed.api.IIsolatedExecutor;
import org.apache.cassandra.harry.sut.TokenPlacementModel;
import org.apache.cassandra.locator.IEndpointSnitch;
import org.apache.cassandra.locator.InetAddressAndPort;
import org.apache.cassandra.locator.Replica;
@ -87,9 +88,9 @@ public class CMSTestBase
public final LocalLog log;
public final ClusterMetadataService service;
public final SchemaProvider schemaProvider;
public final PlacementSimulator.ReplicationFactor rf;
public final TokenPlacementModel.ReplicationFactor rf;
public CMSSut(IIsolatedExecutor.SerializableFunction<LocalLog, Processor> processorFactory, boolean addListeners, PlacementSimulator.ReplicationFactor rf)
public CMSSut(IIsolatedExecutor.SerializableFunction<LocalLog, Processor> processorFactory, boolean addListeners, TokenPlacementModel.ReplicationFactor rf)
{
partitioner = Murmur3Partitioner.instance;
this.rf = rf;

View File

@ -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 })

View File

@ -42,8 +42,6 @@ import org.junit.Assert;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import harry.util.ByteUtils;
import harry.util.TestRunner;
import org.apache.cassandra.ServerTestUtils;
import org.apache.cassandra.concurrent.ExecutorFactory;
import org.apache.cassandra.concurrent.ExecutorPlus;
@ -66,6 +64,11 @@ import org.apache.cassandra.distributed.shared.NetworkTopology;
import org.apache.cassandra.distributed.test.log.PlacementSimulator.Transformations;
import org.apache.cassandra.gms.GossipDigestAck;
import org.apache.cassandra.gms.GossipDigestSyn;
import org.apache.cassandra.harry.sut.TokenPlacementModel;
import org.apache.cassandra.harry.sut.TokenPlacementModel.Node;
import org.apache.cassandra.harry.sut.TokenPlacementModel.NodeFactory;
import org.apache.cassandra.harry.util.ByteUtils;
import org.apache.cassandra.harry.util.TestRunner;
import org.apache.cassandra.locator.InetAddressAndPort;
import org.apache.cassandra.locator.SimpleSeedProvider;
import org.apache.cassandra.tcm.*;
@ -91,7 +94,6 @@ import org.apache.cassandra.utils.concurrent.AsyncPromise;
import org.apache.cassandra.utils.concurrent.CountDownLatch;
import org.apache.cassandra.utils.concurrent.Future;
import static org.apache.cassandra.distributed.test.log.PlacementSimulator.Node;
import static org.apache.cassandra.distributed.test.log.PlacementSimulator.RefSimulatedPlacementHolder;
import static org.apache.cassandra.distributed.test.log.PlacementSimulator.SimulatedPlacementHolder;
import static org.apache.cassandra.distributed.test.log.PlacementSimulator.SimulatedPlacements;
@ -102,7 +104,7 @@ public abstract class CoordinatorPathTestBase extends FuzzTestBase
{
private static final Logger logger = LoggerFactory.getLogger(CoordinatorPathTestBase.class);
public void coordinatorPathTest(PlacementSimulator.ReplicationFactor rf, TestRunner.ThrowingBiConsumer<Cluster, SimulatedCluster> test) throws Throwable
public void coordinatorPathTest(TokenPlacementModel.ReplicationFactor rf, TestRunner.ThrowingBiConsumer<Cluster, SimulatedCluster> test) throws Throwable
{
coordinatorPathTest(rf, test, true);
}
@ -112,7 +114,7 @@ public abstract class CoordinatorPathTestBase extends FuzzTestBase
*
* CMS node is 127.0.0.10 and is simulated.
*/
public void coordinatorPathTest(PlacementSimulator.ReplicationFactor rf, TestRunner.ThrowingBiConsumer<Cluster, SimulatedCluster> test, boolean startCluster) throws Throwable
public void coordinatorPathTest(TokenPlacementModel.ReplicationFactor rf, TestRunner.ThrowingBiConsumer<Cluster, SimulatedCluster> test, boolean startCluster) throws Throwable
{
ServerTestUtils.daemonInitialization();
DatabaseDescriptor.setPartitionerUnsafe(Murmur3Partitioner.instance);
@ -120,8 +122,8 @@ public abstract class CoordinatorPathTestBase extends FuzzTestBase
String nodeUnderTest = "127.0.0.1";
InetAddressAndPort nodeUnderTestAddr = InetAddressAndPort.getByName(nodeUnderTest + ":7012");
PlacementSimulator.NodeFactory factory = PlacementSimulator.nodeFactory();
Node fakeCmsNode = factory.make(10,1,1);
NodeFactory factory = TokenPlacementModel.nodeFactory();
Node fakeCmsNode = factory.make(10, 1, 1);
FBUtilities.setBroadcastInetAddressAndPort(fakeCmsNode.addr());
try (Cluster cluster = builder().withNodes(1)
@ -153,7 +155,7 @@ public abstract class CoordinatorPathTestBase extends FuzzTestBase
.withTokenSupplier(tokenSupplier)
.withNodeIdTopology(NetworkTopology.singleDcNetworkTopology(10, "dc0", "rack0"))
.createWithoutStarting();
SimulatedCluster simulatedCluster = new SimulatedCluster(new PlacementSimulator.SimpleReplicationFactor(3),
SimulatedCluster simulatedCluster = new SimulatedCluster(new TokenPlacementModel.SimpleReplicationFactor(3),
cluster, tokenSupplier))
{
// Note that in case of a CMS node teset we first start a cluster, and then initialize a simulated cluster.
@ -607,9 +609,9 @@ public abstract class CoordinatorPathTestBase extends FuzzTestBase
protected final TokenSupplier tokenSupplier;
protected final IPartitioner partitioner = Murmur3Partitioner.instance;
protected ExecutorPlus executor;
public final PlacementSimulator.NodeFactory nodeFactory = PlacementSimulator.nodeFactory();
public final NodeFactory nodeFactory = TokenPlacementModel.nodeFactory();
public SimulatedCluster(PlacementSimulator.ReplicationFactor rf, ICluster<IInvokableInstance> realCluster, TokenSupplier tokenSupplier)
public SimulatedCluster(TokenPlacementModel.ReplicationFactor rf, ICluster<IInvokableInstance> realCluster, TokenSupplier tokenSupplier)
{
this.tokenSupplier = tokenSupplier;
@ -845,9 +847,9 @@ public abstract class CoordinatorPathTestBase extends FuzzTestBase
{
protected final SimulatedPlacementHolder state;
protected final List<VirtualSimulatedNode> nodes;
protected final PlacementSimulator.NodeFactory factory;
protected final NodeFactory factory;
public VirtualSimulatedCluster(SimulatedPlacementHolder state, PlacementSimulator.NodeFactory factory)
public VirtualSimulatedCluster(SimulatedPlacementHolder state, NodeFactory factory)
{
this.state = state;
this.factory = factory;

View File

@ -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();

View File

@ -18,30 +18,13 @@
package org.apache.cassandra.distributed.test.log;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.concurrent.TimeUnit;
import org.junit.BeforeClass;
import harry.core.Run;
import harry.data.ResultSetRow;
import harry.model.OpSelectors;
import harry.model.QuiescentChecker;
import harry.model.sut.SystemUnderTest;
import harry.operations.CompiledStatement;
import harry.operations.Query;
import org.apache.cassandra.distributed.Cluster;
import org.apache.cassandra.distributed.api.IIsolatedExecutor;
import org.apache.cassandra.distributed.fuzz.HarryHelper;
import org.apache.cassandra.distributed.fuzz.InJvmSut;
import org.apache.cassandra.distributed.test.ExecUtil;
import org.apache.cassandra.distributed.test.TestBaseImpl;
import org.apache.cassandra.locator.ReplicationFactor;
import static harry.model.SelectHelper.resultSetToRow;
import org.apache.cassandra.harry.HarryHelper;
public class FuzzTestBase extends TestBaseImpl
{
@ -61,81 +44,4 @@ public class FuzzTestBase extends TestBaseImpl
// unpaused before event expiration.
.set("cms_await_timeout", String.format("%dms", TimeUnit.MINUTES.toMillis(10))));
}
public static IIsolatedExecutor.SerializableRunnable toRunnable(ExecUtil.ThrowingSerializableRunnable runnable)
{
return () -> {
try
{
runnable.run();
}
catch (Throwable t)
{
System.out.println(t.getMessage());
t.printStackTrace();
}
};
}
public static class QuiescentLocalStateChecker extends QuiescentChecker
{
public final InJvmSut inJvmSut;
public final ReplicationFactor rf;
private final OpSelectors.PdSelector pdSelector;
public QuiescentLocalStateChecker(Run run)
{
this(run, null);
}
public QuiescentLocalStateChecker(Run run, ReplicationFactor rf)
{
super(run);
assert run.sut instanceof InJvmSut;
this.inJvmSut = (InJvmSut) run.sut;
this.rf = rf;
this.pdSelector = run.pdSelector;
}
public void validateAll()
{
for (int lts = 0; lts < clock.peek(); lts++)
validate(Query.selectPartition(schema, pdSelector.pd(lts, schema), false));
}
@Override
public void validate(Query query)
{
CompiledStatement compiled = query.toSelectStatement();
int[] replicas = inJvmSut.getReadReplicasFor(schema.inflatePartitionKey(query.pd), schema.keyspace, schema.table);
if (rf != null && replicas.length != rf.allReplicas)
throw new IllegalStateException(String.format("Total number of replicas %d does not match expectation %d",
replicas.length, rf.allReplicas));
for (int node : replicas)
{
try
{
validate(() -> {
Object[][] objects = inJvmSut.execute(compiled.cql(),
SystemUnderTest.ConsistencyLevel.NODE_LOCAL,
node,
compiled.bindings());
List<ResultSetRow> result = new ArrayList<>();
for (Object[] obj : objects)
result.add(resultSetToRow(query.schemaSpec, clock, obj));
return result;
}, query);
}
catch (ValidationException e)
{
throw new AssertionError(String.format("Caught error while validating replica %d of replica set %s",
node, Arrays.toString(replicas)),
e);
}
}
}
}
}

View File

@ -44,7 +44,7 @@ import org.apache.cassandra.dht.Murmur3Partitioner;
import org.apache.cassandra.dht.Murmur3Partitioner.LongToken;
import org.apache.cassandra.dht.Range;
import org.apache.cassandra.dht.Token;
import org.apache.cassandra.distributed.test.log.PlacementSimulator.SimulatedPlacements;
import org.apache.cassandra.harry.sut.TokenPlacementModel;
import org.apache.cassandra.locator.EndpointsForRange;
import org.apache.cassandra.locator.InetAddressAndPort;
import org.apache.cassandra.locator.CMSPlacementStrategy;
@ -65,8 +65,8 @@ import org.apache.cassandra.tcm.ownership.VersionedEndpoints;
import org.apache.cassandra.tcm.transformations.Register;
import org.apache.cassandra.tcm.transformations.SealPeriod;
import static org.apache.cassandra.distributed.test.log.PlacementSimulator.Node;
import static org.apache.cassandra.distributed.test.log.PlacementSimulator.nodeFactoryHumanReadable;
import static org.apache.cassandra.distributed.test.log.PlacementSimulator.*;
import static org.apache.cassandra.harry.sut.TokenPlacementModel.*;
public class MetadataChangeSimulationTest extends CMSTestBase
{
@ -88,7 +88,7 @@ public class MetadataChangeSimulationTest extends CMSTestBase
{
for (int rf : new int[]{ 2, 3, 5 })
{
simulate(50, new PlacementSimulator.NtsReplicationFactor(3, rf), concurrency);
simulate(50, new NtsReplicationFactor(3, rf), concurrency);
}
}
}
@ -100,7 +100,7 @@ public class MetadataChangeSimulationTest extends CMSTestBase
{
for (int rf : new int[]{ 2, 3, 5 })
{
simulate(50, new PlacementSimulator.SimpleReplicationFactor(rf), concurrency);
simulate(50, new SimpleReplicationFactor(rf), concurrency);
}
}
}
@ -110,49 +110,49 @@ public class MetadataChangeSimulationTest extends CMSTestBase
{
for (int i = 0; i < 4; i++)
{
testMoveReal(new PlacementSimulator.NtsReplicationFactor(1, 3), i, 150, 4);
testMoveReal(new PlacementSimulator.NtsReplicationFactor(1, 3), i, 350, 4);
testMoveReal(new PlacementSimulator.NtsReplicationFactor(1, 3), i, 550, 4);
testMoveReal(new NtsReplicationFactor(1, 3), i, 150, 4);
testMoveReal(new NtsReplicationFactor(1, 3), i, 350, 4);
testMoveReal(new NtsReplicationFactor(1, 3), i, 550, 4);
}
for (int i = 0; i < 5; i++)
{
testMoveReal(new PlacementSimulator.NtsReplicationFactor(1, 3), i, 150, 5);
testMoveReal(new PlacementSimulator.NtsReplicationFactor(1, 3), i, 350, 5);
testMoveReal(new PlacementSimulator.NtsReplicationFactor(1, 3), i, 650, 5);
testMoveReal(new NtsReplicationFactor(1, 3), i, 150, 5);
testMoveReal(new NtsReplicationFactor(1, 3), i, 350, 5);
testMoveReal(new NtsReplicationFactor(1, 3), i, 650, 5);
}
for (int i = 0; i < 10; i++)
{
testMoveReal(new PlacementSimulator.NtsReplicationFactor(1, 3), i, 150, 10);
testMoveReal(new PlacementSimulator.NtsReplicationFactor(1, 3), i, 550, 10);
testMoveReal(new PlacementSimulator.NtsReplicationFactor(1, 3), i, 1050, 10);
testMoveReal(new NtsReplicationFactor(1, 3), i, 150, 10);
testMoveReal(new NtsReplicationFactor(1, 3), i, 550, 10);
testMoveReal(new NtsReplicationFactor(1, 3), i, 1050, 10);
}
for (int i = 0; i < 9; i++)
{
testMoveReal(new PlacementSimulator.NtsReplicationFactor(3, 3), i, 350, 9);
testMoveReal(new PlacementSimulator.NtsReplicationFactor(3, 3), i, 550, 9);
testMoveReal(new PlacementSimulator.NtsReplicationFactor(3, 3), i, 1050, 9);
testMoveReal(new NtsReplicationFactor(3, 3), i, 350, 9);
testMoveReal(new NtsReplicationFactor(3, 3), i, 550, 9);
testMoveReal(new NtsReplicationFactor(3, 3), i, 1050, 9);
}
for (int i = 0; i < 10; i++)
{
testMoveReal(new PlacementSimulator.NtsReplicationFactor(3, 3), i, 350, 10);
testMoveReal(new PlacementSimulator.NtsReplicationFactor(3, 3), i, 550, 10);
testMoveReal(new PlacementSimulator.NtsReplicationFactor(3, 3), i, 1050, 10);
testMoveReal(new NtsReplicationFactor(3, 3), i, 350, 10);
testMoveReal(new NtsReplicationFactor(3, 3), i, 550, 10);
testMoveReal(new NtsReplicationFactor(3, 3), i, 1050, 10);
}
for (int i = 0; i < 18; i++)
{
testMoveReal(new PlacementSimulator.NtsReplicationFactor(3, 3), i, 350, 18);
testMoveReal(new PlacementSimulator.NtsReplicationFactor(3, 3), i, 1050, 18);
testMoveReal(new PlacementSimulator.NtsReplicationFactor(3, 3), i, 2050, 18);
testMoveReal(new NtsReplicationFactor(3, 3), i, 350, 18);
testMoveReal(new NtsReplicationFactor(3, 3), i, 1050, 18);
testMoveReal(new NtsReplicationFactor(3, 3), i, 2050, 18);
}
}
public void testMoveReal(PlacementSimulator.ReplicationFactor rf, int moveNodeId, long moveToken, int numberOfNodes) throws Throwable
public void testMoveReal(ReplicationFactor rf, int moveNodeId, long moveToken, int numberOfNodes) throws Throwable
{
try (CMSTestBase.CMSSut sut = new CMSTestBase.CMSSut(AtomicLongBackedProcessor::new, false, rf))
{
@ -181,14 +181,14 @@ public class MetadataChangeSimulationTest extends CMSTestBase
@Test
public void testLeaveReal() throws Throwable
{
testLeaveReal(new PlacementSimulator.NtsReplicationFactor(1, 3), 1);
testLeaveReal(new PlacementSimulator.NtsReplicationFactor(1, 3), 5);
testLeaveReal(new PlacementSimulator.NtsReplicationFactor(3, 3), 1);
testLeaveReal(new PlacementSimulator.NtsReplicationFactor(3, 3), 5);
testLeaveReal(new NtsReplicationFactor(1, 3), 1);
testLeaveReal(new NtsReplicationFactor(1, 3), 5);
testLeaveReal(new NtsReplicationFactor(3, 3), 1);
testLeaveReal(new NtsReplicationFactor(3, 3), 5);
}
public void testLeaveReal(PlacementSimulator.ReplicationFactor rf, int decomNodeId) throws Throwable
public void testLeaveReal(ReplicationFactor rf, int decomNodeId) throws Throwable
{
try (CMSTestBase.CMSSut sut = new CMSTestBase.CMSSut(AtomicLongBackedProcessor::new, false, rf))
{
@ -217,11 +217,11 @@ public class MetadataChangeSimulationTest extends CMSTestBase
@Test
public void testJoinReal() throws Throwable
{
testJoinReal(new PlacementSimulator.NtsReplicationFactor(3, 3), 1);
testJoinReal(new PlacementSimulator.NtsReplicationFactor(3, 3), 5);
testJoinReal(new NtsReplicationFactor(3, 3), 1);
testJoinReal(new NtsReplicationFactor(3, 3), 5);
}
public void testJoinReal(PlacementSimulator.ReplicationFactor rf, int joinNodeId) throws Throwable
public void testJoinReal(ReplicationFactor rf, int joinNodeId) throws Throwable
{
try (CMSTestBase.CMSSut sut = new CMSTestBase.CMSSut(AtomicLongBackedProcessor::new, false, rf))
{
@ -250,14 +250,14 @@ public class MetadataChangeSimulationTest extends CMSTestBase
}
}
public void simulate(int toBootstrap, PlacementSimulator.ReplicationFactor rf, int concurrency) throws Throwable
public void simulate(int toBootstrap, ReplicationFactor rf, int concurrency) throws Throwable
{
System.out.printf("RUNNING SIMULATION. TO BOOTSTRAP: %s, RF: %s, CONCURRENCY: %s%n",
toBootstrap, rf, concurrency);
long startTime = System.currentTimeMillis();
ModelChecker<ModelState, CMSSut> modelChecker = new ModelChecker<>();
ClusterMetadataService.unsetInstance();
modelChecker.init(ModelState.empty(PlacementSimulator.nodeFactory(), toBootstrap, concurrency),
modelChecker.init(ModelState.empty(nodeFactory(), toBootstrap, concurrency),
new CMSSut(AtomicLongBackedProcessor::new, false, rf))
// Sequentially bootstrap rf nodes first
.step((state, sut) -> state.currentNodes.isEmpty(),
@ -380,7 +380,7 @@ public class MetadataChangeSimulationTest extends CMSTestBase
Random random = new Random(1L);
for (int i = 0; i < 10; i++)
{
PlacementSimulator.ReplicationFactor ntsRf = new PlacementSimulator.NtsReplicationFactor(3, 3);
ReplicationFactor ntsRf = new NtsReplicationFactor(3, 3);
Map<String, Integer> cmsRf = new HashMap<>();
for (String s : ntsRf.asMap().keySet())
cmsRf.put(s, 3);
@ -389,12 +389,12 @@ public class MetadataChangeSimulationTest extends CMSTestBase
}
}
public void simulateBounces(PlacementSimulator.ReplicationFactor rf, CMSPlacementStrategy CMSConfigurationStrategy, Random random) throws Throwable
public void simulateBounces(ReplicationFactor rf, CMSPlacementStrategy CMSConfigurationStrategy, Random random) throws Throwable
{
try(CMSSut sut = new CMSSut(AtomicLongBackedProcessor::new, false, rf))
{
ModelState state = ModelState.empty(PlacementSimulator.nodeFactory(), 300, 1);
ModelState state = ModelState.empty(nodeFactory(), 300, 1);
for (Map.Entry<String, Integer> e : rf.asMap().entrySet())
{
@ -553,16 +553,16 @@ public class MetadataChangeSimulationTest extends CMSTestBase
return sb.toString();
}
public static void match(PlacementForRange actual, Map<PlacementSimulator.Range, List<Node>> predicted) throws Throwable
public static void match(PlacementForRange actual, Map<TokenPlacementModel.Range, List<Node>> predicted) throws Throwable
{
Map<Range<Token>, VersionedEndpoints.ForRange> actualGroups = actual.replicaGroups();
assert predicted.size() == actualGroups.size() :
String.format("\nPredicted:\n%s(%d)" +
"\nActual:\n%s(%d)", toString(predicted), predicted.size(), toString(actual.replicaGroups()), actualGroups.size());
for (Map.Entry<PlacementSimulator.Range, List<Node>> entry : predicted.entrySet())
for (Map.Entry<TokenPlacementModel.Range, List<Node>> entry : predicted.entrySet())
{
PlacementSimulator.Range range = entry.getKey();
TokenPlacementModel.Range range = entry.getKey();
List<Node> predictedNodes = entry.getValue();
Range<Token> predictedRange = new Range<Token>(new Murmur3Partitioner.LongToken(range.start),
new Murmur3Partitioner.LongToken(range.end));
@ -653,7 +653,7 @@ public class MetadataChangeSimulationTest extends CMSTestBase
}
public static void validatePlacements(IPartitioner partitioner,
PlacementSimulator.ReplicationFactor rf,
ReplicationFactor rf,
ModelState modelState,
DataPlacements placements)
{
@ -678,7 +678,7 @@ public class MetadataChangeSimulationTest extends CMSTestBase
Assert.assertEquals(new TreeSet<>(l), new TreeSet<>(r));
}
public static void validatePlacementsInternal(PlacementSimulator.ReplicationFactor rf, List<SimulatedOperation> opStates, List<Range<Token>> expectedRanges, PlacementForRange placements, boolean allowPending)
public static void validatePlacementsInternal(ReplicationFactor rf, List<SimulatedOperation> opStates, List<Range<Token>> expectedRanges, PlacementForRange placements, boolean allowPending)
{
int overreplicated = 0;
for (Range<Token> range : expectedRanges)
@ -719,7 +719,7 @@ public class MetadataChangeSimulationTest extends CMSTestBase
}
}
if (allowPending && rf instanceof PlacementSimulator.SimpleReplicationFactor)
if (allowPending && rf instanceof SimpleReplicationFactor)
{
int bootstrappingNodes = 0;
int movingNodes = 0;

View File

@ -28,6 +28,8 @@ import java.util.Random;
import java.util.Set;
import java.util.TreeMap;
import org.apache.cassandra.harry.sut.TokenPlacementModel;
public class ModelState
{
public final int maxClusterSize;
@ -37,17 +39,17 @@ public class ModelState
public final int[] cancelled;
public final int[] finished;
public final int bootstrappingCount;
public final List<PlacementSimulator.Node> currentNodes;
public final Map<String, List<PlacementSimulator.Node>> nodesByDc;
public final List<PlacementSimulator.Node> registeredNodes;
public final List<PlacementSimulator.Node> leavingNodes;
public final List<PlacementSimulator.Node> movingNodes;
public final List<TokenPlacementModel.Node> currentNodes;
public final Map<String, List<TokenPlacementModel.Node>> nodesByDc;
public final List<TokenPlacementModel.Node> registeredNodes;
public final List<TokenPlacementModel.Node> leavingNodes;
public final List<TokenPlacementModel.Node> movingNodes;
public final List<SimulatedOperation> inFlightOperations;
public final PlacementSimulator.SimulatedPlacements simulatedPlacements;
public final PlacementSimulator.NodeFactory nodeFactory;
public final TokenPlacementModel.NodeFactory nodeFactory;
public static ModelState empty(PlacementSimulator.NodeFactory nodeFactory, int maxClusterSize, int maxConcurrency)
public static ModelState empty(TokenPlacementModel.NodeFactory nodeFactory, int maxClusterSize, int maxConcurrency)
{
return new ModelState(maxClusterSize, maxConcurrency,
0, 0,
@ -58,11 +60,11 @@ public class ModelState
nodeFactory);
}
public static Map<String, List<PlacementSimulator.Node>> groupByDc(List<PlacementSimulator.Node> nodes)
public static Map<String, List<TokenPlacementModel.Node>> groupByDc(List<TokenPlacementModel.Node> nodes)
{
// using treemap here since it is much easier to read/debug when it comes to that
Map<String, List<PlacementSimulator.Node>> grouped = new TreeMap<>();
for (PlacementSimulator.Node node : nodes)
Map<String, List<TokenPlacementModel.Node>> grouped = new TreeMap<>();
for (TokenPlacementModel.Node node : nodes)
{
grouped.computeIfAbsent(node.dc(), (k) -> new ArrayList<>())
.add(node);
@ -76,13 +78,13 @@ public class ModelState
int rejected,
int[] cancelled,
int[] finished,
List<PlacementSimulator.Node> currentNodes,
List<PlacementSimulator.Node> registeredNodes,
List<PlacementSimulator.Node> leavingNodes,
List<PlacementSimulator.Node> movingNodes,
List<TokenPlacementModel.Node> currentNodes,
List<TokenPlacementModel.Node> registeredNodes,
List<TokenPlacementModel.Node> leavingNodes,
List<TokenPlacementModel.Node> movingNodes,
List<SimulatedOperation> operationStates,
PlacementSimulator.SimulatedPlacements simulatedPlacements,
PlacementSimulator.NodeFactory nodeFactory)
TokenPlacementModel.NodeFactory nodeFactory)
{
this.maxClusterSize = maxClusterSize;
this.maxConcurrency = maxConcurrency;
@ -118,30 +120,30 @@ public class ModelState
return withinConcurrencyLimit() && bootstrappingCount + currentNodes.size() < maxClusterSize;
}
public boolean shouldLeave(PlacementSimulator.ReplicationFactor rf, Random rng)
public boolean shouldLeave(TokenPlacementModel.ReplicationFactor rf, Random rng)
{
return canRemove(rf) && rng.nextDouble() > 0.7;
}
public boolean shouldMove(PlacementSimulator.ReplicationFactor rf, Random rng)
public boolean shouldMove(TokenPlacementModel.ReplicationFactor rf, Random rng)
{
return canRemove(rf) && rng.nextDouble() > 0.7;
}
public boolean shouldReplace(PlacementSimulator.ReplicationFactor rf, Random rng)
public boolean shouldReplace(TokenPlacementModel.ReplicationFactor rf, Random rng)
{
return canRemove(rf) && rng.nextDouble() > 0.8;
}
private boolean canRemove(PlacementSimulator.ReplicationFactor rfs)
private boolean canRemove(TokenPlacementModel.ReplicationFactor rfs)
{
if (!withinConcurrencyLimit()) return false;
for (Map.Entry<String, Integer> e : rfs.asMap().entrySet())
{
String dc = e.getKey();
int rf = e.getValue();
List<PlacementSimulator.Node> nodes = nodesByDc.get(dc);
Set<PlacementSimulator.Node> nodesInDc = nodes == null ? new HashSet<>() : new HashSet<>(nodes);
List<TokenPlacementModel.Node> nodes = nodesByDc.get(dc);
Set<TokenPlacementModel.Node> nodesInDc = nodes == null ? new HashSet<>() : new HashSet<>(nodes);
for (SimulatedOperation op : inFlightOperations)
nodesInDc.removeAll(Arrays.asList(op.nodes));
if (nodesInDc.size() > rf)
@ -179,13 +181,13 @@ public class ModelState
// join/replace/leave/move
private int[] cancelled;
private int[] finished;
private List<PlacementSimulator.Node> currentNodes;
private List<PlacementSimulator.Node> registeredNodes;
private List<PlacementSimulator.Node> leavingNodes;
private List<PlacementSimulator.Node> movingNodes;
private List<TokenPlacementModel.Node> currentNodes;
private List<TokenPlacementModel.Node> registeredNodes;
private List<TokenPlacementModel.Node> leavingNodes;
private List<TokenPlacementModel.Node> movingNodes;
private List<SimulatedOperation> operationStates;
private PlacementSimulator.SimulatedPlacements simulatedPlacements;
private PlacementSimulator.NodeFactory nodeFactory;
private TokenPlacementModel.NodeFactory nodeFactory;
private Transformer(ModelState source)
{
@ -254,26 +256,26 @@ public class ModelState
return this;
}
public Transformer withJoined(PlacementSimulator.Node node)
public Transformer withJoined(TokenPlacementModel.Node node)
{
addToCluster(node);
finished[0]++;
return this;
}
public Transformer recycleRejected(PlacementSimulator.Node node)
public Transformer recycleRejected(TokenPlacementModel.Node node)
{
registeredNodes = new ArrayList<>(registeredNodes);
registeredNodes.add(node);
return this;
}
public Transformer withMoved(PlacementSimulator.Node movingNode, PlacementSimulator.Node movedTo)
public Transformer withMoved(TokenPlacementModel.Node movingNode, TokenPlacementModel.Node movedTo)
{
assert currentNodes.contains(movingNode) : movingNode;
List<PlacementSimulator.Node> tmp = currentNodes;
List<TokenPlacementModel.Node> tmp = currentNodes;
currentNodes = new ArrayList<>();
for (PlacementSimulator.Node n : tmp)
for (TokenPlacementModel.Node n : tmp)
{
if (n.idx() == movingNode.idx())
currentNodes.add(movedTo);
@ -288,14 +290,14 @@ public class ModelState
return this;
}
private void addToCluster(PlacementSimulator.Node node)
private void addToCluster(TokenPlacementModel.Node node)
{
// called during both join and replacement
currentNodes = new ArrayList<>(currentNodes);
currentNodes.add(node);
}
public Transformer markMoving(PlacementSimulator.Node moving)
public Transformer markMoving(TokenPlacementModel.Node moving)
{
assert currentNodes.contains(moving);
movingNodes = new ArrayList<>(movingNodes);
@ -303,7 +305,7 @@ public class ModelState
return this;
}
public Transformer markLeaving(PlacementSimulator.Node leaving)
public Transformer markLeaving(TokenPlacementModel.Node leaving)
{
assert currentNodes.contains(leaving);
leavingNodes = new ArrayList<>(leavingNodes);
@ -311,7 +313,7 @@ public class ModelState
return this;
}
public Transformer withLeft(PlacementSimulator.Node node)
public Transformer withLeft(TokenPlacementModel.Node node)
{
assert currentNodes.contains(node);
// for now... assassinate may change this assertion
@ -321,7 +323,7 @@ public class ModelState
return this;
}
private void removeFromCluster(PlacementSimulator.Node node)
private void removeFromCluster(TokenPlacementModel.Node node)
{
// called during both decommission and replacement
currentNodes = new ArrayList<>(currentNodes);
@ -330,7 +332,7 @@ public class ModelState
leavingNodes.remove(node);
}
public Transformer withReplaced(PlacementSimulator.Node oldNode, PlacementSimulator.Node newNode)
public Transformer withReplaced(TokenPlacementModel.Node oldNode, TokenPlacementModel.Node newNode)
{
addToCluster(newNode);
removeFromCluster(oldNode);

View File

@ -31,6 +31,7 @@ import org.junit.Assert;
import org.junit.Test;
import org.apache.cassandra.config.DatabaseDescriptor;
import org.apache.cassandra.harry.sut.TokenPlacementModel;
import org.apache.cassandra.locator.EndpointsForRange;
import org.apache.cassandra.locator.Replica;
import org.apache.cassandra.schema.KeyspaceParams;
@ -60,9 +61,9 @@ public class OperationalEquivalenceTest extends CMSTestBase
}
// Private to this class for now as this is only a limited implementation
private static class TransientReplicationFactor extends PlacementSimulator.ReplicationFactor
private static class TransientReplicationFactor extends TokenPlacementModel.ReplicationFactor
{
private final PlacementSimulator.Lookup lookup = new PlacementSimulator.DefaultLookup();
private final TokenPlacementModel.Lookup lookup = new TokenPlacementModel.DefaultLookup();
private final int dcs;
private final int full;
private final int trans;
@ -96,7 +97,7 @@ public class OperationalEquivalenceTest extends CMSTestBase
throw new IllegalStateException("Does not work with transient replication");
}
public PlacementSimulator.ReplicatedRanges replicate(PlacementSimulator.Range[] ranges, List<PlacementSimulator.Node> nodes)
public TokenPlacementModel.ReplicatedRanges replicate(TokenPlacementModel.Range[] ranges, List<TokenPlacementModel.Node> nodes)
{
throw new IllegalStateException("Does not work with transient replication (yet)");
}
@ -105,28 +106,28 @@ public class OperationalEquivalenceTest extends CMSTestBase
@Test
public void testMove() throws Exception
{
testMove(new PlacementSimulator.SimpleReplicationFactor(2));
testMove(new PlacementSimulator.SimpleReplicationFactor(3));
testMove(new PlacementSimulator.SimpleReplicationFactor(5));
testMove(new TokenPlacementModel.SimpleReplicationFactor(2));
testMove(new TokenPlacementModel.SimpleReplicationFactor(3));
testMove(new TokenPlacementModel.SimpleReplicationFactor(5));
testMove(new PlacementSimulator.NtsReplicationFactor(1, 2));
testMove(new PlacementSimulator.NtsReplicationFactor(1, 3));
testMove(new PlacementSimulator.NtsReplicationFactor(1, 5));
testMove(new TokenPlacementModel.NtsReplicationFactor(1, 2));
testMove(new TokenPlacementModel.NtsReplicationFactor(1, 3));
testMove(new TokenPlacementModel.NtsReplicationFactor(1, 5));
testMove(new PlacementSimulator.NtsReplicationFactor(3, 2));
testMove(new PlacementSimulator.NtsReplicationFactor(3, 3));
testMove(new PlacementSimulator.NtsReplicationFactor(3, 5));
testMove(new TokenPlacementModel.NtsReplicationFactor(3, 2));
testMove(new TokenPlacementModel.NtsReplicationFactor(3, 3));
testMove(new TokenPlacementModel.NtsReplicationFactor(3, 5));
testMove(new TransientReplicationFactor(3, 3, 1));
testMove(new TransientReplicationFactor(3, 3, 2));
}
public void testMove(PlacementSimulator.ReplicationFactor rf) throws Exception
public void testMove(TokenPlacementModel.ReplicationFactor rf) throws Exception
{
PlacementSimulator.NodeFactory nodeFactory = PlacementSimulator.nodeFactory();
TokenPlacementModel.NodeFactory nodeFactory = TokenPlacementModel.nodeFactory();
ClusterMetadata withMove = null;
List<PlacementSimulator.Node> equivalentNodes = new ArrayList<>();
List<TokenPlacementModel.Node> equivalentNodes = new ArrayList<>();
int nodes = 30;
try (CMSSut sut = new CMSSut(AtomicLongBackedProcessor::new, false, rf))
{
@ -134,14 +135,14 @@ public class OperationalEquivalenceTest extends CMSTestBase
for (int i = 0; i < nodes; i++)
{
int dc = toDc(i, rf);
PlacementSimulator.Node node = nodeFactory.make(counter.incrementAndGet(), dc, 1);
TokenPlacementModel.Node node = nodeFactory.make(counter.incrementAndGet(), dc, 1);
sut.service.commit(new Register(new NodeAddresses(node.addr()), new Location(node.dc(), node.rack()), NodeVersion.CURRENT));
sut.service.commit(new UnsafeJoin(node.nodeId(), Collections.singleton(node.longToken()), sut.service.placementProvider()));
equivalentNodes.add(node);
}
PlacementSimulator.Node toMove = equivalentNodes.get(rng.nextInt(equivalentNodes.size()));
PlacementSimulator.Node moved = toMove.withNewToken();
TokenPlacementModel.Node toMove = equivalentNodes.get(rng.nextInt(equivalentNodes.size()));
TokenPlacementModel.Node moved = toMove.withNewToken();
equivalentNodes.set(equivalentNodes.indexOf(toMove), moved);
Move plan = SimulatedOperation.prepareMove(sut, toMove, moved.longToken()).get();
@ -156,12 +157,12 @@ public class OperationalEquivalenceTest extends CMSTestBase
withMove.placements);
}
private static ClusterMetadata simulateAndCompare(PlacementSimulator.ReplicationFactor rf, List<PlacementSimulator.Node> nodes) throws Exception
private static ClusterMetadata simulateAndCompare(TokenPlacementModel.ReplicationFactor rf, List<TokenPlacementModel.Node> nodes) throws Exception
{
Collections.shuffle(nodes, rng);
try (CMSSut sut = new CMSSut(AtomicLongBackedProcessor::new, false, rf))
{
for (PlacementSimulator.Node node : nodes)
for (TokenPlacementModel.Node node : nodes)
{
sut.service.commit(new Register(new NodeAddresses(node.addr()), new Location(node.dc(), node.rack()), NodeVersion.CURRENT));
sut.service.commit(new UnsafeJoin(node.nodeId(), Collections.singleton(node.longToken()), sut.service.placementProvider()));
@ -196,7 +197,7 @@ public class OperationalEquivalenceTest extends CMSTestBase
{
return ep.stream().sorted(Replica::compareTo).collect(Collectors.toList());
}
private static int toDc(int i, PlacementSimulator.ReplicationFactor rf)
private static int toDc(int i, TokenPlacementModel.ReplicationFactor rf)
{
return (i % rf.dcs()) + 1;
}

View File

@ -24,23 +24,25 @@ import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.OutputStreamWriter;
import java.net.UnknownHostException;
import java.util.*;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashSet;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.NavigableMap;
import java.util.Random;
import java.util.Set;
import java.util.TreeMap;
import java.util.function.Function;
import java.util.function.Predicate;
import java.util.stream.Collectors;
import org.junit.Assert;
import harry.generators.PCGFastPure;
import org.apache.cassandra.dht.Murmur3Partitioner;
import org.apache.cassandra.distributed.api.TokenSupplier;
import org.apache.cassandra.locator.InetAddressAndPort;
;
import org.apache.cassandra.schema.KeyspaceParams;
import org.apache.cassandra.tcm.ClusterMetadata;
import org.apache.cassandra.tcm.membership.Location;
import org.apache.cassandra.tcm.membership.NodeId;
import static org.apache.cassandra.harry.sut.TokenPlacementModel.Node;
import static org.apache.cassandra.harry.sut.TokenPlacementModel.Range;
import static org.apache.cassandra.harry.sut.TokenPlacementModel.ReplicationFactor;
/**
* A small class that helps to avoid doing mental arithmetics on ranges.
@ -398,7 +400,7 @@ public class PlacementSimulator
List<Node> finalNodes = new ArrayList<>();
for (int i = 0; i < origNodes.size(); i++)
{
if (origNodes.get(i).nodeIdx == movingNode.nodeIdx)
if (origNodes.get(i).idx() == movingNode.idx())
continue;
finalNodes.add(origNodes.get(i));
}
@ -674,8 +676,8 @@ public class PlacementSimulator
// bootstrap_diffBased implementation and the real code doesn't do this, only the endpoint matters for
// correctness, so we limit this comparison to endpoints only.
Assert.assertEquals(String.format("For key: %s\n", k),
expected.get(k).stream().map(n -> n.nodeIdx).sorted().collect(Collectors.toList()),
actual.get(k).stream().map(n -> n.nodeIdx).sorted().collect(Collectors.toList()));
expected.get(k).stream().map(n -> n.idx()).sorted().collect(Collectors.toList()),
actual.get(k).stream().map(n -> n.idx()).sorted().collect(Collectors.toList()));
});
}
@ -935,199 +937,6 @@ public class PlacementSimulator
return Collections.unmodifiableList(newNodes);
}
/**
* Replicate ranges to rf nodes.
*/
private static ReplicatedRanges combine(NavigableMap<Range, Map<String, List<Node>>> orig)
{
Range[] ranges = new Range[orig.size()];
int idx = 0;
NavigableMap<Range, List<Node>> flattened = new TreeMap<>();
for (Map.Entry<Range, Map<String, List<Node>>> e : orig.entrySet())
{
List<Node> placementsForRange = new ArrayList<>();
for (List<Node> v : e.getValue().values())
placementsForRange.addAll(v);
ranges[idx++] = e.getKey();
flattened.put(e.getKey(), placementsForRange);
}
return new ReplicatedRanges(ranges, flattened);
}
public static class ReplicatedRanges
{
private final Range[] ranges;
private final NavigableMap<Range, List<Node>> placementsForRange;
public ReplicatedRanges(Range[] ranges, NavigableMap<Range, List<Node>> placementsForRange)
{
this.ranges = ranges;
this.placementsForRange = placementsForRange;
}
public List<Node> replicasFor(long token)
{
int idx = indexedBinarySearch(ranges, range -> {
// exclusive start, so token at the start belongs to a lower range
if (token <= range.start)
return 1;
// ie token > start && token <= end
if (token <= range.end ||range.end == Long.MIN_VALUE)
return 0;
return -1;
});
assert idx >= 0 : String.format("Somehow ranges %s do not contain token %d", Arrays.toString(ranges), token);
return placementsForRange.get(ranges[idx]);
}
public NavigableMap<Range, List<Node>> asMap()
{
return placementsForRange;
}
private static <T> int indexedBinarySearch(T[] arr, CompareTo<T> comparator)
{
int low = 0;
int high = arr.length - 1;
while (low <= high)
{
int mid = (low + high) >>> 1;
T midEl = arr[mid];
int cmp = comparator.compareTo(midEl);
if (cmp < 0)
low = mid + 1;
else if (cmp > 0)
high = mid - 1;
else
return mid;
}
return -(low + 1); // key not found
}
}
public interface CompareTo<V>
{
int compareTo(V v);
}
private static <K extends Comparable<K>, T1, T2> Map<K, T2> mapValues(Map<K, T1> allDCs, Function<T1, T2> map)
{
NavigableMap<K, T2> res = new TreeMap<>();
for (Map.Entry<K, T1> e : allDCs.entrySet())
{
res.put(e.getKey(), map.apply(e.getValue()));
}
return res;
}
public static Map<String, List<Node>> nodesByDC(List<Node> nodes)
{
Map<String, List<Node>> nodesByDC = new HashMap<>();
for (Node node : nodes)
nodesByDC.computeIfAbsent(node.dc(), (k) -> new ArrayList<>()).add(node);
return nodesByDC;
}
public static Map<String, Set<String>> racksByDC(List<Node> nodes)
{
Map<String, Set<String>> racksByDC = new HashMap<>();
for (Node node : nodes)
racksByDC.computeIfAbsent(node.dc(), (k) -> new HashSet<>()).add(node.rack());
return racksByDC;
}
private static final class DatacenterNodes
{
private final List<Node> nodes = new ArrayList<>();
private final Set<Location> racks = new HashSet<>();
/** Number of replicas left to fill from this DC. */
int rfLeft;
int acceptableRackRepeats;
public DatacenterNodes copy()
{
return new DatacenterNodes(rfLeft, acceptableRackRepeats);
}
DatacenterNodes(int rf,
int rackCount,
int nodeCount)
{
this.rfLeft = Math.min(rf, nodeCount);
acceptableRackRepeats = rf - rackCount;
}
// for copying
DatacenterNodes(int rfLeft, int acceptableRackRepeats)
{
this.rfLeft = rfLeft;
this.acceptableRackRepeats = acceptableRackRepeats;
}
boolean addAndCheckIfDone(Node node, Location location)
{
if (done())
return false;
if (nodes.contains(node))
// Cannot repeat a node.
return false;
if (racks.add(location))
{
// New rack.
--rfLeft;
nodes.add(node);
return done();
}
if (acceptableRackRepeats <= 0)
// There must be rfLeft distinct racks left, do not add any more rack repeats.
return false;
nodes.add(node);
// Added a node that is from an already met rack to match RF when there aren't enough racks.
--acceptableRackRepeats;
--rfLeft;
return done();
}
boolean done()
{
assert rfLeft >= 0;
return rfLeft == 0;
}
}
public static void addIfUnique(List<Node> nodes, Set<Integer> names, Node node)
{
if (names.contains(node.idx()))
return;
nodes.add(node);
names.add(node.idx());
}
/**
* Finds a primary replica
*/
public static int primaryReplica(List<Node> nodes, Range range)
{
for (int i = 0; i < nodes.size(); i++)
{
if (range.end != Long.MIN_VALUE && nodes.get(i).token() >= range.end)
return i;
}
return -1;
}
/**
* Generates token ranges from the list of nodes
*/
@ -1190,297 +999,6 @@ public class PlacementSimulator
}
}
/**
* A Range is responsible for the tokens between (start, end].
*/
public static class Range implements Comparable<Range>
{
public final long start;
public final long end;
public Range(long start, long end)
{
assert end > start || end == Long.MIN_VALUE : String.format("Start (%d) should be smaller than end (%d)", start, end);
this.start = start;
this.end = end;
}
public boolean contains(long min, long max)
{
assert max > min;
return min > start && (max <= end || end == Long.MIN_VALUE);
}
public boolean contains(long token)
{
return token > start && (token <= end || end == Long.MIN_VALUE);
}
public int compareTo(Range o)
{
int res = Long.compare(start, o.start);
if (res == 0)
return Long.compare(end, o.end);
return res;
}
public boolean equals(Object o)
{
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
Range range = (Range) o;
return start == range.start && end == range.end;
}
public int hashCode()
{
return Objects.hash(start, end);
}
public String toString()
{
return "(" +
"" + (start == Long.MIN_VALUE ? "MIN" : start) +
", " + (end == Long.MIN_VALUE ? "MIN" : end) +
']';
}
}
public interface Lookup
{
String id(int nodeIdx);
String dc(int dcIdx);
String rack(int rackIdx);
NodeId nodeId(int nodeIdx);
long token(int tokenIdx);
Lookup forceToken(int tokenIdx, long token);
InetAddressAndPort addr(int idx);
void reset();
}
public static class DefaultLookup implements Lookup
{
protected final Map<Integer, Long> overrides = new HashMap<>(2);
public String id(int nodeIdx)
{
return String.format("127.0.%d.%d", nodeIdx / 256, nodeIdx % 256);
}
public NodeId nodeId(int nodeIdx)
{
return ClusterMetadata.current().directory.peerId(addr(nodeIdx));
}
public long token(int tokenIdx)
{
Long override = overrides.get(tokenIdx);
if (override != null)
return override;
return PCGFastPure.next(tokenIdx, 1L);
}
public Lookup forceToken(int tokenIdx, long token)
{
DefaultLookup newLookup = new DefaultLookup();
newLookup.overrides.putAll(overrides);
newLookup.overrides.put(tokenIdx, token);
return newLookup;
}
public InetAddressAndPort addr(int idx)
{
try
{
return InetAddressAndPort.getByName(id(idx));
}
catch (UnknownHostException e)
{
throw new RuntimeException(e);
}
}
public void reset()
{
overrides.clear();
}
public String dc(int dcIdx)
{
return String.format("datacenter%d", dcIdx);
}
public String rack(int rackIdx)
{
return String.format("rack%d", rackIdx);
}
}
public static class HumanReadableTokensLookup extends DefaultLookup {
@Override
public long token(int tokenIdx)
{
Long override = overrides.get(tokenIdx);
if (override != null)
return override;
return tokenIdx * 100L;
}
public Lookup forceToken(int tokenIdx, long token)
{
DefaultLookup lookup = new HumanReadableTokensLookup();
lookup.overrides.putAll(overrides);
lookup.overrides.put(tokenIdx, token);
return lookup;
}
}
public static NodeFactory nodeFactory()
{
return new NodeFactory(new DefaultLookup());
}
public static NodeFactory nodeFactoryHumanReadable()
{
return new NodeFactory(new HumanReadableTokensLookup());
}
public static class NodeFactory implements TokenSupplier
{
private final Lookup lookup;
public NodeFactory(Lookup lookup)
{
this.lookup = lookup;
}
public Node make(int idx, int dc, int rack)
{
return new Node(idx, idx, dc, rack, lookup);
}
public Lookup lookup()
{
return lookup;
}
public Collection<String> tokens(int i)
{
return Collections.singletonList(Long.toString(lookup.token(i)));
}
}
public static class Node implements Comparable<Node>
{
private final int tokenIdx;
private final int nodeIdx;
private final int dcIdx;
private final int rackIdx;
private final Lookup lookup;
private Node(int tokenIdx, int idx, int dcIdx, int rackIdx, Lookup lookup)
{
this.tokenIdx = tokenIdx;
this.nodeIdx = idx;
this.dcIdx = dcIdx;
this.rackIdx = rackIdx;
this.lookup = lookup;
}
public InetAddressAndPort addr()
{
return lookup.addr(nodeIdx);
}
public NodeId nodeId()
{
return lookup.nodeId(nodeIdx);
}
public String id()
{
return lookup.id(nodeIdx);
}
public int idx()
{
return nodeIdx;
}
public int dcIdx()
{
return dcIdx;
}
public int rackIdx()
{
return rackIdx;
}
public String dc()
{
return lookup.dc(dcIdx);
}
public String rack()
{
return lookup.rack(rackIdx);
}
public long token()
{
return lookup.token(tokenIdx);
}
public int tokenIdx()
{
return tokenIdx;
}
public Murmur3Partitioner.LongToken longToken()
{
return new Murmur3Partitioner.LongToken(token());
}
public Node withNewToken()
{
return new Node(tokenIdx + 100_000, nodeIdx, dcIdx, rackIdx, lookup);
}
public Node withToken(int tokenIdx)
{
return new Node(tokenIdx, nodeIdx, dcIdx, rackIdx, lookup);
}
public Node overrideToken(long override)
{
return new Node(tokenIdx, nodeIdx, dcIdx, rackIdx, lookup.forceToken(tokenIdx, override));
}
public boolean equals(Object o)
{
if (this == o) return true;
if (o == null || !Node.class.isAssignableFrom(o.getClass())) return false;
Node node = (Node) o;
return Objects.equals(nodeIdx, node.nodeIdx);
}
public int hashCode()
{
return Objects.hash(nodeIdx);
}
public int compareTo(Node o)
{
return Long.compare(token(), o.token());
}
public String toString()
{
return String.format("%s-%s@%d", dc(), id(), token());
}
}
public static String diffsToString(Map<Range, Diff<Node>> placements)
{
StringBuilder builder = new StringBuilder();
@ -1535,255 +1053,4 @@ public class PlacementSimulator
}
}
}
public abstract static class ReplicationFactor
{
private final int nodesTotal;
public ReplicationFactor(int total)
{
this.nodesTotal = total;
}
public int total()
{
return nodesTotal;
}
public abstract int dcs();
public abstract KeyspaceParams asKeyspaceParams();
public abstract Map<String, Integer> asMap();
public ReplicatedRanges replicate(List<Node> nodes)
{
return replicate(toRanges(nodes), nodes);
}
public abstract ReplicatedRanges replicate(Range[] ranges, List<Node> nodes);
}
public static class NtsReplicationFactor extends ReplicationFactor
{
private final Lookup lookup = new DefaultLookup();
public final int[] nodesPerDc;
private KeyspaceParams keyspaceParams;
private Map<String, Integer> map;
public NtsReplicationFactor(int... nodesPerDc)
{
super(total(nodesPerDc));
this.nodesPerDc = nodesPerDc;
}
public NtsReplicationFactor(int dcs, int nodesPerDc)
{
super(dcs * nodesPerDc);
this.nodesPerDc = new int[dcs];
Arrays.fill(this.nodesPerDc, nodesPerDc);
}
private static int total(int... num)
{
int tmp = 0;
for (int i : num)
tmp += i;
return tmp;
}
public int dcs()
{
return nodesPerDc.length;
}
public KeyspaceParams asKeyspaceParams()
{
if (this.keyspaceParams == null)
this.keyspaceParams = toKeyspaceParams(lookup);
return this.keyspaceParams;
}
public Map<String, Integer> asMap()
{
if (this.map == null)
this.map = toMap(lookup);
return this.map;
}
public ReplicatedRanges replicate(Range[] ranges, List<Node> nodes)
{
return replicate(ranges, nodes, asMap());
}
private static <T extends Comparable<T>> void assertStrictlySorted(Collection<T> coll)
{
if (coll.size() <= 1) return;
Iterator<T> iter = coll.iterator();
T prev = iter.next();
while (iter.hasNext())
{
T next = iter.next();
assert next.compareTo(prev) > 0 : String.format("Collection does not seem to be sorted. %s and %s are in wrong order", prev, next);
prev = next;
}
}
public static ReplicatedRanges replicate(Range[] ranges, List<Node> nodes, Map<String, Integer> rfs)
{
assertStrictlySorted(nodes);
Map<String, DatacenterNodes> template = new HashMap<>();
Map<String, List<Node>> nodesByDC = nodesByDC(nodes);
Map<String, Set<String>> racksByDC = racksByDC(nodes);
for (Map.Entry<String, Integer> entry : rfs.entrySet())
{
String dc = entry.getKey();
int rf = entry.getValue();
List<Node> nodesInThisDC = nodesByDC.get(dc);
Set<String> racksInThisDC = racksByDC.get(dc);
int nodeCount = nodesInThisDC == null ? 0 : nodesInThisDC.size();
int rackCount = racksInThisDC == null ? 0 : racksInThisDC.size();
if (rf <= 0 || nodeCount == 0)
continue;
template.put(dc, new DatacenterNodes(rf, rackCount, nodeCount));
}
NavigableMap<Range, Map<String, List<Node>>> replication = new TreeMap<>();
for (Range range : ranges)
{
final int idx = primaryReplica(nodes, range);
int cnt = 0;
if (idx >= 0)
{
int dcsToFill = template.size();
Map<String, DatacenterNodes> nodesInDCs = new HashMap<>();
for (Map.Entry<String, DatacenterNodes> e : template.entrySet())
nodesInDCs.put(e.getKey(), e.getValue().copy());
while (dcsToFill > 0 && cnt < nodes.size())
{
Node node = nodes.get((idx + cnt) % nodes.size());
DatacenterNodes dcNodes = nodesInDCs.get(node.dc());
if (dcNodes != null && dcNodes.addAndCheckIfDone(node, new Location(node.dc(), node.rack())))
dcsToFill--;
cnt++;
}
replication.put(range, mapValues(nodesInDCs, v -> v.nodes));
}
else
{
// if the range end is larger than the highest assigned token, then treat it
// as part of the wraparound and replicate it to the same nodes as the first
// range. This is most likely caused by a decommission removing the node with
// the largest token.
replication.put(range, replication.get(ranges[0]));
}
}
return combine(replication);
}
private KeyspaceParams toKeyspaceParams(Lookup lookup)
{
Object[] args = new Object[nodesPerDc.length * 2];
for (int i = 0; i < nodesPerDc.length; i++)
{
args[i * 2] = lookup.dc(i + 1);
args[i * 2 + 1] = nodesPerDc[i];
}
return KeyspaceParams.nts(args);
}
private Map<String, Integer> toMap(Lookup lookup)
{
Map<String, Integer> map = new TreeMap<>();
for (int i = 0; i < nodesPerDc.length; i++)
{
map.put(lookup.dc(i + 1), nodesPerDc[i]);
}
return map;
}
public String toString()
{
return "NtsReplicationFactor{" +
"map=" + asMap() +
'}';
}
}
public static class SimpleReplicationFactor extends ReplicationFactor
{
private final Lookup lookup = new DefaultLookup();
public SimpleReplicationFactor(int total)
{
super(total);
}
public int dcs()
{
return 1;
}
public KeyspaceParams asKeyspaceParams()
{
return KeyspaceParams.simple(total());
}
public Map<String, Integer> asMap()
{
return Collections.singletonMap(lookup.dc(1), total());
}
public ReplicatedRanges replicate(Range[] ranges, List<Node> nodes)
{
return replicate(ranges, nodes, total());
}
public static ReplicatedRanges replicate(Range[] ranges, List<Node> nodes, int rf)
{
NavigableMap<Range, List<Node>> replication = new TreeMap<>();
for (Range range : ranges)
{
Set<Integer> names = new HashSet<>();
List<Node> replicas = new ArrayList<>();
int idx = primaryReplica(nodes, range);
if (idx >= 0)
{
for (int i = idx; i < nodes.size() && replicas.size() < rf; i++)
addIfUnique(replicas, names, nodes.get(i));
for (int i = 0; replicas.size() < rf && i < idx; i++)
addIfUnique(replicas, names, nodes.get(i));
if (range.start == Long.MIN_VALUE)
replication.put(ranges[ranges.length - 1], replicas);
replication.put(range, replicas);
}
else
{
// if the range end is larger than the highest assigned token, then treat it
// as part of the wraparound and replicate it to the same nodes as the first
// range. This is most likely caused by a decommission removing the node with
// the largest token.
replication.put(range, replication.get(ranges[0]));
}
}
return new ReplicatedRanges(ranges, Collections.unmodifiableNavigableMap(replication));
}
public String toString()
{
return "SimpleReplicationFactor{" +
"rf=" + total() +
'}';
}
}
}

View File

@ -30,7 +30,24 @@ import java.util.function.Supplier;
import org.junit.Test;
import static org.apache.cassandra.distributed.test.log.PlacementSimulator.*;
import org.apache.cassandra.harry.sut.TokenPlacementModel;
import static org.apache.cassandra.distributed.test.log.PlacementSimulator.SimulatedPlacements;
import static org.apache.cassandra.distributed.test.log.PlacementSimulator.Transformations;
import static org.apache.cassandra.distributed.test.log.PlacementSimulator.assertPlacements;
import static org.apache.cassandra.distributed.test.log.PlacementSimulator.assertRanges;
import static org.apache.cassandra.distributed.test.log.PlacementSimulator.filter;
import static org.apache.cassandra.distributed.test.log.PlacementSimulator.join;
import static org.apache.cassandra.distributed.test.log.PlacementSimulator.leave;
import static org.apache.cassandra.distributed.test.log.PlacementSimulator.move;
import static org.apache.cassandra.distributed.test.log.PlacementSimulator.replace;
import static org.apache.cassandra.distributed.test.log.PlacementSimulator.split;
import static org.apache.cassandra.distributed.test.log.PlacementSimulator.superset;
import static org.apache.cassandra.harry.sut.TokenPlacementModel.Node;
import static org.apache.cassandra.harry.sut.TokenPlacementModel.NodeFactory;
import static org.apache.cassandra.harry.sut.TokenPlacementModel.Range;
import static org.apache.cassandra.harry.sut.TokenPlacementModel.ReplicationFactor;
import static org.apache.cassandra.harry.sut.TokenPlacementModel.SimpleReplicationFactor;
import static org.junit.Assert.assertTrue;
public class PlacementSimulatorTest
@ -50,7 +67,7 @@ public class PlacementSimulatorTest
public void testMove(long t1, long t2, long t3, long t4, long newToken, ReplicationFactor rf)
{
NodeFactory factory = PlacementSimulator.nodeFactory();
NodeFactory factory = TokenPlacementModel.nodeFactory();
Node movingNode = factory.make(1, 1, 1).overrideToken(t1);
List<Node> orig = Arrays.asList(movingNode,
factory.make(2, 1, 1).overrideToken(t2),
@ -103,7 +120,7 @@ public class PlacementSimulatorTest
public void testBootstrap(long t1, long t2, long t3, long t4, long newToken, ReplicationFactor rf)
{
NodeFactory factory = PlacementSimulator.nodeFactory();
NodeFactory factory = TokenPlacementModel.nodeFactory();
List<Node> orig = Arrays.asList(factory.make(1, 1, 1).overrideToken(t1),
factory.make(2, 1, 1).overrideToken(t2),
factory.make(3, 1, 1).overrideToken(t3),
@ -157,7 +174,7 @@ public class PlacementSimulatorTest
public void testDecommission(long t1, long t2, long t3, long t4, long t5, ReplicationFactor rf)
{
NodeFactory factory = PlacementSimulator.nodeFactory();
NodeFactory factory = TokenPlacementModel.nodeFactory();
Node leavingNode = factory.make(1, 1, 1).overrideToken(t1);
List<Node> orig = Arrays.asList(leavingNode,
factory.make(2, 1, 1).overrideToken(t2),
@ -225,7 +242,7 @@ public class PlacementSimulatorTest
public void simulate(ReplicationFactor rf) throws Throwable
{
NodeFactory factory = PlacementSimulator.nodeFactory();
NodeFactory factory = TokenPlacementModel.nodeFactory();
List<Node> orig = Collections.singletonList(factory.make(1, 1, 1));
ModelChecker<SimulatedPlacements, SUTState> modelChecker = new ModelChecker<>();
@ -287,7 +304,7 @@ public class PlacementSimulatorTest
for (int n : new int[]{ 2, 3, 5 })
{
ReplicationFactor rf = new SimpleReplicationFactor(n);
NodeFactory factory = PlacementSimulator.nodeFactoryHumanReadable();
NodeFactory factory = TokenPlacementModel.nodeFactoryHumanReadable();
List<Node> nodes = new ArrayList<>(10);
for (int i = 1; i <= 10; i++)
nodes.add(factory.make(i, 1, 1));
@ -304,7 +321,7 @@ public class PlacementSimulatorTest
for (int n : new int[]{ 2, 3, 5 })
{
ReplicationFactor rf = new SimpleReplicationFactor(n);
NodeFactory factory = PlacementSimulator.nodeFactoryHumanReadable();
NodeFactory factory = TokenPlacementModel.nodeFactoryHumanReadable();
List<Node> nodes = new ArrayList<>(10);
for (int i = 1; i <= 10; i++)
nodes.add(factory.make(i, 1, 1));
@ -321,7 +338,7 @@ public class PlacementSimulatorTest
for (int n : new int[]{ 2, 3, 5 })
{
ReplicationFactor rf = new SimpleReplicationFactor(n);
NodeFactory factory = PlacementSimulator.nodeFactoryHumanReadable();
NodeFactory factory = TokenPlacementModel.nodeFactoryHumanReadable();
List<Node> nodes = new ArrayList<>(10);
for (int i = 1; i <= 10; i++)
nodes.add(factory.make(i, 1, 1));

View File

@ -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();
}
}

View File

@ -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
{

View File

@ -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());

View File

@ -0,0 +1,144 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.apache.cassandra.fuzz.harry.examples;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.function.Supplier;
import org.junit.Test;
import org.apache.cassandra.harry.checker.ModelChecker;
import org.apache.cassandra.harry.ddl.SchemaGenerators;
import org.apache.cassandra.harry.ddl.SchemaSpec;
import org.apache.cassandra.harry.dsl.ReplayingHistoryBuilder;
import org.apache.cassandra.harry.gen.EntropySource;
import org.apache.cassandra.harry.gen.rng.JdkRandomEntropySource;
import org.apache.cassandra.harry.model.AgainstSutChecker;
import org.apache.cassandra.fuzz.harry.integration.model.IntegrationTestBase;
import org.apache.cassandra.harry.sut.QueryModifyingSut;
import org.apache.cassandra.harry.sut.SystemUnderTest;
import org.apache.cassandra.harry.sut.TokenPlacementModel;
import org.apache.cassandra.harry.tracker.DataTracker;
import org.apache.cassandra.harry.tracker.DefaultDataTracker;
public class RangeTombstoneBurnTest extends IntegrationTestBase
{
private final long seed = 1;
private final int ITERATIONS = 5;
private final int STEPS_PER_ITERATION = 100;
@Test
public void rangeTombstoneBurnTest() throws Throwable
{
Supplier<SchemaSpec> supplier = SchemaGenerators.progression(SchemaGenerators.DEFAULT_SWITCH_AFTER);
for (int i = 0; i < SchemaGenerators.DEFAULT_RUNS; i++)
{
SchemaSpec schema = supplier.get();
beforeEach();
SchemaSpec doubleWriteSchema = schema.cloneWithName(schema.keyspace, schema.keyspace + "_debug");
sut.schemaChange(schema.compile().cql());
sut.schemaChange(doubleWriteSchema.compile().cql());
QueryModifyingSut sut = new QueryModifyingSut(this.sut,
schema.table,
doubleWriteSchema.table);
cluster.get(1).nodetool("disableautocompaction");
for (int iteration = 0; iteration < ITERATIONS; iteration++)
{
ModelChecker<ReplayingHistoryBuilder> modelChecker = new ModelChecker<>();
EntropySource entropySource = new JdkRandomEntropySource(iteration);
int maxPartitionSize = entropySource.nextInt(1, 1 << entropySource.nextInt(5, 11));
int[] partitions = new int[10];
for (int j = 0; j < partitions.length; j++)
partitions[j] = iteration * partitions.length + j;
float deleteRowChance = entropySource.nextFloat(0.99f, 1.0f);
float deletePartitionChance = entropySource.nextFloat(0.999f, 1.0f);
float deleteRangeChance = entropySource.nextFloat(0.95f, 1.0f);
float flushChance = entropySource.nextFloat(0.999f, 1.0f);
AtomicInteger flushes = new AtomicInteger();
TokenPlacementModel.ReplicationFactor rf = new TokenPlacementModel.SimpleReplicationFactor(1);
DataTracker tracker = new DefaultDataTracker();
modelChecker.init(new ReplayingHistoryBuilder(seed, maxPartitionSize, STEPS_PER_ITERATION, new DefaultDataTracker(), sut, schema, rf, SystemUnderTest.ConsistencyLevel.ALL))
.step((history) -> {
int rowIdx = entropySource.nextInt(maxPartitionSize);
int partitionIdx = partitions[entropySource.nextInt(partitions.length)];
history.visitPartition(partitionIdx).insert(rowIdx);
return history;
})
.step((history) -> entropySource.nextFloat() > deleteRowChance,
(history) -> {
int partitionIdx = partitions[entropySource.nextInt(partitions.length)];
history.visitPartition(partitionIdx).deleteRow();
return history;
})
.step((history) -> entropySource.nextFloat() > deleteRowChance,
(history) -> {
int partitionIdx = partitions[entropySource.nextInt(partitions.length)];
history.visitPartition(partitionIdx).deleteColumns();
return history;
})
.step((history) -> entropySource.nextFloat() > deletePartitionChance,
(history) -> {
int partitionIdx = partitions[entropySource.nextInt(partitions.length)];
history.visitPartition(partitionIdx).deletePartition();
return history;
})
.step((history) -> entropySource.nextFloat() > flushChance,
(history) -> {
cluster.get(1).nodetool("flush", schema.keyspace, schema.table);
flushes.incrementAndGet();
return history;
})
.step((history) -> entropySource.nextFloat() > deleteRangeChance,
(history) -> {
int partitionIdx = partitions[entropySource.nextInt(partitions.length)];
history.visitPartition(partitionIdx).deleteRowSlice();
return history;
})
.step((history) -> entropySource.nextFloat() > deleteRangeChance,
(history) -> {
int row1 = entropySource.nextInt(maxPartitionSize);
int row2 = entropySource.nextInt(maxPartitionSize);
int partitionIdx = partitions[entropySource.nextInt(partitions.length)];
history.visitPartition(partitionIdx).deleteRowRange(Math.min(row1, row2),
Math.max(row1, row2),
entropySource.nextBoolean(),
entropySource.nextBoolean());
return history;
})
.afterAll((history) -> {
// Sanity check
history.validate(new AgainstSutChecker(tracker, history.clock(), sut, schema, doubleWriteSchema),
partitions);
history.validate(partitions);
})
.run(STEPS_PER_ITERATION, seed);
}
}
}
}

View File

@ -0,0 +1,504 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.apache.cassandra.fuzz.harry.gen;
import java.lang.reflect.Array;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.Comparator;
import java.util.Iterator;
import java.util.List;
import java.util.function.BiConsumer;
import java.util.function.Consumer;
import org.junit.Assert;
import org.junit.Ignore;
import org.junit.Test;
import org.apache.cassandra.harry.ddl.ColumnSpec;
import org.apache.cassandra.harry.gen.Bijections;
import org.apache.cassandra.harry.gen.Bytes;
import org.apache.cassandra.harry.gen.DataGenerators;
import org.apache.cassandra.harry.gen.EntropySource;
import org.apache.cassandra.harry.gen.Generator;
import org.apache.cassandra.harry.gen.StringBijection;
public class DataGeneratorsTest
{
private static final int RUNS = 100;
private static final EntropySource rand = EntropySource.forTests(1);
@Test
public void testSingleTypeRoundTrip()
{
for (ColumnSpec.DataType dt : new ColumnSpec.DataType[]{ColumnSpec.int8Type,
ColumnSpec.int16Type,
ColumnSpec.int32Type,
ColumnSpec.int64Type,
ColumnSpec.asciiType,
ColumnSpec.floatType,
ColumnSpec.doubleType})
{
for (int i = 0; i < RUNS; i++)
{
DataGenerators.SinglePartKeyGenerator gen = new DataGenerators.SinglePartKeyGenerator(Collections.singletonList(ColumnSpec.ck("ck0", dt, false)));
long descriptor = rand.next();
descriptor = gen.adjustEntropyDomain(descriptor);
Assert.assertEquals(descriptor,
gen.deflate(gen.inflate(descriptor)));
}
}
}
@Test
public void testRequiredBytes()
{
testRequiredBytes(sizes(4, 1),
ColumnSpec.int32Type, ColumnSpec.int8Type);
testRequiredBytes(sizes(7, 1),
ColumnSpec.int64Type, ColumnSpec.int8Type);
testRequiredBytes(sizes(4, 4),
ColumnSpec.int32Type, ColumnSpec.int64Type);
testRequiredBytes(sizes(4, 4),
ColumnSpec.int32Type, ColumnSpec.int32Type);
testRequiredBytes(sizes(4, 3),
ColumnSpec.int32Type, ColumnSpec.floatType);
testRequiredBytes(sizes(4, 4),
ColumnSpec.int64Type, ColumnSpec.int64Type);
testRequiredBytes(sizes(4, 2, 2),
ColumnSpec.int64Type, ColumnSpec.int16Type, ColumnSpec.int32Type);
testRequiredBytes(sizes(6, 1, 1),
ColumnSpec.int64Type, ColumnSpec.int8Type, ColumnSpec.int8Type);
testRequiredBytes(sizes(4, 2, 2),
ColumnSpec.int32Type, ColumnSpec.int32Type, ColumnSpec.int64Type);
testRequiredBytes(sizes(4, 2, 2),
ColumnSpec.int32Type, ColumnSpec.int32Type, ColumnSpec.int64Type);
testRequiredBytes(sizes(1, 5, 2),
ColumnSpec.int8Type, ColumnSpec.asciiType, ColumnSpec.int64Type);
testRequiredBytes(sizes(1, 1, 6),
ColumnSpec.int8Type, ColumnSpec.int8Type, ColumnSpec.int64Type);
testRequiredBytes(sizes(2, 2, 2, 2),
ColumnSpec.int64Type, ColumnSpec.int32Type, ColumnSpec.int32Type, ColumnSpec.int32Type);
testRequiredBytes(sizes(1, 3, 2, 2),
ColumnSpec.int8Type, ColumnSpec.int32Type, ColumnSpec.int32Type, ColumnSpec.int32Type);
testRequiredBytes(sizes(1, 3, 2, 2),
ColumnSpec.int8Type, ColumnSpec.int32Type, ColumnSpec.int32Type, ColumnSpec.int32Type);
testRequiredBytes(sizes(1, 3, 2, 2),
ColumnSpec.int8Type, ColumnSpec.int32Type, ColumnSpec.int32Type, ColumnSpec.int32Type, ColumnSpec.int64Type);
}
private static void testRequiredBytes(int[] sizes,
ColumnSpec.DataType<?>... types)
{
int sum = 0;
for (int size : sizes)
sum += size;
Assert.assertTrue(sum > 0);
Assert.assertTrue(sum <= 8);
List<ColumnSpec<?>> columns = new ArrayList<>(types.length);
for (int i = 0; i < types.length; i++)
columns.add(ColumnSpec.ck("r" + i, types[i], false));
Assert.assertArrayEquals(columns.toString(),
sizes,
DataGenerators.requiredBytes(columns));
}
@Test
public void testSliceStitch()
{
for (int i = 2; i < 5; i++)
{
Iterator<ColumnSpec.DataType[]> iter = permutations(i,
ColumnSpec.DataType.class,
ColumnSpec.int8Type,
ColumnSpec.asciiType,
ColumnSpec.int16Type,
ColumnSpec.int32Type,
ColumnSpec.int32Type,
ColumnSpec.floatType,
ColumnSpec.doubleType);
while (iter.hasNext())
{
testSliceStitch(iter.next());
}
}
}
private static void testSliceStitch(ColumnSpec.DataType... types)
{
List<ColumnSpec<?>> spec = new ArrayList<>(types.length);
for (int i = 0; i < types.length; i++)
spec.add(ColumnSpec.ck("r" + i, types[i], false));
DataGenerators.MultiPartKeyGenerator gen = new DataGenerators.MultiPartKeyGenerator(spec);
for (int i = 0; i < RUNS; i++)
{
long orig = gen.adjustEntropyDomain(rand.next());
long[] sliced = gen.slice(orig);
long stitched = gen.stitch(sliced);
Assert.assertEquals(String.format("Orig: %s. Stitched: %s",
Long.toHexString(orig),
Long.toHexString(stitched)),
orig, stitched);
}
}
@Test
public void testKeyGenerators()
{
for (int i = 1; i < 5; i++)
{
for (boolean asReversed : new boolean[]{ false, true })
{
Iterator<ColumnSpec.DataType[]> iter = permutations(i,
ColumnSpec.DataType.class,
ColumnSpec.int8Type,
ColumnSpec.asciiType,
ColumnSpec.int16Type,
ColumnSpec.int32Type,
ColumnSpec.int64Type,
ColumnSpec.floatType,
ColumnSpec.doubleType
);
while (iter.hasNext())
{
ColumnSpec.DataType[] types = iter.next();
try
{
testKeyGenerators(asReversed, types);
}
catch (Throwable t)
{
throw new AssertionError("Caught error for the type combination " + Arrays.toString(types), t);
}
}
}
}
}
@Ignore
@Test // this one is mostly useful when the above test fails and you need a quicker turnaround
public void testSomeKeyGenerators()
{
Iterator<ColumnSpec.DataType[]> iter = Collections.singletonList(new ColumnSpec.DataType[]{ ColumnSpec.int32Type, ColumnSpec.int32Type }).iterator();
while (iter.hasNext())
{
ColumnSpec.DataType[] types = iter.next();
try
{
testKeyGenerators(true, types);
testKeyGenerators(false, types);
}
catch (Throwable t)
{
throw new AssertionError("Caught error for the type combination " + Arrays.toString(types), t);
}
}
}
static void testKeyGenerators(boolean reversed, ColumnSpec.DataType<?>... types)
{
List<ColumnSpec<?>> spec = new ArrayList<>(types.length);
for (int i = 0; i < types.length; i++)
spec.add(ColumnSpec.ck("r" + i, types[i], reversed));
DataGenerators.KeyGenerator keyGenerator = DataGenerators.createKeyGenerator(spec);
for (int i = 0; i < RUNS; i++)
{
testKeyGenerators(rand.next(), rand.next(), keyGenerator);
// test some edge cases
testKeyGenerators(0, 0, keyGenerator);
testKeyGenerators(0xffffffffffffffffL, 0xffffffffffffffffL, keyGenerator);
testKeyGenerators(keyGenerator.minValue(), keyGenerator.maxValue(), keyGenerator);
testKeyGenerators(0, keyGenerator.minValue(), keyGenerator);
testKeyGenerators(0, keyGenerator.maxValue(), keyGenerator);
long descriptor = rand.next();
testKeyGenerators(descriptor, 0, keyGenerator);
testKeyGenerators(descriptor, keyGenerator.minValue(), keyGenerator);
testKeyGenerators(descriptor, keyGenerator.maxValue(), keyGenerator);
testKeyGenerators(descriptor, 0xffffffffffffffffL, keyGenerator);
testKeyGenerators(descriptor, descriptor + 1, keyGenerator);
testKeyGenerators(descriptor, descriptor - 1, keyGenerator);
testKeyGenerators(descriptor, descriptor, keyGenerator);
}
// Fixed prefix, tests sign inversion of subsequent values
if (types.length > 1)
{
long pattern = Bytes.bytePatternFor(Long.BYTES - DataGenerators.requiredBytes(spec)[0]);
for (int i = 0; i < RUNS; i++)
{
long descriptor = rand.next();
long descriptor2 = (descriptor & ~pattern) | (rand.next() & pattern);
testKeyGenerators(descriptor, descriptor2, keyGenerator);
}
}
}
static void testKeyGenerators(long descriptor1, long descriptor2, DataGenerators.KeyGenerator keyGenerator)
{
descriptor1 = keyGenerator.adjustEntropyDomain(descriptor1);
descriptor2 = keyGenerator.adjustEntropyDomain(descriptor2);
Object[] value1 = keyGenerator.inflate(descriptor1);
Object[] value2 = keyGenerator.inflate(descriptor2);
assertDescriptorsEqual(descriptor1,
keyGenerator.deflate(value1));
assertDescriptorsEqual(descriptor2,
keyGenerator.deflate(value2));
Assert.assertEquals(String.format("%s %s %s and %s %s %s have different order. ",
Arrays.toString(value1),
toSignString(compare(value1, value2, keyGenerator.columns)),
Arrays.toString(value2),
Long.toHexString(descriptor1),
toSignString(Long.compare(descriptor1, descriptor2)),
Long.toHexString(descriptor2)),
normalize(Long.compare(descriptor1, descriptor2)),
compare(value1, value2, keyGenerator.columns));
}
private static void assertDescriptorsEqual(long l, long r)
{
Assert.assertEquals(String.format("Expected %d (0x%s), but got %d (0x%s)",
l, Long.toHexString(l), r, Long.toHexString(r)),
l, r);
}
@Test
public void int8GeneratorTest()
{
testInverse(Bijections.INT8_GENERATOR);
}
@Test
public void int16GeneratorTest()
{
testInverse(Bijections.INT16_GENERATOR);
}
@Test
public void int32GeneratorTest()
{
testInverse(Bijections.INT32_GENERATOR);
}
@Test
public void int64GeneratorTest()
{
testInverse(Bijections.INT64_GENERATOR);
testOrderPreserving(Bijections.INT64_GENERATOR);
testInverse(new Bijections.ReverseBijection(Bijections.INT64_GENERATOR));
testOrderPreserving(new Bijections.ReverseBijection(Bijections.INT64_GENERATOR), true);
}
@Test
public void booleanGenTest()
{
testInverse(Bijections.BOOLEAN_GENERATOR);
testOrderPreserving(Bijections.BOOLEAN_GENERATOR);
}
@Test
public void floatGeneratorTest()
{
testInverse(Bijections.FLOAT_GENERATOR);
testOrderPreserving(Bijections.FLOAT_GENERATOR, Float::compareTo);
}
@Test
public void doubleGeneratorTest()
{
testInverse(Bijections.DOUBLE_GENERATOR);
testOrderPreserving(Bijections.DOUBLE_GENERATOR);
}
@Test
public void stringGenTest()
{
testInverse(new StringBijection());
testOrderPreserving(new StringBijection());
}
public static <T> void testInverse(Bijections.Bijection<T> gen)
{
test(gen,
(v) -> Assert.assertEquals(gen.adjustEntropyDomain(v.descriptor), gen.deflate(v.value)));
}
public static <T extends Comparable> void testOrderPreserving(Bijections.Bijection<T> gen)
{
testOrderPreserving(gen, false);
}
public static <T extends Comparable> void testOrderPreserving(Bijections.Bijection<T> gen, boolean reverse)
{
test(gen, gen,
(v1, v2) -> {
long v1Descriptor = gen.adjustEntropyDomain(v1.descriptor);
long v2Descriptor = gen.adjustEntropyDomain(v2.descriptor);
Assert.assertEquals(String.format("%s (%s) and %s (%s) sort wrong",
v1.value,
Long.toHexString(v1Descriptor),
v2.value,
Long.toHexString(v2Descriptor)),
normalize(Long.compare(v1Descriptor, v2Descriptor)) * (reverse ? -1 : 1),
normalize(v1.value.compareTo(v2.value)));
});
}
public static <T extends Comparable> void testOrderPreserving(Bijections.Bijection<T> gen, Comparator<T> comparator)
{
test(gen, gen,
(v1, v2) -> Assert.assertEquals(normalize(Long.compare(gen.adjustEntropyDomain(v1.descriptor),
gen.adjustEntropyDomain(v2.descriptor))),
normalize(comparator.compare(v1.value, v2.value))));
}
public static <T1> void test(Bijections.Bijection<T1> gen1,
Consumer<Generator.Value<T1>> validate)
{
for (int i = 0; i < RUNS; i++)
{
long descriptor1 = rand.next();
validate.accept(new Generator.Value<T1>(descriptor1, gen1.inflate(gen1.adjustEntropyDomain(descriptor1))));
}
}
public static <T1, T2> void test(Bijections.Bijection<T1> gen1,
Bijections.Bijection<T2> gen2,
BiConsumer<Generator.Value<T1>, Generator.Value<T2>> validate)
{
for (int i = 0; i < RUNS; i++)
{
long descriptor1 = rand.next();
long descriptor2 = rand.next();
validate.accept(new Generator.Value<T1>(descriptor1, gen1.inflate(gen1.adjustEntropyDomain(descriptor1))),
new Generator.Value<T2>(descriptor2, gen2.inflate(gen1.adjustEntropyDomain(descriptor2))));
}
}
public static int[] sizes(int... ints)
{
return ints;
}
public static String toSignString(int l)
{
if (l == 0)
return "=";
else if (l > 0)
return ">";
return "<";
}
public static int normalize(int l)
{
if (l == 0)
return 0;
if (l > 0)
return 1;
else
return -1;
}
static int compare(Object[] a, Object[] b, List<ColumnSpec<?>> spec)
{
assert a.length == b.length;
for (int i = 0; i < a.length; i++)
{
Comparable comparableA = (Comparable) a[i];
Comparable comparableB = (Comparable) b[i];
int cmp = comparableA.compareTo(comparableB);
if (cmp != 0)
{
if (spec.get(i).isReversed())
cmp *= -1;
return cmp < 0 ? -1 : 1;
}
}
return 0;
}
public static <T> Iterator<T[]> permutations(int size, Class<T> klass, T... values)
{
int[] cursors = new int[size];
return new Iterator<T[]>()
{
int left = 0;
T[] next = fromCursors();
public boolean hasNext()
{
for (int i = cursors.length - 1; i >= 0; i--)
{
if (cursors[i] < values.length - 1)
return true;
}
return false;
}
public T[] computeNext()
{
cursors[left]++;
for (int i = 0; i < cursors.length; i++)
{
if (cursors[i] == values.length)
{
cursors[i] = 0;
cursors[i + 1]++;
}
}
return fromCursors();
}
public T[] next()
{
if (next == null)
next = computeNext();
T[] ret = next;
next = null;
return ret;
}
public T[] fromCursors()
{
T[] res = (T[]) Array.newInstance(klass, cursors.length);
for (int i = 0; i < cursors.length; i++)
res[i] = values[cursors[i]];
return res;
}
};
}
}

View File

@ -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);
}
}

View File

@ -16,28 +16,29 @@
* limitations under the License.
*/
package org.apache.cassandra.distributed.fuzz;
package org.apache.cassandra.fuzz.harry.gen;
import com.fasterxml.jackson.annotation.JsonCreator;
import com.fasterxml.jackson.annotation.JsonTypeName;
import harry.core.Configuration;
import harry.ddl.SchemaSpec;
import harry.model.sut.SystemUnderTest;
import java.util.function.Supplier;
@JsonTypeName("fixed")
public class FixedSchemaProviderConfiguration implements Configuration.SchemaProviderConfiguration
import org.junit.Assert;
import org.junit.Test;
import org.apache.cassandra.harry.gen.EntropySource;
import org.apache.cassandra.harry.gen.Generators;
public class ExtensionsTest
{
private final SchemaSpec schemaSpec;
@JsonCreator
public FixedSchemaProviderConfiguration(SchemaSpec schemaSpec)
@Test
public void testPick()
{
this.schemaSpec = schemaSpec;
}
Supplier<Integer> gen = Generators.pick(101, 102, 103, 104, 105).bind(EntropySource.forTests());
@Override
public SchemaSpec make(long l, SystemUnderTest systemUnderTest)
{
return this.schemaSpec;
int[] counts = new int[5];
for (int i = 0; i < 1000; i++)
counts[gen.get() - 101] += 1;
// It is possible, however very improbable we won't hit each one at least once
for (int count: counts) Assert.assertTrue(count > 0);
}
}

View File

@ -0,0 +1,63 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.apache.cassandra.fuzz.harry.gen;
import java.util.HashMap;
import java.util.Map;
import org.junit.Assert;
import org.junit.Test;
import org.apache.cassandra.harry.gen.EntropySource;
import org.apache.cassandra.harry.gen.Surjections;
import org.apache.cassandra.harry.gen.rng.PcgRSUFast;
public class SurjectionsTest
{
private static int RUNS = 1000000;
@Test
public void weightedTest()
{
int[] weights = new int[] {50, 40, 10};
Surjections.Surjection<String> gen = Surjections.weighted(Surjections.weights(weights),
"a", "b", "c");
Map<String, Integer> frequencies = new HashMap<>();
EntropySource rng = new PcgRSUFast(System.currentTimeMillis(), 0);
for (int i = 0; i < RUNS; i++)
{
String s = gen.inflate(rng.next());
frequencies.compute(s, (s1, i1) -> {
if (i1 == null)
return 1;
else
return i1 + 1;
});
}
Assert.assertEquals(frequencies.get("a") / 10000, weights[0], 1);
Assert.assertEquals(frequencies.get("b") / 10000, weights[1], 1);
Assert.assertEquals(frequencies.get("c") / 10000, weights[2], 1);
}
}

View File

@ -0,0 +1,100 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.apache.cassandra.fuzz.harry.integration;
import org.apache.cassandra.harry.gen.Generator;
import org.apache.cassandra.harry.gen.EntropySource;
import org.apache.cassandra.harry.gen.rng.RngUtils;
import org.quicktheories.core.Gen;
import org.quicktheories.core.RandomnessSource;
import org.quicktheories.impl.Constraint;
public class QuickTheoriesAdapter
{
public static <T> Gen<T> convert(Generator<T> generator)
{
return new Gen<T>()
{
private final RandomnessSourceAdapter<T> adapter = new RandomnessSourceAdapter<>();
public T generate(RandomnessSource randomnessSource)
{
return adapter.generate(randomnessSource, generator);
}
};
}
public static class RandomnessSourceAdapter<T> implements EntropySource
{
private RandomnessSource rnd;
public long next()
{
return rnd.next(Constraint.none());
}
public void seed(long seed)
{
throw new RuntimeException("Seed is not settable");
}
public EntropySource derive()
{
return new RandomnessSourceAdapter<>();
}
public int nextInt()
{
return RngUtils.asInt(next());
}
public int nextInt(int max)
{
return RngUtils.asInt(next(), max);
}
public int nextInt(int min, int max)
{
return RngUtils.asInt(next(), min, max);
}
public long nextLong(long min, long max)
{
return RngUtils.trim(next(), min, max);
}
public float nextFloat()
{
return RngUtils.asFloat(next());
}
public boolean nextBoolean()
{
return RngUtils.asBoolean(next());
}
public T generate(RandomnessSource rnd, Generator<T> generate)
{
this.rnd = rnd;
T value = generate.generate(this);
this.rnd = null;
return value;
}
}
}

View File

@ -0,0 +1,216 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.apache.cassandra.fuzz.harry.integration.ddl;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.Comparator;
import java.util.Iterator;
import java.util.List;
import org.junit.Assert;
import org.junit.Test;
import org.apache.cassandra.fuzz.harry.integration.QuickTheoriesAdapter;
import org.apache.cassandra.harry.ddl.ColumnSpec;
import org.apache.cassandra.harry.ddl.SchemaGenerators;
import org.apache.cassandra.harry.ddl.SchemaSpec;
import org.apache.cassandra.harry.gen.Generator;
import org.apache.cassandra.harry.operations.CompiledStatement;
import org.apache.cassandra.harry.operations.Query;
import org.apache.cassandra.harry.util.TestRunner;
import org.apache.cassandra.cql3.CQLTester;
import org.apache.cassandra.db.Keyspace;
import org.apache.cassandra.schema.ColumnMetadata;
import org.apache.cassandra.schema.TableMetadata;
import org.apache.cassandra.utils.Pair;
import org.quicktheories.QuickTheory;
import org.quicktheories.core.Gen;
import static org.quicktheories.generators.SourceDSL.integers;
public class SchemaGenTest extends CQLTester
{
private static final int CYCLES = 10;
// TODO: compact storage tests
@Test
public void testSelectForwardAndReverseIteration() throws Throwable
{
Generator<SchemaSpec> gen = new SchemaGenerators.Builder(KEYSPACE).partitionKeyColumnCount(1, 4)
.clusteringColumnCount(1, 10)
.regularColumnCount(0, 10)
.staticColumnCount(0, 10)
.generator();
TestRunner.test(gen,
schemaDefinition -> {
String tableDef = schemaDefinition.compile().cql();
createTable(tableDef);
try
{
CompiledStatement statement = Query.selectPartition(schemaDefinition, 1, false).toSelectStatement();
execute(statement.cql(), statement.bindings());
statement = Query.selectPartition(schemaDefinition, 1, true).toSelectStatement();
execute(statement.cql(), statement.bindings());
}
catch (Throwable t)
{
throw new AssertionError("Exception caught", t);
}
});
}
@Test
public void createTableRoundTrip() throws Throwable
{
Generator<SchemaSpec> gen = new SchemaGenerators.Builder(KEYSPACE).partitionKeyColumnCount(1, 10)
.clusteringColumnCount(1, 10)
.regularColumnCount(0, 10)
.staticColumnCount(0, 10)
.generator();
TestRunner.test(gen,
schemaDefinition -> {
String tableDef = schemaDefinition.compile().cql();
createTable(KEYSPACE, tableDef);
TableMetadata tableMetadata = Keyspace.open(KEYSPACE).getColumnFamilyStore(schemaDefinition.table).metadata.get();
compareColumns(schemaDefinition.partitionKeys, tableMetadata.partitionKeyColumns());
compareColumns(schemaDefinition.clusteringKeys, tableMetadata.clusteringColumns());
compareColumns(schemaDefinition.regularColumns, tableMetadata.regularColumns());
compareColumns(schemaDefinition.staticColumns, tableMetadata.staticColumns());
});
}
@Test
public void testReverseComparator()
{
SchemaSpec spec = new SchemaSpec(KEYSPACE, "tbl1",
Arrays.asList(ColumnSpec.pk("pk1", ColumnSpec.asciiType),
ColumnSpec.pk("pk2", ColumnSpec.int64Type)),
Arrays.asList(ColumnSpec.ck("ck1", ColumnSpec.asciiType, true),
ColumnSpec.ck("ck2", ColumnSpec.int64Type, false)),
Arrays.asList(ColumnSpec.regularColumn("v1", ColumnSpec.asciiType),
ColumnSpec.regularColumn("v2", ColumnSpec.asciiType),
ColumnSpec.regularColumn("v3", ColumnSpec.int64Type),
ColumnSpec.regularColumn("v4", ColumnSpec.int64Type)),
Arrays.asList(ColumnSpec.staticColumn("static1", ColumnSpec.asciiType),
ColumnSpec.staticColumn("static2", ColumnSpec.int64Type)));
String tableDef = spec.compile().cql();
createTable(KEYSPACE, tableDef);
TableMetadata tableMetadata = Keyspace.open(KEYSPACE).getColumnFamilyStore(spec.table).metadata.get();
compareColumns(spec.partitionKeys, tableMetadata.partitionKeyColumns());
compareColumns(spec.clusteringKeys, tableMetadata.clusteringColumns());
compareColumns(spec.regularColumns, tableMetadata.regularColumns());
compareColumns(spec.staticColumns, tableMetadata.staticColumns());
}
@Test
public void testSchemaGeneration()
{
Gen<Pair<Integer, Integer>> ckCounts = integers().between(0, 4).zip(integers().between(0, 6), Pair::create);
Gen<Pair<Integer, Integer>> regCounts = integers().between(0, 4).zip(integers().between(0, 6), Pair::create);
// Gen<Pair<Integer, Integer>> staticCounts = integers().between(0, 4).zip(integers().between(0, 6), Pair::create);
Gen<Pair<Integer, Integer>> pkCounts = integers().between(1, 4).zip(integers().between(0, 6), Pair::create);
Gen<SchemaGenerationInputs> inputs = pkCounts.zip(ckCounts, regCounts,
(pks, cks, regs) ->
new SchemaGenerationInputs(pks.left, pks.left + pks.right,
cks.left, cks.left + cks.right,
regs.left, regs.left + regs.right));
Gen<Pair<SchemaGenerationInputs, SchemaSpec>> schemaAndInputs = inputs.flatMap(input -> {
Generator<SchemaSpec> gen = new SchemaGenerators.Builder("test")
.partitionKeyColumnCount(input.minPk, input.maxPk)
.clusteringColumnCount(input.minCks, input.maxCks)
.regularColumnCount(input.minRegs, input.maxRegs)
.generator();
return QuickTheoriesAdapter.convert(gen).map(schema -> Pair.create(input, schema));
});
qt().forAll(schemaAndInputs)
.check(schemaAndInput -> {
SchemaGenerationInputs input = schemaAndInput.left;
SchemaSpec schema = schemaAndInput.right;
return schema.partitionKeys.size() <= input.maxPk && schema.partitionKeys.size() >= input.minPk &&
schema.clusteringKeys.size() <= input.maxCks && schema.clusteringKeys.size() >= input.minCks &&
schema.regularColumns.size() <= input.maxRegs && schema.regularColumns.size() >= input.minRegs;
});
}
private static class SchemaGenerationInputs {
private final int minPk;
private final int maxPk;
private final int minCks;
private final int maxCks;
private final int minRegs;
private final int maxRegs;
public SchemaGenerationInputs(int minPk, int maxPk, int minCks, int maxCks, int minRegs, int maxRegs)
{
this.minPk = minPk;
this.maxPk = maxPk;
this.minCks = minCks;
this.maxCks = maxCks;
this.minRegs = minRegs;
this.maxRegs = maxRegs;
}
}
private static boolean compareColumns(Collection<ColumnSpec<?>> expectedColl, Collection<ColumnMetadata> actualColl)
{
List<ColumnSpec<?>> expectedSorted = new ArrayList<>(expectedColl);
expectedSorted.sort(Comparator.comparing(Object::toString));
List<ColumnMetadata> actualSorted = new ArrayList<>(actualColl);
actualSorted.sort(Comparator.comparing(Object::toString));
Iterator<ColumnSpec<?>> expectedIter = expectedSorted.iterator();
Iterator<ColumnMetadata> actualIter = actualSorted.iterator();
while (expectedIter.hasNext() && actualIter.hasNext())
{
ColumnSpec expected = expectedIter.next();
ColumnMetadata actual = actualIter.next();
Assert.assertEquals(expected.kind.toString(), actual.kind.toString());
Assert.assertEquals(expected.name, actual.name.toString());
Assert.assertEquals(expected.type.toString(), actual.type.asCQL3Type().toString());
}
Assert.assertEquals(String.format("Collections %s and %s have different sizes", expectedColl, actualColl),
expectedIter.hasNext(), actualIter.hasNext());
return true;
}
public static QuickTheory qt()
{
return QuickTheory.qt()
.withExamples(CYCLES)
.withShrinkCycles(0);
}
}

View File

@ -0,0 +1,148 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.apache.cassandra.fuzz.harry.integration.dsl;
import java.util.Random;
import java.util.function.LongSupplier;
import java.util.function.Supplier;
import org.junit.Test;
import org.apache.cassandra.fuzz.harry.integration.model.ModelTestBase;
import org.apache.cassandra.harry.checker.ModelChecker;
import org.apache.cassandra.harry.core.Configuration;
import org.apache.cassandra.harry.core.Run;
import org.apache.cassandra.harry.ddl.SchemaGenerators;
import org.apache.cassandra.harry.ddl.SchemaSpec;
import org.apache.cassandra.harry.dsl.BatchVisitBuilder;
import org.apache.cassandra.harry.dsl.HistoryBuilder;
import org.apache.cassandra.harry.dsl.SingleOperationBuilder;
import org.apache.cassandra.harry.dsl.ValueDescriptorIndexGenerator;
import org.apache.cassandra.harry.gen.rng.JdkRandomEntropySource;
import org.apache.cassandra.harry.model.Model;
import org.apache.cassandra.harry.operations.Query;
import org.apache.cassandra.harry.sut.SystemUnderTest;
import org.apache.cassandra.harry.sut.TokenPlacementModel;
import org.apache.cassandra.harry.visitors.ReplayingVisitor;
public class HistoryBuilderIntegrationTest extends ModelTestBase
{
private final long seed = 1L;
private final int STEPS_PER_ITERATION = 1_000;
private final int MAX_PARTITIONS = 100;
@Override
protected Configuration.ModelConfiguration modelConfiguration()
{
return new Configuration.QuiescentCheckerConfig();
}
public Configuration.ConfigurationBuilder configuration(long seed, SchemaSpec schema)
{
return super.configuration(seed, schema)
.setPartitionDescriptorSelector(new Configuration.DefaultPDSelectorConfiguration(2, 2))
.setClusteringDescriptorSelector((builder) -> builder.setOperationsPerLtsDistribution(new Configuration.ConstantDistributionConfig(100_000)));
}
@Test
public void simpleDSLTest() throws Throwable
{
Supplier<SchemaSpec> supplier = SchemaGenerators.progression(SchemaGenerators.DEFAULT_SWITCH_AFTER);
for (int i = 0; i < SchemaGenerators.DEFAULT_RUNS; i++)
{
SchemaSpec schema = supplier.get();
Configuration config = configuration(i, schema).build();
Run run = config.createRun();
beforeEach();
run.sut.schemaChange(schema.compile().cql());
ModelChecker<SingleOperationBuilder> modelChecker = new ModelChecker<>();
JdkRandomEntropySource entropySource = new JdkRandomEntropySource(new Random(seed));
LongSupplier[] valueGenerators = new LongSupplier[run.schemaSpec.regularColumns.size()];
for (int j = 0; j < valueGenerators.length; j++)
{
valueGenerators[j] = new ValueDescriptorIndexGenerator(run.schemaSpec.regularColumns.get(j),
run.rng)
.toSupplier(entropySource.derive(), 20, 0.2f);
}
TokenPlacementModel.ReplicationFactor rf = new TokenPlacementModel.SimpleReplicationFactor(1);
int maxPartitionSize = 100;
modelChecker.init(new HistoryBuilder(seed, maxPartitionSize, 10, schema, rf))
.step((history) -> {
return history.insert();
})
.step((history) -> {
return history.insert(entropySource.nextInt(maxPartitionSize));
})
.step((history) -> {
int row = entropySource.nextInt(maxPartitionSize);
long[] vds = new long[valueGenerators.length];
for (int j = 0; j < valueGenerators.length; j++)
vds[j] = valueGenerators[j].getAsLong();
return history.insert(row, vds);
})
.step((history) -> {
return history.deleteRow();
})
.step((history) -> {
return history.deleteRow(entropySource.nextInt(maxPartitionSize));
})
.step(SingleOperationBuilder::deletePartition)
.step(SingleOperationBuilder::deleteColumns)
.step(SingleOperationBuilder::deleteRowSlice)
.step((history) -> {
return history.deleteRowRange();
})
.step((history) -> {
return history.deleteRowRange(entropySource.nextInt(maxPartitionSize),
entropySource.nextInt(maxPartitionSize),
entropySource.nextBoolean(),
entropySource.nextBoolean());
})
.step((history) -> history instanceof HistoryBuilder,
(history) -> ((HistoryBuilder) history).beginBatch())
.step((history) -> (history instanceof BatchVisitBuilder) && ((BatchVisitBuilder) history).size() > 1,
(history) -> ((BatchVisitBuilder) history).endBatch())
.exitCondition((history) -> {
if (!(history instanceof HistoryBuilder))
return false;
HistoryBuilder historyBuilder = (HistoryBuilder) history;
ReplayingVisitor visitor = historyBuilder.visitor(run.tracker, run.sut, SystemUnderTest.ConsistencyLevel.ALL);
visitor.replayAll();
if (historyBuilder.presetSelector.pds().size() < MAX_PARTITIONS)
return false;
Model model = historyBuilder.quiescentChecker(run.tracker, sut);
for (Long pd : historyBuilder.presetSelector.pds())
model.validate(Query.selectPartition(run.schemaSpec, pd,false));
return true;
})
.run(STEPS_PER_ITERATION, seed);
}
}
}

View File

@ -0,0 +1,179 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.apache.cassandra.fuzz.harry.integration.generators;
import java.util.Random;
import java.util.concurrent.CompletableFuture;
import com.google.common.collect.Iterators;
import org.junit.Test;
import org.apache.cassandra.harry.HarryHelper;
import org.apache.cassandra.harry.core.Configuration;
import org.apache.cassandra.harry.core.Run;
import org.apache.cassandra.harry.ddl.ColumnSpec;
import org.apache.cassandra.harry.ddl.SchemaGenerators;
import org.apache.cassandra.harry.ddl.SchemaSpec;
import org.apache.cassandra.harry.gen.Generator;
import org.apache.cassandra.harry.gen.Surjections;
import org.apache.cassandra.harry.gen.distribution.Distribution;
import org.apache.cassandra.harry.model.NoOpChecker;
import org.apache.cassandra.harry.model.OpSelectors;
import org.apache.cassandra.harry.sut.SystemUnderTest;
import org.apache.cassandra.harry.visitors.MutatingVisitor;
import org.apache.cassandra.harry.visitors.MutatingRowVisitor;
import org.apache.cassandra.harry.visitors.SingleValidator;
import org.apache.cassandra.harry.util.TestRunner;
import org.apache.cassandra.harry.visitors.Visitor;
import org.apache.cassandra.cql3.CQLTester;
import org.apache.cassandra.cql3.UntypedResultSet;
import org.apache.cassandra.distributed.impl.RowUtil;
public class DataGeneratorsIntegrationTest extends CQLTester
{
@Test
public void testTimestampTieResolution()
{
Random rng = new Random(1);
String ks = "test_timestamp_tie_resolution";
createKeyspace(String.format("CREATE KEYSPACE %s WITH replication = {'class': 'SimpleStrategy', 'replication_factor': 1}", ks));
int counter = 0;
for (ColumnSpec.DataType<?> dataType : new ColumnSpec.DataType[]{ ColumnSpec.int8Type,
ColumnSpec.int16Type,
ColumnSpec.int32Type,
ColumnSpec.int64Type,
ColumnSpec.asciiType,
ColumnSpec.floatType,
ColumnSpec.doubleType })
{
String tbl = "table_" + (counter++);
createTable(String.format("CREATE TABLE %s.%s (pk int PRIMARY KEY, v %s)",
ks, tbl,
dataType));
for (int i = 0; i < 10_000; i++)
{
long d1 = dataType.generator().adjustEntropyDomain(rng.nextLong());
long d2 = dataType.generator().adjustEntropyDomain(rng.nextLong());
for (long d : new long[]{ d1, d2 })
{
execute(String.format("INSERT INTO %s.%s (pk, v) VALUES (?,?) USING TIMESTAMP 1", ks, tbl),
i, dataType.generator().inflate(d));
}
if (dataType.compareLexicographically(d1, d2) > 0)
assertRows(execute(String.format("SELECT v FROM %s.%s WHERE pk=?", ks, tbl), i),
row(dataType.generator().inflate(d1)));
else
assertRows(execute(String.format("SELECT v FROM %s.%s WHERE pk=?", ks, tbl), i),
row(dataType.generator().inflate(d2)));
}
}
}
@Test
public void queryParseabilityTest() throws Throwable
{
Generator<SchemaSpec> gen = new SchemaGenerators.Builder(KEYSPACE).partitionKeyColumnCount(2, 4)
.clusteringColumnCount(1, 4)
.regularColumnCount(1, 4)
.staticColumnCount(1, 4)
.generator();
TestRunner.test(gen,
(schema) -> {
try
{
schema.validate();
}
catch (AssertionError e)
{
return;
}
createTable(schema.compile().cql());
Configuration.ConfigurationBuilder builder = HarryHelper.defaultConfiguration()
.setDataTracker(new Configuration.NoOpDataTrackerConfiguration())
.setSchemaProvider(new Configuration.FixedSchemaProviderConfiguration(schema))
.setSUT(CqlTesterSut::new);
for (OpSelectors.OperationKind kind : OpSelectors.OperationKind.values())
{
Run run = builder
.setClusteringDescriptorSelector((rng, schema_) -> {
return new OpSelectors.DefaultDescriptorSelector(rng,
OpSelectors.columnSelectorBuilder().forAll(schema_).build(),
OpSelectors.OperationSelector.weighted(Surjections.weights(100), kind),
new Distribution.ConstantDistribution(2),
100);
})
.build()
.createRun();
Visitor visitor = new MutatingVisitor(run, MutatingRowVisitor::new);
for (int lts = 0; lts < 100; lts++)
visitor.visit();
SingleValidator validator = new SingleValidator(100, run, NoOpChecker::new);
for (int lts = 0; lts < 10; lts++)
validator.visit(lts);
}
});
}
public class CqlTesterSut implements SystemUnderTest
{
public boolean isShutdown()
{
return false;
}
public void shutdown()
{
}
public void schemaChange(String statement)
{
createTable(statement);
}
public Object[][] execute(String statement, ConsistencyLevel cl, Object... bindings)
{
try
{
UntypedResultSet res = DataGeneratorsIntegrationTest.this.execute(statement, bindings);
if (res == null)
return new Object[][]{};
return Iterators.toArray(RowUtil.toIter(res), Object[].class);
}
catch (Throwable throwable)
{
throw new RuntimeException(throwable);
}
}
public CompletableFuture<Object[][]> executeAsync(String statement, ConsistencyLevel cl, Object... bindings)
{
return CompletableFuture.completedFuture(execute(statement, cl, bindings));
}
}
}

View File

@ -0,0 +1,71 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.apache.cassandra.fuzz.harry.integration.model;
import java.util.concurrent.TimeUnit;
import java.util.function.Supplier;
import org.junit.Test;
import org.apache.cassandra.harry.core.Configuration;
import org.apache.cassandra.harry.ddl.SchemaGenerators;
import org.apache.cassandra.harry.ddl.SchemaSpec;
import org.apache.cassandra.harry.tracker.LockingDataTracker;
import org.apache.cassandra.harry.runner.Runner;
import org.apache.cassandra.harry.visitors.MutatingVisitor;
import org.apache.cassandra.harry.visitors.QueryLogger;
import org.apache.cassandra.harry.visitors.RandomPartitionValidator;
import static org.apache.cassandra.harry.core.Configuration.VisitorPoolConfiguration.pool;
import static java.util.Arrays.asList;
public class ConcurrentQuiescentCheckerIntegrationTest extends ModelTestBase
{
@Test
public void testConcurrentReadWriteWorkload() throws Throwable
{
Supplier<SchemaSpec> supplier = SchemaGenerators.progression(1);
int writeThreads = 2;
int readThreads = 2;
for (int i = 0; i < SchemaGenerators.GENERATORS_COUNT; i++)
{
SchemaSpec schema = supplier.get();
Configuration config = configuration(i, schema)
.setKeyspaceDdl("CREATE KEYSPACE IF NOT EXISTS " + schema.keyspace + " WITH replication = {'class': 'SimpleStrategy', 'replication_factor': 1};")
.setCreateSchema(true)
.setDropSchema(true)
.setDataTracker(LockingDataTracker::new)
.build();
Runner.concurrent(config,
asList(pool("Writer", writeThreads, MutatingVisitor::new),
pool("Reader", readThreads, (run) -> new RandomPartitionValidator(run, modelConfiguration(), QueryLogger.NO_OP))),
2, TimeUnit.MINUTES)
.run();
}
}
@Override
protected Configuration.ModelConfiguration modelConfiguration()
{
return new Configuration.QuiescentCheckerConfig();
}
}

View File

@ -0,0 +1,98 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.apache.cassandra.fuzz.harry.integration.model;
import java.util.concurrent.TimeUnit;
import java.util.function.Supplier;
import org.junit.Before;
import org.junit.BeforeClass;
import org.junit.Test;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.apache.cassandra.harry.core.Configuration;
import org.apache.cassandra.harry.core.Run;
import org.apache.cassandra.harry.ddl.SchemaGenerators;
import org.apache.cassandra.harry.ddl.SchemaSpec;
import org.apache.cassandra.harry.model.QuiescentChecker;
import org.apache.cassandra.harry.model.RepairingLocalStateValidator;
import org.apache.cassandra.harry.sut.SystemUnderTest;
import org.apache.cassandra.harry.sut.TokenPlacementModel;
import org.apache.cassandra.harry.sut.injvm.InJVMTokenAwareVisitExecutor;
import org.apache.cassandra.harry.sut.injvm.InJvmSut;
import org.apache.cassandra.harry.sut.injvm.InJvmSutBase;
import org.apache.cassandra.harry.runner.Runner;
import org.apache.cassandra.harry.runner.UpToLtsRunner;
import org.apache.cassandra.harry.visitors.MutatingVisitor;
import org.apache.cassandra.distributed.Cluster;
import org.apache.cassandra.distributed.api.Feature;
public class InJVMTokenAwareExecutorTest extends IntegrationTestBase
{
private static final Logger logger = LoggerFactory.getLogger(InJVMTokenAwareExecutorTest.class);
@BeforeClass
public static void before() throws Throwable
{
cluster = Cluster.build()
.withNodes(5)
.withConfig((cfg) -> InJvmSutBase.defaultConfig().accept(cfg.with(Feature.GOSSIP, Feature.NETWORK)))
.createWithoutStarting();
cluster.setUncaughtExceptionsFilter(t -> {
logger.error("Caught exception, reporting during shutdown. Ignoring.", t);
return true;
});
cluster.startup();
cluster = init(cluster);
sut = new InJvmSut(cluster);
}
@Override
@Before
public void beforeEach()
{
cluster.schemaChange("DROP KEYSPACE IF EXISTS harry");
cluster.schemaChange("CREATE KEYSPACE harry WITH replication = {'class': 'SimpleStrategy', 'replication_factor': 3};");
}
@Test
public void testRepair() throws Throwable
{
Supplier<SchemaSpec> schemaGen = SchemaGenerators.progression(1);
for (int cnt = 0; cnt < SchemaGenerators.DEFAULT_RUNS; cnt++)
{
SchemaSpec schema = schemaGen.get();
Configuration.ConfigurationBuilder builder = sharedConfiguration(cnt, schema);
Configuration configuration = builder.build();
Run run = configuration.createRun();
run.sut.schemaChange(run.schemaSpec.compile().cql());
Runner.chain(configuration,
UpToLtsRunner.factory(MutatingVisitor.factory(InJVMTokenAwareVisitExecutor.factory(new Configuration.MutatingRowVisitorConfiguration(),
SystemUnderTest.ConsistencyLevel.NODE_LOCAL,
new TokenPlacementModel.SimpleReplicationFactor(3))),
10_000, 2, TimeUnit.SECONDS),
Runner.single(RepairingLocalStateValidator.factoryForTests(5, QuiescentChecker::new)))
.run();
}
}
}

View File

@ -0,0 +1,116 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.apache.cassandra.fuzz.harry.integration.model;
import java.util.function.Supplier;
import org.junit.AfterClass;
import org.junit.Before;
import org.junit.BeforeClass;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.apache.cassandra.harry.core.Configuration;
import org.apache.cassandra.harry.ddl.SchemaGenerators;
import org.apache.cassandra.harry.ddl.SchemaSpec;
import org.apache.cassandra.harry.clock.OffsetClock;
import org.apache.cassandra.harry.model.OpSelectors;
import org.apache.cassandra.harry.sut.injvm.InJvmSut;
import org.apache.cassandra.harry.sut.injvm.InJvmSutBase;
import org.apache.cassandra.distributed.Cluster;
import org.apache.cassandra.distributed.test.TestBaseImpl;
public class IntegrationTestBase extends TestBaseImpl
{
protected static final Logger logger = LoggerFactory.getLogger(IntegrationTestBase.class);
protected static Cluster cluster;
protected static InJvmSut sut;
@BeforeClass
public static void before() throws Throwable
{
cluster = Cluster.build()
.withNodes(1)
.withConfig(InJvmSutBase.defaultConfig())
.createWithoutStarting();
cluster.setUncaughtExceptionsFilter(t -> {
logger.error("Caught exception, reporting during shutdown. Ignoring.", t);
return true;
});
cluster.startup();
cluster = init(cluster);
sut = new InJvmSut(cluster);
}
@AfterClass
public static void afterClass()
{
sut.shutdown();
}
@Before
public void beforeEach()
{
cluster.schemaChange("DROP KEYSPACE IF EXISTS harry");
cluster.schemaChange("CREATE KEYSPACE harry WITH replication = {'class': 'SimpleStrategy', 'replication_factor': 1};");
}
private static long seed = 0;
public static Supplier<Configuration.ConfigurationBuilder> sharedConfiguration()
{
Supplier<SchemaSpec> specGenerator = SchemaGenerators.progression(SchemaGenerators.DEFAULT_SWITCH_AFTER);
return () -> {
SchemaSpec schemaSpec = specGenerator.get();
return sharedConfiguration(seed, schemaSpec);
};
}
public static Configuration.CDSelectorConfigurationBuilder sharedCDSelectorConfiguration()
{
return new Configuration.CDSelectorConfigurationBuilder()
.setOperationsPerLtsDistribution(new Configuration.ConstantDistributionConfig(2))
.setMaxPartitionSize(100)
.setOperationKindWeights(new Configuration.OperationKindSelectorBuilder()
.addWeight(OpSelectors.OperationKind.DELETE_ROW, 1)
.addWeight(OpSelectors.OperationKind.DELETE_COLUMN, 1)
.addWeight(OpSelectors.OperationKind.DELETE_RANGE, 1)
.addWeight(OpSelectors.OperationKind.DELETE_SLICE, 1)
.addWeight(OpSelectors.OperationKind.DELETE_PARTITION, 1)
.addWeight(OpSelectors.OperationKind.DELETE_COLUMN_WITH_STATICS, 5)
.addWeight(OpSelectors.OperationKind.INSERT_WITH_STATICS, 20)
.addWeight(OpSelectors.OperationKind.INSERT, 20)
.addWeight(OpSelectors.OperationKind.UPDATE_WITH_STATICS, 25)
.addWeight(OpSelectors.OperationKind.UPDATE, 25)
.build());
}
public static Configuration.ConfigurationBuilder sharedConfiguration(long seed, SchemaSpec schema)
{
return new Configuration.ConfigurationBuilder().setSeed(seed)
.setClock(() -> new OffsetClock(100000))
.setCreateSchema(true)
.setTruncateTable(false)
.setDropSchema(true)
.setSchemaProvider((seed1, sut) -> schema)
.setClusteringDescriptorSelector(sharedCDSelectorConfiguration().build())
.setPartitionDescriptorSelector(new Configuration.DefaultPDSelectorConfiguration(2, 200))
.setSUT(() -> sut);
}
}

View File

@ -0,0 +1,117 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.apache.cassandra.fuzz.harry.integration.model;
import java.util.Arrays;
import org.apache.cassandra.harry.ddl.ColumnSpec;
import org.apache.cassandra.harry.ddl.SchemaGenerators;
import org.apache.cassandra.harry.ddl.SchemaSpec;
import org.apache.cassandra.harry.gen.Surjections;
import org.apache.cassandra.harry.util.BitSet;
public class MockSchema
{
public static final String KEYSPACE = "harry";
public static final Surjections.Surjection<BitSet> columnMaskSelector1;
public static final Surjections.Surjection<BitSet> columnMaskSelector2;
public static final Surjections.Surjection<BitSet> compactColumnMaskSelector;
public static final SchemaSpec tbl1;
public static final SchemaSpec tbl2;
public static final SchemaSpec compact_schema;
static
{
columnMaskSelector1 = Surjections.pick(BitSet.create(0b0000111, 7),
BitSet.create(0b1110000, 7),
BitSet.create(0b1111111, 7));
columnMaskSelector2 = Surjections.pick(BitSet.create(0b11, 2),
BitSet.create(0b01, 2),
BitSet.create(0b10, 2));
compactColumnMaskSelector = Surjections.pick(BitSet.create(1, 1));
tbl1 = new SchemaSpec(KEYSPACE, "tbl1",
Arrays.asList(ColumnSpec.pk("pk1", ColumnSpec.asciiType),
ColumnSpec.pk("pk2", ColumnSpec.int64Type)),
Arrays.asList(ColumnSpec.ck("ck1", ColumnSpec.asciiType, false),
ColumnSpec.ck("ck2", ColumnSpec.int64Type, false)),
Arrays.asList(ColumnSpec.regularColumn("v1", ColumnSpec.int32Type),
ColumnSpec.regularColumn("v2", ColumnSpec.int64Type),
// TODO: test boolean values; before that - submit a PR to Cassandra where we add boolean to ByteBufferUtil#objectToBytes
ColumnSpec.regularColumn("v3", ColumnSpec.int32Type),
ColumnSpec.regularColumn("v4", ColumnSpec.asciiType),
ColumnSpec.regularColumn("v5", ColumnSpec.int64Type),
ColumnSpec.regularColumn("v6", ColumnSpec.floatType),
ColumnSpec.regularColumn("v7", ColumnSpec.doubleType)),
Arrays.asList(ColumnSpec.staticColumn("static1", ColumnSpec.asciiType),
ColumnSpec.staticColumn("static2", ColumnSpec.int64Type)));
tbl2 = new SchemaSpec(KEYSPACE, "tbl2",
Arrays.asList(ColumnSpec.pk("pk1", ColumnSpec.asciiType),
ColumnSpec.pk("pk2", ColumnSpec.int64Type)),
Arrays.asList(ColumnSpec.ck("ck1", ColumnSpec.asciiType, true)),
Arrays.asList(ColumnSpec.regularColumn("v1", ColumnSpec.int32Type),
ColumnSpec.regularColumn("v2", ColumnSpec.asciiType)),
Arrays.asList(ColumnSpec.regularColumn("static1", ColumnSpec.int32Type),
ColumnSpec.regularColumn("static2", ColumnSpec.asciiType)));
compact_schema = new SchemaSpec(KEYSPACE, "tbl3",
Arrays.asList(ColumnSpec.pk("pk1", ColumnSpec.asciiType),
ColumnSpec.pk("pk2", ColumnSpec.int64Type)),
Arrays.asList(ColumnSpec.ck("ck1", ColumnSpec.asciiType, true),
ColumnSpec.ck("ck2", ColumnSpec.int64Type, false)),
Arrays.asList(ColumnSpec.regularColumn("v1", ColumnSpec.asciiType)),
Arrays.asList(ColumnSpec.staticColumn("static1", ColumnSpec.asciiType)))
.withCompactStorage();
}
public static SchemaSpec randomSchema(String keyspace, String table, long seed)
{
return new SchemaGenerators.Builder(keyspace, () -> table)
.partitionKeySpec(1, 4,
// ColumnSpec.int8Type,
// ColumnSpec.int16Type,
// ColumnSpec.int32Type,
ColumnSpec.int64Type,
ColumnSpec.floatType,
ColumnSpec.doubleType,
ColumnSpec.asciiType(4, 10))
.clusteringKeySpec(1, 4,
// ColumnSpec.int8Type,
// ColumnSpec.int16Type,
// ColumnSpec.int32Type,
ColumnSpec.int64Type,
ColumnSpec.floatType,
ColumnSpec.doubleType,
ColumnSpec.asciiType(4, 10))
.regularColumnSpec(2, 10,
ColumnSpec.int8Type,
ColumnSpec.int16Type,
ColumnSpec.int32Type,
ColumnSpec.int64Type,
ColumnSpec.floatType,
ColumnSpec.doubleType,
ColumnSpec.asciiType(5, 10))
.surjection()
.inflate(seed);
}
}

View File

@ -0,0 +1,138 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.apache.cassandra.fuzz.harry.integration.model;
import java.util.Arrays;
import java.util.Collections;
import java.util.concurrent.TimeUnit;
import java.util.function.BiConsumer;
import java.util.function.Function;
import java.util.function.Supplier;
import org.apache.cassandra.harry.core.Configuration;
import org.apache.cassandra.harry.core.Run;
import org.apache.cassandra.harry.ddl.SchemaGenerators;
import org.apache.cassandra.harry.ddl.SchemaSpec;
import org.apache.cassandra.harry.runner.Runner;
import org.apache.cassandra.harry.runner.UpToLtsRunner;
import org.apache.cassandra.harry.visitors.MutatingRowVisitor;
import org.apache.cassandra.harry.visitors.MutatingVisitor;
import org.apache.cassandra.harry.visitors.SingleValidator;
public abstract class ModelTestBase extends IntegrationTestBase
{
private final int ITERATIONS = 20_000;
void negativeTest(Function<Run, Boolean> corrupt, BiConsumer<Throwable, Run> validate) throws Throwable
{
Supplier<SchemaSpec> supplier = SchemaGenerators.progression(SchemaGenerators.DEFAULT_SWITCH_AFTER);
for (int i = 0; i < SchemaGenerators.DEFAULT_RUNS; i++)
{
SchemaSpec schema = supplier.get();
negativeTest(corrupt, validate, i, schema);
}
}
void negativeIntegrationTest(Configuration.RunnerConfiguration runnerConfig) throws Throwable
{
Supplier<SchemaSpec> supplier = SchemaGenerators.progression(1);
for (int i = 0; i < SchemaGenerators.DEFAULT_RUNS; i++)
{
SchemaSpec schema = supplier.get();
Configuration.ConfigurationBuilder builder = configuration(i, schema);
builder.setClock(new Configuration.ApproximateClockConfiguration((int) TimeUnit.MINUTES.toMillis(10),
1, TimeUnit.SECONDS))
.setCreateSchema(false)
.setDropSchema(false)
.setRunner(runnerConfig);
Configuration config = builder.build();
Runner runner = config.createRunner();
Run run = runner.getRun();
beforeEach();
run.sut.schemaChange(run.schemaSpec.compile().cql());
runner.run();
}
}
protected abstract Configuration.ModelConfiguration modelConfiguration();
protected SingleValidator validator(Run run)
{
return new SingleValidator(100, run , modelConfiguration());
}
public Configuration.ConfigurationBuilder configuration(long seed, SchemaSpec schema)
{
return sharedConfiguration(seed, schema);
}
void negativeTest(Function<Run, Boolean> corrupt, BiConsumer<Throwable, Run> validate, int counter, SchemaSpec schemaSpec) throws Throwable
{
Configuration config = configuration(counter, schemaSpec)
.setCreateSchema(true)
.setKeyspaceDdl(String.format("CREATE KEYSPACE IF NOT EXISTS %s WITH replication = {'class': 'SimpleStrategy', 'replication_factor': %d};",
schemaSpec.keyspace, cluster.size()))
.setDropSchema(true)
.build();
Run run = config.createRun();
new Runner.ChainRunner(run, config,
Arrays.asList(writer(ITERATIONS, 2, TimeUnit.MINUTES),
(r, c) -> new Runner.SingleVisitRunner(r, c, Collections.singletonList(this::validator)) {
@Override
public void runInternal()
{
if (!corrupt.apply(run))
{
System.out.println("Could not corrupt");
return;
}
try
{
super.runInternal();
throw new ShouldHaveThrownException();
}
catch (Throwable t)
{
validate.accept(t, run);
}
}
})).run();
}
public static Configuration.RunnerConfiguration writer(long iterations, int runtime, TimeUnit timeUnit)
{
return (run, config) -> {
return new UpToLtsRunner(run, config,
Collections.singletonList((r_) -> new MutatingVisitor(r_, MutatingRowVisitor::new)),
iterations,
runtime, timeUnit);
};
}
public static class ShouldHaveThrownException extends AssertionError
{
}
}

View File

@ -0,0 +1,157 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.apache.cassandra.fuzz.harry.integration.model;
import java.util.Arrays;
import java.util.Collection;
import java.util.HashMap;
import java.util.Map;
import java.util.Random;
import java.util.function.Supplier;
import org.junit.Assert;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.junit.runners.Parameterized;
import org.apache.cassandra.harry.core.Configuration;
import org.apache.cassandra.harry.core.Run;
import org.apache.cassandra.harry.corruptor.AddExtraRowCorruptor;
import org.apache.cassandra.harry.corruptor.ChangeValueCorruptor;
import org.apache.cassandra.harry.corruptor.HideRowCorruptor;
import org.apache.cassandra.harry.corruptor.HideValueCorruptor;
import org.apache.cassandra.harry.corruptor.QueryResponseCorruptor;
import org.apache.cassandra.harry.corruptor.ShowValueCorruptor;
import org.apache.cassandra.harry.ddl.SchemaGenerators;
import org.apache.cassandra.harry.model.Model;
import org.apache.cassandra.harry.model.QuiescentChecker;
import org.apache.cassandra.harry.visitors.MutatingVisitor;
import org.apache.cassandra.harry.visitors.MutatingRowVisitor;
import org.apache.cassandra.harry.operations.Query;
import org.apache.cassandra.harry.operations.QueryGenerator;
import org.apache.cassandra.harry.visitors.Visitor;
import static org.apache.cassandra.harry.corruptor.QueryResponseCorruptor.SimpleQueryResponseCorruptor;
@RunWith(Parameterized.class)
public class QuerySelectorNegativeTest extends IntegrationTestBase
{
private final int CYCLES = 1000;
private final Random rnd = new Random();
private final QueryResponseCorruptorFactory corruptorFactory;
public QuerySelectorNegativeTest(QueryResponseCorruptorFactory corruptorFactory)
{
this.corruptorFactory = corruptorFactory;
}
@Parameterized.Parameters
public static Collection<QueryResponseCorruptorFactory> source()
{
return Arrays.asList((run) -> new SimpleQueryResponseCorruptor(run.schemaSpec,
run.clock,
ChangeValueCorruptor::new),
(run) -> new SimpleQueryResponseCorruptor(run.schemaSpec,
run.clock,
HideValueCorruptor::new),
(run) -> new SimpleQueryResponseCorruptor(run.schemaSpec,
run.clock,
ShowValueCorruptor::new),
(run) -> new SimpleQueryResponseCorruptor(run.schemaSpec,
run.clock,
HideRowCorruptor::new),
(run) -> new AddExtraRowCorruptor(run.schemaSpec,
run.clock,
run.tracker,
run.descriptorSelector)
);
}
interface QueryResponseCorruptorFactory
{
QueryResponseCorruptor create(Run run);
}
@Test
public void selectRows()
{
Map<Query.QueryKind, Integer> stats = new HashMap<>();
Supplier<Configuration.ConfigurationBuilder> gen = sharedConfiguration();
int rounds = SchemaGenerators.DEFAULT_RUNS;
int failureCounter = 0;
outer:
for (int counter = 0; counter < rounds; counter++)
{
beforeEach();
Configuration config = gen.get()
.setClusteringDescriptorSelector(sharedCDSelectorConfiguration()
.setOperationsPerLtsDistribution(new Configuration.ConstantDistributionConfig(2))
.setMaxPartitionSize(2000)
.build())
.build();
Run run = config.createRun();
run.sut.schemaChange(run.schemaSpec.compile().cql());
System.out.println(run.schemaSpec.compile().cql());
Visitor visitor = new MutatingVisitor(run, MutatingRowVisitor::new);
Model model = new QuiescentChecker(run);
QueryResponseCorruptor corruptor = this.corruptorFactory.create(run);
for (int i = 0; i < CYCLES; i++)
visitor.visit();
while (true)
{
long verificationLts = rnd.nextInt(1000);
QueryGenerator queryGen = new QueryGenerator(run.schemaSpec,
run.pdSelector,
run.descriptorSelector,
run.rng);
QueryGenerator.TypedQueryGenerator querySelector = new QueryGenerator.TypedQueryGenerator(run.rng, queryGen);
Query query = querySelector.inflate(verificationLts, counter);
model.validate(query);
if (!corruptor.maybeCorrupt(query, run.sut))
continue;
try
{
model.validate(query);
Assert.fail("Should've failed");
}
catch (Throwable t)
{
// expected
failureCounter++;
stats.compute(query.queryKind, (Query.QueryKind kind, Integer cnt) -> cnt == null ? 1 : (cnt + 1));
continue outer;
}
}
}
Assert.assertTrue(String.format("Seen only %d failures", failureCounter),
failureCounter > (rounds * 0.8));
}
}

View File

@ -0,0 +1,163 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.apache.cassandra.fuzz.harry.integration.model;
import java.util.HashSet;
import java.util.Set;
import java.util.function.Supplier;
import org.junit.Assert;
import org.junit.Test;
import org.apache.cassandra.harry.core.Configuration;
import org.apache.cassandra.harry.core.Run;
import org.apache.cassandra.harry.ddl.SchemaGenerators;
import org.apache.cassandra.harry.ddl.SchemaSpec;
import org.apache.cassandra.harry.model.Model;
import org.apache.cassandra.harry.model.QuiescentChecker;
import org.apache.cassandra.harry.model.SelectHelper;
import org.apache.cassandra.harry.sut.SystemUnderTest;
import org.apache.cassandra.harry.operations.CompiledStatement;
import org.apache.cassandra.harry.visitors.MutatingVisitor;
import org.apache.cassandra.harry.visitors.MutatingRowVisitor;
import org.apache.cassandra.harry.operations.Query;
import org.apache.cassandra.harry.operations.QueryGenerator;
import org.apache.cassandra.harry.visitors.Visitor;
import static org.apache.cassandra.harry.gen.DataGenerators.NIL_DESCR;
public class QuerySelectorTest extends IntegrationTestBase
{
private static int CYCLES = 300;
@Test
public void basicQuerySelectorTest()
{
Supplier<SchemaSpec> schemaGen = SchemaGenerators.progression(SchemaGenerators.DEFAULT_SWITCH_AFTER);
for (int cnt = 0; cnt < SchemaGenerators.DEFAULT_RUNS; cnt++)
{
beforeEach();
SchemaSpec schemaSpec = schemaGen.get();
int partitionSize = 200;
int[] fractions = new int[schemaSpec.clusteringKeys.size()];
int last = partitionSize;
for (int i = fractions.length - 1; i >= 0; i--)
{
fractions[i] = last;
last = last / 2;
}
Configuration config = sharedConfiguration(cnt, schemaSpec)
.setClusteringDescriptorSelector(sharedCDSelectorConfiguration()
.setMaxPartitionSize(partitionSize)
.setFractions(fractions)
.build())
.build();
Run run = config.createRun();
run.sut.schemaChange(run.schemaSpec.compile().cql());
Visitor visitor = new MutatingVisitor(run, MutatingRowVisitor::new);
for (int i = 0; i < CYCLES; i++)
visitor.visit();
QueryGenerator.TypedQueryGenerator querySelector = new QueryGenerator.TypedQueryGenerator(run);
for (int i = 0; i < CYCLES; i++)
{
Query query = querySelector.inflate(i, i);
Object[][] results = run.sut.execute(query.toSelectStatement(), SystemUnderTest.ConsistencyLevel.QUORUM);
Set<Long> matchingClusterings = new HashSet<>();
for (Object[] row : results)
{
long cd = SelectHelper.resultSetToRow(run.schemaSpec,
run.clock,
row).cd;
matchingClusterings.add(cd);
}
// the simplest test there can be: every row that is in the partition and was returned by the query,
// has to "match", every other row has to be a non-match
CompiledStatement selectPartition = SelectHelper.select(run.schemaSpec, run.pdSelector.pd(i, schemaSpec));
Object[][] partition = run.sut.execute(selectPartition, SystemUnderTest.ConsistencyLevel.QUORUM);
for (Object[] row : partition)
{
long cd = SelectHelper.resultSetToRow(run.schemaSpec,
run.clock,
row).cd;
// Skip static clustering
if (cd == NIL_DESCR)
continue;
boolean expected = matchingClusterings.contains(cd);
boolean actual = query.matchCd(cd);
Assert.assertEquals(String.format("Mismatch for clustering: %d. Expected: %s. Actual: %s.\nQuery: %s",
cd, expected, actual, query.toSelectStatement()),
expected,
actual);
}
}
}
}
@Test
public void querySelectorModelTest()
{
Supplier<SchemaSpec> gen = SchemaGenerators.progression(SchemaGenerators.DEFAULT_SWITCH_AFTER);
for (int cnt = 0; cnt < SchemaGenerators.DEFAULT_RUNS; cnt++)
{
SchemaSpec schemaSpec = gen.get();
int[] fractions = new int[schemaSpec.clusteringKeys.size()];
int partitionSize = 200;
int last = partitionSize;
for (int i = fractions.length - 1; i >= 0; i--)
{
fractions[i] = last;
last = last / 2;
}
Configuration config = sharedConfiguration(cnt, schemaSpec)
.setClusteringDescriptorSelector(sharedCDSelectorConfiguration()
.setMaxPartitionSize(partitionSize)
.setFractions(fractions)
.build())
.build();
Run run = config.createRun();
run.sut.schemaChange(run.schemaSpec.compile().cql());
Visitor visitor = new MutatingVisitor(run, MutatingRowVisitor::new);
for (int i = 0; i < CYCLES; i++)
visitor.visit();
QueryGenerator.TypedQueryGenerator querySelector = new QueryGenerator.TypedQueryGenerator(run);
Model model = new QuiescentChecker(run);
long verificationLts = 10;
for (int i = 0; i < CYCLES; i++)
{
Query query = querySelector.inflate(verificationLts, i);
model.validate(query);
}
}
}
}

View File

@ -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());
}
}

View File

@ -0,0 +1,79 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.apache.cassandra.fuzz.harry.integration.model;
import java.util.concurrent.TimeUnit;
import java.util.function.Supplier;
import org.junit.BeforeClass;
import org.junit.Test;
import org.apache.cassandra.harry.core.Configuration;
import org.apache.cassandra.harry.ddl.SchemaGenerators;
import org.apache.cassandra.harry.ddl.SchemaSpec;
import org.apache.cassandra.harry.sut.TokenPlacementModel;
import org.apache.cassandra.harry.sut.injvm.InJvmSut;
import org.apache.cassandra.harry.sut.injvm.InJvmSutBase;
import org.apache.cassandra.harry.sut.injvm.QuiescentLocalStateChecker;
import org.apache.cassandra.harry.runner.Runner;
import org.apache.cassandra.harry.visitors.AllPartitionsValidator;
import org.apache.cassandra.harry.visitors.MutatingVisitor;
import org.apache.cassandra.distributed.Cluster;
import org.apache.cassandra.distributed.api.Feature;
public class QuiescentLocalStateCheckerIntegrationTest extends ModelTestBase
{
@BeforeClass
public static void before() throws Throwable
{
cluster = init(Cluster.build()
.withNodes(2)
.withConfig(InJvmSutBase.defaultConfig().andThen((cfg) -> cfg.with(Feature.GOSSIP)))
.start());
sut = new InJvmSut(cluster);
}
@Test
public void testQuiescentLocalStateChecker() throws Throwable
{
Supplier<SchemaSpec> supplier = SchemaGenerators.progression(1);
for (int i = 0; i < SchemaGenerators.GENERATORS_COUNT; i++)
{
SchemaSpec schema = supplier.get();
Configuration config = configuration(i, schema)
.setKeyspaceDdl("CREATE KEYSPACE IF NOT EXISTS " + schema.keyspace + " WITH replication = {'class': 'SimpleStrategy', 'replication_factor': 1};")
.setCreateSchema(true)
.setDropSchema(true)
.build();
Runner.chain(config,
Runner.sequential(MutatingVisitor::new, 2, TimeUnit.SECONDS),
Runner.single(AllPartitionsValidator.factoryForTests(1, QuiescentLocalStateChecker.factory(new TokenPlacementModel.SimpleReplicationFactor(1)))))
.run();
break;
}
}
@Override
protected Configuration.ModelConfiguration modelConfiguration()
{
return new Configuration.QuiescentCheckerConfig();
}
}

View File

@ -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);
}
}

View File

@ -0,0 +1,341 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.apache.cassandra.fuzz.harry.integration.model.reconciler;
import java.util.*;
import org.junit.Test;
import org.apache.cassandra.harry.core.Configuration;
import org.apache.cassandra.harry.core.Run;
import org.apache.cassandra.harry.ddl.ColumnSpec;
import org.apache.cassandra.harry.ddl.SchemaGenerators;
import org.apache.cassandra.harry.ddl.SchemaSpec;
import org.apache.cassandra.harry.gen.Surjections;
import org.apache.cassandra.harry.model.OpSelectors;
import org.apache.cassandra.harry.model.QuiescentChecker;
import org.apache.cassandra.harry.model.SelectHelper;
import org.apache.cassandra.harry.model.reconciler.PartitionState;
import org.apache.cassandra.harry.sut.injvm.InJvmSut;
import org.apache.cassandra.harry.operations.CompiledStatement;
import org.apache.cassandra.harry.operations.Query;
import org.apache.cassandra.harry.operations.WriteHelper;
import org.apache.cassandra.harry.util.BitSet;
import org.apache.cassandra.distributed.api.ConsistencyLevel;
import org.apache.cassandra.fuzz.harry.integration.model.IntegrationTestBase;
public class SimpleReconcilerTest extends IntegrationTestBase
{
public static Surjections.Surjection<SchemaSpec> defaultSchemaSpecGen(String ks, String table)
{
return new SchemaGenerators.Builder(ks, () -> table)
.partitionKeySpec(1, 3,
ColumnSpec.int64Type,
ColumnSpec.asciiType(5, 256))
.clusteringKeySpec(1, 3,
ColumnSpec.int64Type,
ColumnSpec.asciiType(2, 3),
ColumnSpec.ReversedType.getInstance(ColumnSpec.int64Type),
ColumnSpec.ReversedType.getInstance(ColumnSpec.asciiType(2, 3)))
.regularColumnSpec(50, 50,
ColumnSpec.int64Type,
ColumnSpec.asciiType(5, 256))
.staticColumnSpec(50, 50,
ColumnSpec.int64Type,
ColumnSpec.asciiType(4, 256))
.surjection();
}
@Test
public void testStatics() throws Throwable
{
int rowsPerPartition = 50;
SchemaSpec schema = defaultSchemaSpecGen("harry", "tbl").inflate(1);
Configuration config = sharedConfiguration(1, schema).build();
Run run = config.createRun();
SyntheticTest test = new SyntheticTest(run.rng, schema);
beforeEach();
cluster.schemaChange(schema.compile().cql());
ModelState state = new ModelState(new HashMap<>());
InJvmSut sut = (InJvmSut) run.sut;
Random rng = new Random(1);
int partitionIdx = 0;
for (int i = 0; i < 100; i++)
{
BitSet subset = BitSet.allUnset(schema.allColumns.size());
for (int j = 0; j < subset.size(); j++)
{
if (rng.nextBoolean())
subset.set(j);
}
if (!isValidSubset(schema.allColumns, subset))
continue;
int pdIdx = partitionIdx++;
long pd = test.pd(pdIdx);
for (int j = 0; j < 10; j++)
{
int cdIdx = rng.nextInt(rowsPerPartition);
long cd = test.cd(pdIdx, cdIdx);
long[] vds = run.descriptorSelector.descriptors(pd, cd, state.lts, 0, schema.regularColumns,
schema.regularColumnsMask(),
subset,
schema.regularColumnsOffset);
long[] sds = run.descriptorSelector.descriptors(pd, cd, state.lts, 0, schema.staticColumns,
schema.staticColumnsMask,
subset,
schema.staticColumnsOffset);
CompiledStatement statement = WriteHelper.inflateUpdate(schema, pd, cd, vds, sds, run.clock.rts(state.lts));
sut.cluster.coordinator(1).execute(statement.cql(), ConsistencyLevel.QUORUM, statement.bindings());
PartitionState partitionState = state.state.get(pd);
if (partitionState == null)
{
partitionState = new PartitionState(pd, -1, schema);
state.state.put(pd, partitionState);
}
partitionState.writeStaticRow(sds, state.lts);
partitionState.write(cd, vds, state.lts, true);
state.lts++;
}
}
// Validate that all partitions correspond to our expectations
for (Long pd : state.state.keySet())
{
ArrayList<Long> clusteringDescriptors = new ArrayList<>(state.state.get(pd).rows().keySet());
// TODO: allow sub-selection
// Try different column subsets
for (int i = 0; i < 10; i++)
{
BitSet bitset = BitSet.allUnset(schema.allColumns.size());
for (int j = 0; j < bitset.size(); j++)
{
if (rng.nextBoolean())
bitset.set(j);
}
Set<ColumnSpec<?>> subset = i == 0 ? null : subset(schema.allColumns, bitset);
if (subset != null && !isValidSubset(schema.allColumns, bitset))
continue;
int a = rng.nextInt(clusteringDescriptors.size());
long cd1tmp = clusteringDescriptors.get(a);
long cd2tmp;
int b;
while (true)
{
b = rng.nextInt(clusteringDescriptors.size());
long tmp = clusteringDescriptors.get(b);
if (tmp != cd1tmp)
{
cd2tmp = tmp;
break;
}
}
long cd1 = Math.min(cd1tmp, cd2tmp);
long cd2 = Math.max(cd1tmp, cd2tmp);
for (boolean reverse : new boolean[]{ true, false })
{
Query query;
query = Query.selectPartition(schema, pd, reverse);
QuiescentChecker.validate(schema,
run.tracker,
subset,
state.state.get(pd),
SelectHelper.execute(sut, run.clock, query, subset),
query);
query = Query.singleClustering(schema, pd, cd1, false);
QuiescentChecker.validate(schema,
run.tracker,
subset,
state.state.get(pd).apply(query),
SelectHelper.execute(sut, run.clock, query, subset),
query);
for (boolean isGt : new boolean[]{ true, false })
{
for (boolean isEquals : new boolean[]{ true, false })
{
try
{
query = Query.clusteringSliceQuery(schema, pd, cd1, rng.nextLong(), isGt, isEquals, reverse);
}
catch (IllegalArgumentException impossibleQuery)
{
continue;
}
QuiescentChecker.validate(schema,
run.tracker,
subset,
state.state.get(pd).apply(query),
SelectHelper.execute(sut, run.clock, query, subset),
query);
}
}
for (boolean isMinEq : new boolean[]{ true, false })
{
for (boolean isMaxEq : new boolean[]{ true, false })
{
try
{
query = Query.clusteringRangeQuery(schema, pd, cd1, cd2, rng.nextLong(), isMinEq, isMaxEq, reverse);
}
catch (IllegalArgumentException impossibleQuery)
{
continue;
}
QuiescentChecker.validate(schema,
run.tracker,
subset,
state.state.get(pd).apply(query),
SelectHelper.execute(sut, run.clock, query, subset),
query);
}
}
}
}
}
}
public static boolean isValidSubset(List<ColumnSpec<?>> superset, BitSet bitSet)
{
boolean hasRegular = false;
for (int i = 0; i < superset.size(); i++)
{
ColumnSpec<?> column = superset.get(i);
if (column.kind == ColumnSpec.Kind.PARTITION_KEY && !bitSet.isSet(i))
return false;
if (column.kind == ColumnSpec.Kind.CLUSTERING && !bitSet.isSet(i))
return false;
if (column.kind == ColumnSpec.Kind.REGULAR && bitSet.isSet(i))
hasRegular = true;
}
return hasRegular;
}
public static Set<ColumnSpec<?>> subset(List<ColumnSpec<?>> superset, BitSet bitSet)
{
Set<ColumnSpec<?>> subset = new HashSet<>();
for (int i = 0; i < superset.size(); i++)
{
if (bitSet.isSet(i))
subset.add(superset.get(i));
}
return subset;
}
public static Set<ColumnSpec<?>> randomSubset(List<ColumnSpec<?>> superset, Random e)
{
Set<ColumnSpec<?>> set = new HashSet<>();
boolean hadRegular = false;
for (ColumnSpec<?> v : superset)
{
// TODO: allow selecting without partition and clustering key, too
if (e.nextBoolean() || v.kind == ColumnSpec.Kind.CLUSTERING || v.kind == ColumnSpec.Kind.PARTITION_KEY)
{
set.add(v);
hadRegular |= v.kind == ColumnSpec.Kind.REGULAR;
}
}
// TODO: this is an oversimplification and a workaround for "Invalid restrictions on clustering columns since the UPDATE statement modifies only static columns"
if (!hadRegular)
return randomSubset(superset, e);
return set;
}
public static <T> BitSet subsetToBitset(List<T> superset, Set<T> subset)
{
BitSet bitSet = new BitSet.BitSet64Bit(superset.size());
for (int i = 0; i < superset.size(); i++)
{
if (subset.contains(superset.get(i)))
bitSet.set(i);
}
return bitSet;
}
public static class ModelState
{
public long lts = 0;
public final Map<Long, PartitionState> state;
public ModelState(Map<Long, PartitionState> state)
{
this.state = state;
}
}
public static class SyntheticTest // TODO: horrible name
{
private static long PD_STREAM = System.nanoTime();
private final OpSelectors.PureRng rng;
private final SchemaSpec schema;
public SyntheticTest(OpSelectors.PureRng rng, SchemaSpec schema)
{
this.schema = schema;
this.rng = rng;
}
public long pd(int pdIdx)
{
long pd = this.rng.randomNumber(pdIdx + 1, PD_STREAM);
long adjusted = schema.adjustPdEntropy(pd);
assert adjusted == pd : "Partition descriptors not utilising all entropy bits are not supported.";
return pd;
}
public long pdIdx(long pd)
{
return this.rng.sequenceNumber(pd, PD_STREAM) - 1;
}
public long cd(int pdIdx, int cdIdx)
{
long cd = this.rng.randomNumber(cdIdx + 1, pd(pdIdx));
long adjusted = schema.adjustCdEntropy(cd);
assert adjusted == cd : "Clustering descriptors not utilising all entropy bits are not supported.";
return cd;
}
public long cdIdx(long pd)
{
return this.rng.sequenceNumber(pd, PD_STREAM) - 1;
}
}
}

View File

@ -0,0 +1,90 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.apache.cassandra.fuzz.harry.integration.op;
import java.util.function.Supplier;
import org.junit.Before;
import org.junit.Test;
import org.apache.cassandra.harry.core.MetricReporter;
import org.apache.cassandra.harry.core.Run;
import org.apache.cassandra.harry.ddl.SchemaGenerators;
import org.apache.cassandra.harry.ddl.SchemaSpec;
import org.apache.cassandra.harry.gen.EntropySource;
import org.apache.cassandra.harry.gen.distribution.Distribution;
import org.apache.cassandra.harry.model.OpSelectors;
import org.apache.cassandra.harry.clock.OffsetClock;
import org.apache.cassandra.harry.sut.SystemUnderTest;
import org.apache.cassandra.harry.tracker.DataTracker;
import org.apache.cassandra.harry.visitors.GeneratingVisitor;
import org.apache.cassandra.harry.visitors.MutatingRowVisitor;
import org.apache.cassandra.harry.visitors.MutatingVisitor;
import org.apache.cassandra.cql3.CQLTester;
import static org.apache.cassandra.harry.model.OpSelectors.DefaultDescriptorSelector.DEFAULT_OP_SELECTOR;
public class RowVisitorTest extends CQLTester
{
@Before
public void beforeTest() throws Throwable {
super.beforeTest();
schemaChange(String.format("CREATE KEYSPACE IF NOT EXISTS %s WITH replication = {'class': 'SimpleStrategy', 'replication_factor': '1'}", SchemaGenerators.DEFAULT_KEYSPACE_NAME));
}
@Test
public void rowWriteGeneratorTest()
{
Supplier<SchemaSpec> specGenerator = SchemaGenerators.progression(SchemaGenerators.DEFAULT_SWITCH_AFTER);
EntropySource rand = EntropySource.forTests(6371747244598697093L);
OpSelectors.PureRng rng = new OpSelectors.PCGFast(1);
OpSelectors.PdSelector pdSelector = new OpSelectors.DefaultPdSelector(rng, 10, 10);
for (int i = 0; i < SchemaGenerators.DEFAULT_RUNS; i++)
{
SchemaSpec schema = specGenerator.get();
createTable(schema.compile().cql());
OpSelectors.DescriptorSelector descriptorSelector = new OpSelectors.DefaultDescriptorSelector(rng,
new OpSelectors.ColumnSelectorBuilder().forAll(schema)
.build(),
DEFAULT_OP_SELECTOR,
new Distribution.ScaledDistribution(1, 30),
100);
Run run = new Run(rng,
new OffsetClock(10000),
pdSelector,
descriptorSelector,
schema,
DataTracker.NO_OP,
SystemUnderTest.NO_OP,
MetricReporter.NO_OP);
long[] descriptors = new long[4];
for (int j = 0; j < descriptors.length; j++)
descriptors[j] = rand.next();
// some improvement wont hurt here
new GeneratingVisitor(run, new MutatingVisitor(run, MutatingRowVisitor::new)).visit();
}
}
}

View File

@ -0,0 +1,180 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.apache.cassandra.fuzz.harry.model;
import java.util.Iterator;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicReference;
import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.LockSupport;
import java.util.concurrent.locks.ReentrantLock;
import org.junit.Assert;
import org.junit.Test;
import org.apache.cassandra.harry.clock.ApproximateClock;
public class ApproximateClockTest
{
@Test
public void approximateClockTest() throws InterruptedException
{
ConcurrentHashMap<Long, Long> m = new ConcurrentHashMap<>();
ConcurrentHashMap<Long, Long> inverse = new ConcurrentHashMap<>();
TimeUnit timeUnit = TimeUnit.MILLISECONDS;
int duration = 1000;
int concurrency = 5;
long maxTicks = timeUnit.toMicros(duration) / (4 * concurrency);
ApproximateClock clock = new ApproximateClock(duration, timeUnit);
ScheduledExecutorService scheduledExecutor = Executors.newScheduledThreadPool(1);
ExecutorService executor = Executors.newFixedThreadPool(concurrency);
final Lock lock = new ReentrantLock();
AtomicReference<Throwable> throwable = new AtomicReference();
final Condition signalError = lock.newCondition();
lock.lock();
for (int i = 0; i < concurrency; i++)
{
executor.submit(() -> {
try
{
int sleepCnt = 0;
while (!executor.isShutdown() && !Thread.currentThread().isInterrupted())
{
sleepCnt++;
if (sleepCnt >= maxTicks)
{
LockSupport.parkNanos(timeUnit.toNanos(duration));
sleepCnt = 0;
}
if (executor.isShutdown() || Thread.currentThread().isInterrupted())
return;
long lts = clock.nextLts();
// Make sure to test "history" path
if (lts % 10000 == 0)
{
scheduledExecutor.schedule(() -> {
try
{
long rts = clock.rts(lts);
Assert.assertNull(m.put(lts, rts));
Assert.assertNull(inverse.put(rts, lts));
}
catch (Throwable t)
{
throwable.set(t);
signalError.signalAll();
t.printStackTrace();
}
}, 2 * duration, timeUnit);
continue;
}
try
{
long rts = clock.rts(lts);
Assert.assertNull(m.put(lts, rts));
Assert.assertNull(inverse.put(rts, lts));
}
catch (Throwable t)
{
throwable.set(t);
signalError.signalAll();
}
}
}
catch (Throwable t)
{
throwable.set(t);
signalError.signalAll();
t.printStackTrace();
}
});
}
signalError.await(10, TimeUnit.SECONDS);
lock.unlock();
executor.shutdown();
Assert.assertTrue(executor.awaitTermination(30, TimeUnit.SECONDS));
scheduledExecutor.shutdown();
Assert.assertTrue(scheduledExecutor.awaitTermination(10, TimeUnit.SECONDS));
Throwable t = throwable.get();
if (t != null)
throw new AssertionError("Caught an exception while executing", t);
Assert.assertEquals(m.size(), inverse.size());
Iterator<Map.Entry<Long, Long>> iter = m.entrySet().iterator();
Map.Entry<Long, Long> previous = iter.next();
while (iter.hasNext())
{
if (previous == null)
{
previous = iter.next();
continue;
}
Map.Entry<Long, Long> current = iter.next();
long lts = current.getKey();
long rts = current.getValue();
Assert.assertEquals(String.format("%s and %s sort wrong", previous, current),
Long.compare(previous.getKey(), current.getKey()),
Long.compare(previous.getValue(), current.getValue()));
Assert.assertEquals(clock.rts(lts), rts);
Assert.assertEquals(clock.lts(rts), lts);
previous = current;
}
}
@Test
public void approximateClockInvertibilityTest()
{
ConcurrentHashMap<Long, Long> m = new ConcurrentHashMap<>();
TimeUnit timeUnit = TimeUnit.MILLISECONDS;
int duration = 100;
int cycles = 10_000;
ApproximateClock clock = new ApproximateClock(duration, timeUnit);
for (long i = 0; i < cycles; i++)
{
long lts = clock.nextLts();
Assert.assertEquals(lts, i);
long rts = clock.rts(lts);
Assert.assertNull(m.put(lts, rts));
}
for (Map.Entry<Long, Long> entry : m.entrySet())
{
Assert.assertEquals(entry.getKey(),
Long.valueOf(clock.lts(entry.getValue())));
}
}
}

View File

@ -0,0 +1,418 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.apache.cassandra.fuzz.harry.model;
import java.util.*;
import java.util.function.BiConsumer;
import java.util.function.Supplier;
import org.junit.Assert;
import org.junit.Test;
import org.apache.cassandra.harry.core.MetricReporter;
import org.apache.cassandra.harry.core.Run;
import org.apache.cassandra.harry.ddl.ColumnSpec;
import org.apache.cassandra.harry.ddl.SchemaGenerators;
import org.apache.cassandra.harry.ddl.SchemaSpec;
import org.apache.cassandra.harry.gen.Surjections;
import org.apache.cassandra.harry.gen.distribution.Distribution;
import org.apache.cassandra.harry.clock.OffsetClock;
import org.apache.cassandra.harry.model.OpSelectors;
import org.apache.cassandra.harry.sut.SystemUnderTest;
import org.apache.cassandra.harry.operations.CompiledStatement;
import org.apache.cassandra.harry.tracker.DataTracker;
import org.apache.cassandra.harry.visitors.LtsVisitor;
import org.apache.cassandra.harry.visitors.MutatingVisitor;
import org.apache.cassandra.harry.visitors.OperationExecutor;
import org.apache.cassandra.harry.util.BitSet;
import org.apache.cassandra.harry.visitors.VisitExecutor;
public class OpSelectorsTest
{
private static int RUNS = 1000;
private static SchemaSpec SCHEMA = new SchemaSpec("ks", "tbl",
Collections.singletonList(ColumnSpec.pk("pk", ColumnSpec.int64Type)),
Collections.singletonList(ColumnSpec.pk("ck", ColumnSpec.int64Type)),
Collections.emptyList(), Collections.emptyList());
@Test
public void testRowDataDescriptorSupplier()
{
OpSelectors.PureRng rng = new OpSelectors.PCGFast(1);
SchemaSpec schema = new SchemaSpec("ks", "tbl1",
Arrays.asList(ColumnSpec.pk("pk1", ColumnSpec.asciiType),
ColumnSpec.pk("pk2", ColumnSpec.int64Type)),
Arrays.asList(ColumnSpec.ck("ck1", ColumnSpec.asciiType, false),
ColumnSpec.ck("ck2", ColumnSpec.int64Type, false)),
Arrays.asList(ColumnSpec.regularColumn("v1", ColumnSpec.int32Type),
ColumnSpec.regularColumn("v2", ColumnSpec.int64Type)),
Arrays.asList(ColumnSpec.staticColumn("static1", ColumnSpec.asciiType),
ColumnSpec.staticColumn("static2", ColumnSpec.int64Type)));
OpSelectors.DefaultDescriptorSelector descriptorSelector = new OpSelectors.DefaultDescriptorSelector(rng,
new OpSelectors.ColumnSelectorBuilder().forAll(schema)
.build(),
OpSelectors.DefaultDescriptorSelector.DEFAULT_OP_SELECTOR,
new Distribution.ScaledDistribution(2, 10),
50);
OpSelectors.PdSelector pdSupplier = new OpSelectors.DefaultPdSelector(rng,
100,
100);
for (int lts = 0; lts < RUNS; lts++)
{
long pd = pdSupplier.pd(lts, schema);
int opsPerLts = descriptorSelector.operationsPerLts(lts);
for (int opId = 0; opId < opsPerLts; opId++)
{
long cd = descriptorSelector.cd(pd, lts, opId, schema);
Assert.assertEquals(opId, descriptorSelector.opId(pd, lts, cd));
Assert.assertTrue(descriptorSelector.isCdVisitedBy(pd, lts, cd));
for (int col = 0; col < 10; col++)
{
long vd = descriptorSelector.vd(pd, cd, lts, opId, col);
}
}
}
}
@Test
public void pdSelectorSymmetryTest()
{
OpSelectors.PureRng rng = new OpSelectors.PCGFast(1);
Supplier<SchemaSpec> gen = SchemaGenerators.progression(SchemaGenerators.DEFAULT_SWITCH_AFTER);
SchemaSpec schema = gen.get();
for (long[] positions : new long[][]{ { 0, Long.MAX_VALUE }, { 100, Long.MAX_VALUE }, { 1000, Long.MAX_VALUE } })
{
for (int repeats = 2; repeats <= 1000; repeats++)
{
for (int windowSize = 2; windowSize <= 10; windowSize++)
{
OpSelectors.DefaultPdSelector pdSelector = new OpSelectors.DefaultPdSelector(rng, windowSize, repeats, positions[0], positions[1]);
Map<Long, List<Long>> m = new HashMap<>();
final long maxLts = 10_000;
for (long lts = 0; lts <= maxLts; lts++)
{
long pd = pdSelector.pd(lts, schema);
m.computeIfAbsent(pd, (k) -> new ArrayList<>()).add(lts);
}
for (Long pd : m.keySet())
{
long currentLts = pdSelector.minLtsFor(pd);
List<Long> predicted = new ArrayList<>();
while (currentLts <= maxLts && currentLts >= 0)
{
predicted.add(currentLts);
currentLts = pdSelector.nextLts(currentLts);
}
Assert.assertEquals(m.get(pd), predicted);
}
}
}
}
}
@Test
public void pdSelectorTest()
{
OpSelectors.PureRng rng = new OpSelectors.PCGFast(1);
int cycles = 10000;
for (long[] positions : new long[][]{ { 0, Long.MAX_VALUE }, { 100, Long.MAX_VALUE }, { 1000, Long.MAX_VALUE } })
{
for (int repeats = 2; repeats <= 1000; repeats++)
{
for (int windowSize = 2; windowSize <= 10; windowSize++)
{
OpSelectors.DefaultPdSelector pdSupplier = new OpSelectors.DefaultPdSelector(rng, windowSize, repeats, positions[0], positions[1]);
long[] pds = new long[cycles];
for (int i = 0; i < cycles; i++)
{
long pd = pdSupplier.pd(i, SCHEMA);
pds[i] = pd;
Assert.assertEquals(pdSupplier.positionFor(i), pdSupplier.positionForPd(pd));
}
Set<Long> noNext = new HashSet<>();
for (int i = 0; i < cycles; i++)
{
long nextLts = pdSupplier.nextLts(i);
Assert.assertFalse(noNext.contains(pds[i]));
if (nextLts == -1)
{
noNext.add(nextLts);
}
else if (nextLts < cycles)
{
Assert.assertEquals(pds[(int) nextLts], pdSupplier.pd(i, SCHEMA));
}
}
Set<Long> noPrev = new HashSet<>();
for (int i = cycles - 1; i >= 0; i--)
{
long prevLts = pdSupplier.prevLts(i);
Assert.assertFalse(noPrev.contains(pds[i]));
if (prevLts == -1)
{
noPrev.add(prevLts);
}
else if (prevLts >= 0)
{
Assert.assertEquals(pds[(int) prevLts], pdSupplier.pd(i, SCHEMA));
}
}
Set<Long> seen = new HashSet<>();
for (int i = 0; i < cycles; i++)
{
long pd = pdSupplier.pd(i, SCHEMA);
if (!seen.contains(pd))
{
Assert.assertEquals(i, pdSupplier.minLtsAt(pdSupplier.positionFor(i)));
seen.add(pd);
}
}
for (int i = 0; i < cycles; i++)
{
long pd = pdSupplier.pd(i, SCHEMA);
long maxLts = pdSupplier.maxLtsFor(pd);
Assert.assertEquals(-1, pdSupplier.nextLts(maxLts));
Assert.assertEquals(pdSupplier.pd(i, SCHEMA), pdSupplier.pd(maxLts, SCHEMA));
}
}
}
}
}
@Test
public void ckSelectorTest()
{
Supplier<SchemaSpec> gen = SchemaGenerators.progression(SchemaGenerators.DEFAULT_SWITCH_AFTER);
for (int i = 0; i < SchemaGenerators.DEFAULT_RUNS; i++)
ckSelectorTest(gen.get());
}
public void ckSelectorTest(SchemaSpec schema)
{
OpSelectors.PureRng rng = new OpSelectors.PCGFast(1);
OpSelectors.PdSelector pdSelector = new OpSelectors.DefaultPdSelector(rng, 10, 10);
OpSelectors.DescriptorSelector ckSelector = new OpSelectors.DefaultDescriptorSelector(rng,
new OpSelectors.ColumnSelectorBuilder().forAll(schema, Surjections.pick(schema.regularColumnsMask())).build(),
OpSelectors.OperationSelector.weighted(Surjections.weights(10, 10, 40, 40),
OpSelectors.OperationKind.DELETE_ROW,
OpSelectors.OperationKind.DELETE_COLUMN,
OpSelectors.OperationKind.INSERT,
OpSelectors.OperationKind.UPDATE),
new Distribution.ConstantDistribution(10),
10);
Map<Long, Set<Long>> partitionMap = new HashMap<>();
CompiledStatement compiledStatement = new CompiledStatement("");
BiConsumer<Long, Long> consumer = (pd, cd) -> {
partitionMap.compute(pd, (pk, list) -> {
if (list == null)
list = new HashSet<>();
list.add(cd);
return list;
});
};
Run run = new Run(rng,
new OffsetClock(0),
pdSelector,
ckSelector,
schema,
DataTracker.NO_OP,
SystemUnderTest.NO_OP,
MetricReporter.NO_OP);
LtsVisitor visitor = new MutatingVisitor(run,
(r) -> new OperationExecutor()
{
public CompiledStatement insert(VisitExecutor.WriteOp op)
{
consumer.accept(op.pd(), op.cd());
return compiledStatement;
}
public CompiledStatement update(VisitExecutor.WriteOp op)
{
consumer.accept(op.pd(), op.cd());
return compiledStatement;
}
public CompiledStatement insertWithStatics(VisitExecutor.WriteStaticOp op)
{
consumer.accept(op.pd(), op.cd());
return compiledStatement;
}
public CompiledStatement updateWithStatics(VisitExecutor.WriteStaticOp op)
{
consumer.accept(op.pd(), op.cd());
return compiledStatement;
}
public CompiledStatement deleteColumn(VisitExecutor.DeleteColumnsOp op)
{
consumer.accept(op.pd(), op.cd());
return compiledStatement;
}
public CompiledStatement deleteColumnWithStatics(VisitExecutor.DeleteColumnsOp op)
{
consumer.accept(op.pd(), op.cd());
return compiledStatement;
}
public CompiledStatement deleteRow(VisitExecutor.DeleteRowOp op)
{
consumer.accept(op.pd(), op.cd());
return compiledStatement;
}
public CompiledStatement deletePartition(VisitExecutor.DeleteOp op)
{
// ignore
return compiledStatement;
}
public CompiledStatement deleteRange(VisitExecutor.DeleteOp op)
{
// ignore
return compiledStatement;
}
public CompiledStatement deleteSlice(VisitExecutor.DeleteOp op)
{
// ignore
return compiledStatement;
}
});
for (int lts = 0; lts < 1000; lts++)
visitor.visit();
for (Collection<Long> value : partitionMap.values())
Assert.assertEquals(10, value.size());
}
@Test
public void hierarchicalDescriptorSelector()
{
SchemaSpec schema = new SchemaSpec("ks", "tbl1",
Collections.singletonList(ColumnSpec.pk("pk1", ColumnSpec.asciiType)),
Arrays.asList(ColumnSpec.ck("ck1", ColumnSpec.asciiType),
ColumnSpec.ck("ck2", ColumnSpec.asciiType),
ColumnSpec.ck("ck3", ColumnSpec.asciiType)),
Collections.singletonList(ColumnSpec.regularColumn("v1", ColumnSpec.asciiType)),
Collections.emptyList());
OpSelectors.PureRng rng = new OpSelectors.PCGFast(1);
OpSelectors.DescriptorSelector ckSelector = new OpSelectors.HierarchicalDescriptorSelector(rng,
new int[] {10, 20},
OpSelectors.columnSelectorBuilder().forAll(schema, Surjections.pick(BitSet.allUnset(0))).build(),
OpSelectors.OperationSelector.weighted(Surjections.weights(10, 10, 40, 40),
OpSelectors.OperationKind.DELETE_ROW,
OpSelectors.OperationKind.DELETE_COLUMN,
OpSelectors.OperationKind.INSERT,
OpSelectors.OperationKind.UPDATE),
new Distribution.ConstantDistribution(10),
100);
Set<Long> ck1 = new TreeSet<>();
Set<Long> ck2 = new TreeSet<>();
Set<Long> ck3 = new TreeSet<>();
for (int i = 0; i < 1000; i++)
{
long[] part = schema.ckGenerator.slice(ckSelector.cd(0, i, 0, schema));
ck1.add(part[0]);
ck2.add(part[1]);
ck3.add(part[2]);
}
Assert.assertEquals(10, ck1.size());
Assert.assertEquals(20, ck2.size());
Assert.assertEquals(100, ck3.size());
}
@Test
public void testWeights()
{
Map<OpSelectors.OperationKind, Integer> config = new EnumMap<>(OpSelectors.OperationKind.class);
config.put(OpSelectors.OperationKind.DELETE_RANGE, 1);
config.put(OpSelectors.OperationKind.DELETE_SLICE, 1);
config.put(OpSelectors.OperationKind.DELETE_ROW, 1);
config.put(OpSelectors.OperationKind.DELETE_COLUMN, 1);
config.put(OpSelectors.OperationKind.DELETE_PARTITION, 1);
config.put(OpSelectors.OperationKind.DELETE_COLUMN_WITH_STATICS, 1);
config.put(OpSelectors.OperationKind.UPDATE, 500);
config.put(OpSelectors.OperationKind.INSERT, 500);
config.put(OpSelectors.OperationKind.UPDATE_WITH_STATICS, 500);
config.put(OpSelectors.OperationKind.INSERT_WITH_STATICS, 500);
int[] weights = new int[config.size()];
for (int i = 0; i < config.values().size(); i++)
weights[i] = config.get(OpSelectors.OperationKind.values()[i]);
OpSelectors.OperationSelector selector = OpSelectors.OperationSelector.weighted(Surjections.weights(weights),
OpSelectors.OperationKind.values());
OpSelectors.PureRng rng = new OpSelectors.PCGFast(1);
OpSelectors.PdSelector pdSelector = new OpSelectors.DefaultPdSelector(rng, 10, 10);
OpSelectors.DescriptorSelector descriptorSelector = new OpSelectors.DefaultDescriptorSelector(rng,
null,
selector,
new Distribution.ConstantDistribution(10),
100);
EnumMap<OpSelectors.OperationKind, Integer> m = new EnumMap<OpSelectors.OperationKind, Integer>(OpSelectors.OperationKind.class);
for (int lts = 0; lts < 1000000; lts++)
{
int total = descriptorSelector.operationsPerLts(lts);
long pd = pdSelector.pd(lts, SCHEMA);
for (int opId = 0; opId < total; opId++)
{
m.compute(descriptorSelector.operationType(pd, lts, opId),
(OpSelectors.OperationKind k, Integer old) -> {
if (old == null) return 1;
else return old + 1;
});
}
}
for (OpSelectors.OperationKind l : OpSelectors.OperationKind.values())
{
for (OpSelectors.OperationKind r : OpSelectors.OperationKind.values())
{
if (l != r)
{
Assert.assertEquals(m.get(l) * 1.0 / m.get(r),
config.get(l) * 1.0 / config.get(r),
(config.get(l) * 1.0 / config.get(r)) * 0.10);
}
}
}
}
}

View File

@ -0,0 +1,204 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.apache.cassandra.fuzz.harry.operations;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.Iterator;
import java.util.List;
import org.junit.Assert;
import org.junit.Test;
import org.apache.cassandra.harry.ddl.ColumnSpec;
import org.apache.cassandra.harry.ddl.SchemaSpec;
import org.apache.cassandra.fuzz.harry.gen.DataGeneratorsTest;
import org.apache.cassandra.harry.model.OpSelectors;
import org.apache.cassandra.harry.operations.Query;
import org.apache.cassandra.harry.operations.QueryGenerator;
import org.apache.cassandra.harry.util.BitSet;
public class RelationTest
{
private static int RUNS = 50;
@Test
public void testKeyGenerators()
{
for (int size = 1; size < 5; size++)
{
Iterator<ColumnSpec.DataType[]> iter = DataGeneratorsTest.permutations(size,
ColumnSpec.DataType.class,
ColumnSpec.int8Type,
ColumnSpec.asciiType,
ColumnSpec.int16Type,
ColumnSpec.int32Type,
ColumnSpec.int64Type,
ColumnSpec.floatType,
ColumnSpec.doubleType
);
while (iter.hasNext())
{
ColumnSpec.DataType[] types = iter.next();
List<ColumnSpec<?>> spec = new ArrayList<>(types.length);
for (int i = 0; i < types.length; i++)
spec.add(ColumnSpec.ck("r" + i, types[i], false));
SchemaSpec schemaSpec = new SchemaSpec("ks",
"tbl",
Collections.singletonList(ColumnSpec.pk("pk", ColumnSpec.int64Type)),
spec,
Collections.emptyList(),
Collections.emptyList());
long[] cds = new long[RUNS];
int[] fractions = new int[schemaSpec.clusteringKeys.size()];
int last = cds.length;
for (int i = fractions.length - 1; i >= 0; i--)
{
fractions[i] = last;
last = last / 2;
}
for (int i = 0; i < cds.length; i++)
{
long cd = OpSelectors.HierarchicalDescriptorSelector.cd(i, fractions, schemaSpec, new OpSelectors.PCGFast(1L), 1L);
cds[i] = schemaSpec.adjustPdEntropy(cd);
}
Arrays.sort(cds);
OpSelectors.PureRng rng = new OpSelectors.PCGFast(1L);
// TODO: replace with mocks?
QueryGenerator querySelector = new QueryGenerator(schemaSpec,
new OpSelectors.PdSelector()
{
protected long pd(long lts)
{
return lts;
}
public long nextLts(long lts)
{
throw new RuntimeException("not implemented");
}
public long prevLts(long lts)
{
throw new RuntimeException("not implemented");
}
public long maxLtsFor(long pd)
{
throw new RuntimeException("not implemented");
}
public long minLtsAt(long position)
{
throw new RuntimeException("not implemented");
}
public long minLtsFor(long pd)
{
throw new RuntimeException("not implemented");
}
public long maxPosition(long maxLts)
{
throw new RuntimeException("not implemented");
}
},
new OpSelectors.DescriptorSelector()
{
public int operationsPerLts(long lts)
{
throw new RuntimeException("not implemented");
}
public int maxPartitionSize()
{
throw new RuntimeException("not implemented");
}
public boolean isCdVisitedBy(long pd, long lts, long cd)
{
throw new RuntimeException("not implemented");
}
protected long cd(long pd, long lts, long opId)
{
throw new RuntimeException("not implemented");
}
public long randomCd(long pd, long entropy)
{
return Math.abs(rng.prev(entropy)) % cds.length;
}
protected long vd(long pd, long cd, long lts, long opId, int col)
{
throw new RuntimeException("not implemented");
}
public OpSelectors.OperationKind operationType(long pd, long lts, long opId)
{
throw new RuntimeException("not implemented");
}
public BitSet columnMask(long pd, long lts, long opId, OpSelectors.OperationKind opType)
{
throw new RuntimeException("not implemented");
}
public long opId(long pd, long lts, long cd)
{
return 0;
}
},
rng);
QueryGenerator.TypedQueryGenerator gen = new QueryGenerator.TypedQueryGenerator(rng, querySelector);
try
{
for (int i = 0; i < RUNS; i++)
{
Query query = gen.inflate(i, 0);
for (int j = 0; j < cds.length; j++)
{
long cd = schemaSpec.ckGenerator.adjustEntropyDomain(cds[i]);
// the only thing we care about here is that query
Assert.assertEquals(String.format("Error caught while running a query %s with cd %d",
query, cd),
Query.simpleMatch(query, cd),
query.matchCd(cd));
}
}
}
catch (Throwable t)
{
throw new AssertionError("Caught error for the type combination " + Arrays.toString(types), t);
}
}
}
}
}

View File

@ -0,0 +1,133 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.apache.cassandra.fuzz.harry.runner;
import java.util.List;
import java.util.concurrent.CopyOnWriteArrayList;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.concurrent.atomic.AtomicReference;
import org.junit.Assert;
import org.junit.Test;
import org.apache.cassandra.concurrent.ExecutorFactory;
import org.apache.cassandra.concurrent.Interruptible;
import org.apache.cassandra.harry.ddl.SchemaGenerators;
import org.apache.cassandra.harry.ddl.SchemaSpec;
import org.apache.cassandra.harry.model.OpSelectors;
import org.apache.cassandra.harry.runner.Runner;
import org.apache.cassandra.harry.tracker.LockingDataTracker;
import org.apache.cassandra.utils.concurrent.WaitQueue;
import static org.apache.cassandra.concurrent.InfiniteLoopExecutor.Daemon.NON_DAEMON;
import static org.apache.cassandra.concurrent.InfiniteLoopExecutor.Interrupts.UNSYNCHRONIZED;
import static org.apache.cassandra.concurrent.InfiniteLoopExecutor.SimulatorSafe.SAFE;
public class LockingDataTrackerTest
{
@Test
public void testDataTracker() throws Throwable
{
SchemaSpec schemaSpec = SchemaGenerators.defaultSchemaSpecGen("test").inflate(1L);
OpSelectors.PureRng rng = new OpSelectors.PCGFast(1L);
OpSelectors.PdSelector pdSelector = new OpSelectors.DefaultPdSelector(rng, 5, 2);
LockingDataTracker tracker = new LockingDataTracker(pdSelector, schemaSpec);
AtomicReference<State> excluded = new AtomicReference<>(State.UNLOCKED);
AtomicInteger readers = new AtomicInteger(0);
AtomicInteger writers = new AtomicInteger(0);
WaitQueue queue = WaitQueue.newWaitQueue();
WaitQueue.Signal interrupt = queue.register();
List<Throwable> errors = new CopyOnWriteArrayList<>();
long lts = 1;
long pd = pdSelector.pd(lts, schemaSpec);
int parallelism = 2;
for (int i = 0; i < parallelism; i++)
{
ExecutorFactory.Global.executorFactory().infiniteLoop("write-" + i, Runner.wrapInterrupt(state -> {
try
{
if (state == Interruptible.State.NORMAL)
{
tracker.beginModification(lts);
Assert.assertEquals(0, readers.get());
writers.incrementAndGet();
excluded.updateAndGet((prev) -> {
assert (prev == State.UNLOCKED || prev == State.LOCKED_FOR_WRITE) : prev;
return State.LOCKED_FOR_WRITE;
});
Assert.assertEquals(0, readers.get());
excluded.updateAndGet((prev) -> {
assert (prev == State.UNLOCKED || prev == State.LOCKED_FOR_WRITE) : prev;
return State.UNLOCKED;
});
Assert.assertEquals(0, readers.get());
writers.decrementAndGet();
tracker.endModification(lts);
}
}
catch (Throwable t)
{
t.printStackTrace();
throw t;
}
}, interrupt::signal, errors::add), SAFE, NON_DAEMON, UNSYNCHRONIZED);
}
for (int i = 0; i < parallelism; i++)
{
ExecutorFactory.Global.executorFactory().infiniteLoop("read-" + i, Runner.wrapInterrupt(state -> {
try
{
if (state == Interruptible.State.NORMAL)
{
tracker.beginValidation(pd);
Assert.assertEquals(0, writers.get());
readers.incrementAndGet();
excluded.updateAndGet((prev) -> {
assert (prev == State.UNLOCKED || prev == State.LOCKED_FOR_READ) : prev;
return State.LOCKED_FOR_READ;
});
Assert.assertEquals(0, writers.get());
excluded.updateAndGet((prev) -> {
assert (prev == State.UNLOCKED || prev == State.LOCKED_FOR_READ) : prev;
return State.UNLOCKED;
});
Assert.assertEquals(0, writers.get());
readers.decrementAndGet();
tracker.endValidation(pd);
}
}
catch (Throwable t)
{
t.printStackTrace();
throw t;
}
}, interrupt::signal, errors::add), SAFE, NON_DAEMON, UNSYNCHRONIZED);
}
interrupt.await(1, TimeUnit.MINUTES);
if (!errors.isEmpty())
Runner.mergeAndThrow(errors);
}
enum State { UNLOCKED, LOCKED_FOR_READ, LOCKED_FOR_WRITE }
}

View File

@ -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));
}
}

View File

@ -0,0 +1,102 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.apache.cassandra.fuzz.harry.util;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
import java.util.Random;
import org.junit.Assert;
import org.junit.Test;
import org.apache.cassandra.harry.util.DescriptorRanges;
public class RangesTest
{
@Test
public void simpleRangesTest()
{
List<DescriptorRanges.DescriptorRange> list = Arrays.asList(inclusiveRange(10, 20, 10),
inclusiveRange(40, 50, 10),
inclusiveRange(60, 70, 10),
inclusiveRange(80, 90, 10));
Collections.shuffle(list);
DescriptorRanges ranges = new DescriptorRanges(list);
Assert.assertTrue(ranges.isShadowed(10, 5));
Assert.assertFalse(ranges.isShadowed(10, 20));
Assert.assertFalse(ranges.isShadowed(15, 20));
Assert.assertTrue(ranges.isShadowed(49, 5));
Assert.assertFalse(ranges.isShadowed(55, 5));
Assert.assertFalse(ranges.isShadowed(50, 20));
Assert.assertTrue(ranges.isShadowed(90, 9));
}
@Test
public void randomizedRangesTest()
{
for (int i = 0; i < 1000; i++)
_randomizedRangesTest();
}
private void _randomizedRangesTest()
{
List<DescriptorRanges.DescriptorRange> rangesList = new ArrayList<>();
Random rnd = new Random();
for (int i = 0; i < 100; i++)
{
long a = rnd.nextInt(1000);
long b = rnd.nextInt(1000);
DescriptorRanges.DescriptorRange range = new DescriptorRanges.DescriptorRange(Math.min(a, b),
Math.max(a,b),
rnd.nextBoolean(),
rnd.nextBoolean(),
rnd.nextInt(1000));
rangesList.add(range);
}
DescriptorRanges ranges = new DescriptorRanges(rangesList);
for (int i = 0; i < 10000; i++)
{
long descriptor = rnd.nextLong();
long ts = rnd.nextInt(1000);
Assert.assertEquals(matchLinear(rangesList, descriptor, ts),
ranges.isShadowed(descriptor, ts));
}
}
public boolean matchLinear(List<DescriptorRanges.DescriptorRange> ranges, long descriptor, long ts)
{
for (DescriptorRanges.DescriptorRange range : ranges)
{
if (range.contains(descriptor, ts))
return true;
}
return false;
}
public DescriptorRanges.DescriptorRange inclusiveRange(long start, long end, long ts)
{
return new DescriptorRanges.DescriptorRange(start, end, true, true, 10);
}
}

View File

@ -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)

View File

@ -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;
}
}
}
}

View File

@ -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;
}
}

View File

@ -16,7 +16,7 @@
* limitations under the License.
*/
package org.apache.cassandra.distributed.test.log;
package org.apache.cassandra.fuzz.ring;
import java.util.List;
import java.util.Random;
@ -27,26 +27,21 @@ import java.util.stream.Collectors;
import com.google.common.util.concurrent.Uninterruptibles;
import org.junit.Test;
import harry.core.Configuration;
import harry.core.Run;
import harry.model.sut.SystemUnderTest;
import harry.visitors.GeneratingVisitor;
import harry.visitors.LoggingVisitor;
import harry.visitors.MutatingRowVisitor;
import harry.visitors.MutatingVisitor;
import harry.visitors.Visitor;
import org.apache.cassandra.distributed.Cluster;
import org.apache.cassandra.distributed.api.ConsistencyLevel;
import org.apache.cassandra.distributed.api.Feature;
import org.apache.cassandra.distributed.api.IInvokableInstance;
import org.apache.cassandra.distributed.api.TokenSupplier;
import org.apache.cassandra.distributed.fuzz.HarryHelper;
import org.apache.cassandra.distributed.fuzz.InJvmSut;
import org.apache.cassandra.harry.HarryHelper;
import org.apache.cassandra.distributed.shared.NetworkTopology;
import org.apache.cassandra.distributed.test.log.FuzzTestBase;
import org.apache.cassandra.gms.ApplicationState;
import org.apache.cassandra.gms.Gossiper;
import org.apache.cassandra.harry.dsl.ReplayingHistoryBuilder;
import org.apache.cassandra.harry.sut.SystemUnderTest;
import org.apache.cassandra.harry.sut.TokenPlacementModel;
import org.apache.cassandra.harry.sut.injvm.InJvmSut;
import org.apache.cassandra.locator.InetAddressAndPort;
import org.apache.cassandra.locator.ReplicationFactor;
import org.apache.cassandra.service.StorageService;
import org.apache.cassandra.tcm.Epoch;
import org.apache.cassandra.tcm.transformations.PrepareMove;
@ -65,35 +60,35 @@ public class ConsistentMoveTest extends FuzzTestBase
@Test
public void moveTest() throws Throwable
{
Configuration.ConfigurationBuilder configBuilder = HarryHelper.defaultConfiguration()
.setPartitionDescriptorSelector(new Configuration.DefaultPDSelectorConfiguration(1, 1))
.setClusteringDescriptorSelector(HarryHelper.defaultClusteringDescriptorSelectorConfiguration().setMaxPartitionSize(100).build());
IInvokableInstance cmsInstance = null;
try (Cluster cluster = builder().withNodes(3)
.withTokenSupplier(TokenSupplier.evenlyDistributedTokens(3))
.withNodeIdTopology(NetworkTopology.singleDcNetworkTopology(3, "dc0", "rack0"))
.appendConfig(c -> c.with(Feature.NETWORK))
.start())
{
IInvokableInstance cmsInstance = cluster.get(1);
cmsInstance = cluster.get(1);
IInvokableInstance movingInstance = cluster.get(2);
waitForCMSToQuiesce(cluster, cmsInstance);
configBuilder.setSUT(() -> new InJvmSut(cluster));
Run run = configBuilder.build().createRun();
cluster.coordinator(1).execute("CREATE KEYSPACE " + run.schemaSpec.keyspace +
" WITH replication = {'class': 'SimpleStrategy', 'replication_factor' : 2};",
ReplayingHistoryBuilder harry = HarryHelper.dataGen(new InJvmSut(cluster),
new TokenPlacementModel.SimpleReplicationFactor(2),
SystemUnderTest.ConsistencyLevel.ALL);
cluster.coordinator(1).execute(String.format("CREATE KEYSPACE %s WITH replication = {'class': 'SimpleStrategy', 'replication_factor' : 2};", HarryHelper.KEYSPACE),
ConsistencyLevel.ALL);
cluster.coordinator(1).execute(run.schemaSpec.compile().cql(), ConsistencyLevel.ALL);
cluster.coordinator(1).execute(harry.schema().compile().cql(), ConsistencyLevel.ALL);
waitForCMSToQuiesce(cluster, cmsInstance);
FuzzTestBase.QuiescentLocalStateChecker model = new FuzzTestBase.QuiescentLocalStateChecker(run, ReplicationFactor.fullOnly(2));
Visitor visitor = new LoggingVisitor(run, MutatingRowVisitor::new);
for (int i = 0; i < WRITES; i++)
visitor.visit();
Runnable writeAndValidate = () -> {
System.out.println("Starting write phase...");
for (int i = 0; i < WRITES; i++)
harry.insert();
System.out.println("Starting validate phase...");
harry.validateAll(harry.quiescentLocalChecker());
};
writeAndValidate.run();
model.validateAll();
// Make sure there can be only one FinishLeave in flight
waitForCMSToQuiesce(cluster, cmsInstance);
@ -114,15 +109,7 @@ public class ConsistentMoveTest extends FuzzTestBase
waitForCMSToQuiesce(cluster, nextEpoch);
// TODO: rewrite the test to check only PENDING ranges.
visitor = new GeneratingVisitor(run, new MutatingVisitor.MutatingVisitExecutor(run, new MutatingRowVisitor(run), SystemUnderTest.ConsistencyLevel.ALL));
for (int i = 0; i < WRITES; i++)
visitor.visit();
model.validateAll();
for (int i = 0; i < WRITES; i++)
visitor.visit();
model.validateAll();
writeAndValidate.run();
int clusterSize = cluster.size();
List<InetAddressAndPort> endpoints = cluster.stream().map(i -> InetAddressAndPort.getByAddress(i.config().broadcastAddress())).collect(Collectors.toList());
@ -135,6 +122,12 @@ public class ConsistentMoveTest extends FuzzTestBase
}
}));
}
catch (Throwable t)
{
if (cmsInstance != null)
unpauseCommits(cmsInstance);
throw t;
}
}
private void assertGossipStatus(Cluster cluster, int leavingInstance, String status)

647
test/harry/main/README.md Normal file
View File

@ -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, were 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 theres 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 well focus on ones that are most often used to implement different use cases:
* System Under Test: defines how Harry can communicate with Cassandra instances and issue common queries. Examples of a
system under test can be a CCM cluster, a “real” Cassandra cluster, or an in-JVM dtest cluster.
* Visitor: defines behaviour that gets triggered at a specific logical timestamp. One of the default implementations is
MutatingVisitor, which executes write workload against SystemUnderTest. Examples of a visitor, besides a mutating
visitor, could be a validator that uses the model to validate results of different queries, a repair runner, or a
fault injector.
* Model: validates results of read queries by comparing its own internal representation against the results returned by
system under test. You can find three simplified implementations of model in this document: Visible Rows Checker,
Quiescent Checker, and an Exhaustive Checker.
* Runner: defines how operations defined by visitors are executed. Harry includes two default implementations: a
sequential and a concurrent runner. Sequential runner allows no overlap between different visitors or logical
timestamps. Concurrent runner allows visitors for different timestamps to overlap.
System under test is the simplest one to implement: you only need a way to execute Cassandra queries. At the moment of
writing, all custom things, such as nodetool commands, failure injection, etc, are implemented using a SUT / visitor
combo: visitor knows about internals of the cluster it is dealing with.
Generally, visitor has to follow the rules specified by DescriptorSelector and PdSelector: (it can only visit issue
mutations against the partition that PdSelector has picked for this LTS), and DescriptorSelector (it can visit exactly
DescriptorSelector#numberOfModifications rows within this partition, operations have to have a type specified by
#operationKind, clustering and value descriptors have to be in accordance with DescriptorSelector#cd and
DescriptorSelector#vds). The reason for these limitations is because model has to be able to reproduce the exact
sequence of events that was applied to system under test.
Default implementations of partition and clustering descriptors, used in fuzz mode allow to optimise verification. For
example, it is possible to go find logical timestamps that are visiting the same partition as any given logical
timestamp. When running Harry in unit-test mode, we use a special generating visitor that keeps an entire given sequence
of events in memory rather than producing it on the fly. For reasons of efficiency, we do not use generating visitors
for generating and verifying large datasets.
# Formal Relations Between Entities
To be able to implement efficient models, we had to reduce the amount of state required for validation to a minimum and
try to operate on primitive data values most of the time. Any Harry run starts with a `seed`. Given the same
configuration, and the same seed, we're able to make runs deterministic (in other words, records in two clusters created
from the same seed are going to have different real-time timestamps, but will be otherwise identical; logical time
stamps will also be identical).
Since it's clear how to generate things like random schemas, cluster configurations, etc., let's discuss how we're
generating data, and why this type of generation makes validation efficient.
First, we're using PCG family of random number generators, which, besides having nice characteristics that any RNG
should have, have two important features:
* Streams: for single seed, we can have several independent _different_ streams of random numbers.
* Walkability: PCG generators generate a stream of numbers you can walk _back_ and _forth_. That is, for any number _n_
that represents a _position_ of the random number in the stream of random numbers, we can get the random number at
this position. Conversely, given a random number, we can determine what is its position in the stream. Moreover,
knowing a random number, we can determine which number precedes it in the stream of random numbers, and, finally, we
can determine how many numbers there are in a stream between the two random numbers.
Out of these operations, determining the _next_ random number in the sequence can be done in constant time, `O(1)`.
Advancing generator by _n_ steps can be done in `O(log(n))` steps. Since generation is cyclical, advancing the iterator
backward is equivalent to advancing it by `cardinality - 1` steps. If we're generating 64 bits of entropy, advancing
by `-1` can be done in 64 steps.
Let's introduce some definitions:
* `lts` is a *logical timestamp*, an entity (number in our case), given by the clock, on which some action occurs
* `m` is a *modification id*, a sequential number of the modification that occurs on `lts`
* `rts` is an approximate real-time as of clock for this run
* `pid` is a partition position, a number between `0` and N, for `N` unique generated partitions
* `pd` is a partition descriptor, a unique descriptor identifying the partition
* `cd` is a clustering descriptor, a unique descriptor identifying row within some partition
A small introduction that can help to understand the relation between these
entities. Hierarchically, the generation process looks as follows:
* `lts` is an entry point, from which the decision process starts
* `pd` is picked from `lts`, and determines which partition is going to be visited
* for `(pd, lts)` combination, `#mods` (the number of modification batches) and `#rows` (the number of rows per
modification batch) is determined. `m` is an index of the modification batch, and `i` is an index of the operation in
the modification batch.
* `cd` is picked based on `(pd, lts)`, and `n`, a sequential number of the operation among all modification batches
* operation type (whether we're going to perform a write, delete, range delete, etc), columns involved in this
operation, and values for the modification are picked depending on the `pd`, `lts`, `m`, and `i`
Most of this formalization is implemented in `OpSelectors`, and is relied upon in`PartitionVisitor` and any
implementation of a `Model`.
Random number generation (see `OpSelectors#Rng`):
* `rng(i, stream[, e])`: returns i'th number drawn from random sequence `stream` producing values with `e` bits of
entropy (64 bits for now).
* `rng'(s, stream[, e])`: returns `i` of the random number `s` drawn from the random sequence `stream`. This function is
an inverse of `rng`.
* `next(rnd, stream[, e])` and `prev(rnd, stream[, e])`: the next/previous number relative to `rnd` drawn from random
sequence `stream`.
A simple example of a partition descriptor selector is one that is based on a sliding window of a size `s`, that slides
every `n` iterations. First, we determine _where_ the window should start for a given `lts` (in other words, how many
times it has already slid). After that, we determine which `pd` we pick out of `s` available ones. After picking each
one of the `s` descriptors `n` times, we retire the oldest descriptor and pick a new one to the window. Window start and
offset are then used as input for the `rng(start + offset, stream)` to make sure descriptors are uniformly distributed.
We can build a clustering descriptor selector in a similar manner. Each partition will use its `pd` as a stream id, and
pick `cd` from a universe of possible `cds` of size `#cds`. On each `lts`, we pick a random `offset`, and start
picking `#ops` clusterings from this `offset < #cds`, and wrap around to index 0 after that. This way, each operation
maps to a unique `cd`, and `#op` can be determined from `cd` deterministically.
# Data Generation
So far, we have established how to generate partition, clustering, and value _descriptors_. Now, we need to understand
how we can generate data modification statements out of these descriptors in a way that helps us to validate data later.
Since every run has a predefined schema, and by the time we visit a partition we have a logical timestamp, we can make
the rest of the decisions: pick a number of batches we're about to perform, determine what kind of operations each one
of the batches is going to contain, which rows we're going to visit (clustering for each modification operation).
To generate a write, we need to know _which partition_ we're going to visit (in other words, partition descriptor),
_which row_ we'd like to modify (in other words, clustering descriptor), _which columns_ we're modifying (in other
words, a column mask), and, for each modified column - its value. By the time we're ready to make an actual query to the
database, we already know `pd`, `cd`, `rts`, and `vds[]`, which is all we need to "inflate" a write.
To inflate each value descriptor, we take a generator for its datatype, and turn its descriptor into the object. This
generation process has the following important properties:
* it is invertible: for every `inflate(vd) -> value`, there's `deflate(value) -> vd`
* it is order-preserving: `compare(vd1, vd2) == compare(inflate(vd1), inflate(vd2))`
Inflating `pd` and `cd` is slightly more involved than inflating `vds`, since partition and clustering keys are often
composite. This means that `inflate(pd)` returns an array of objects, rather just a single
object: `inflate(pd) -> value[]`, and `deflate(value[]) -> pd`. Just like inflating value descriptors, inflating keys
preserves order.
It is easy to see that, given two modifications: `Update(pd1, cd1, [vd1_1, vd2_1, vd3_1], lts1)`
and `Update(pd1, cd1, [vd1_2, vd3_2], lts2)`, we will end up with a resultset that contains effects of both
operations: `ResultSetRow(pd1, cd1, [vd1_2@rts2, vd2_1@rts1, vd3_2@rts2])`.
# Model
`Model` in Harry ties the rest of the components together and allows us to check whether or not data returned by the
cluster actually makes sense. The model relies on the clock, since we have to convert real-time timestamps of the
returned values back to logical timestamps, and on descriptor selectors to pick the right partition and rows.
## Visible Rows Checker
Let's try to put it all together and build a simple model. The simplest one is a visible row checker. It can check if
any row in the response returned from the database could have been produced by one of the operations. However, it won't
be able to find errors related to missing rows, and will only notice some cases of erroneously overwritten rows.
In the model, we can see a response from the database in its deflated state. In other words, instead of the actual
values returned, we see their descriptors. Every resultset row consists of `pd`, `cd`, `vds[]` (value descriptors),
and `lts[]` (logical timestamps at which these values were written).
To validate, we need to iterate through all operations for this partition, starting with the latest one the model is
aware of. This model has no internal state, and validates entire partitions:
```
void validatePartitionState(long validationLts, List<ResultSetRow> rows) {
long pd = pdSelector.pd(validationLts, schema);
for (ResultSetRow row : rows) {
// iterator that gives us unique lts from the row in descending order
LongIterator rowLtsIter = descendingIterator(row.lts);
// iterator that gives us unique lts from the model in descending order
LongIterator modelLtsIter = descendingIterator(pdSelector, validationLts);
outer:
while (rowLtsIter.hasNext()) {
long rowLts = rowLtsIter.nextLong();
// this model can not check columns whose values were never written or were deleted
if (rowLts == NO_TIMESTAMP)
continue outer;
if (!modelLtsIter.hasNext())
throw new ValidationException(String.format("Model iterator is exhausted, could not verify %d lts for the row: \n%s %s",
rowLts, row));
while (modelLtsIter.hasNext()) {
long modelLts = modelLtsIter.nextLong();
// column was written by the operation that has a lower lts than the current one from the model
if (modelLts > rowLts)
continue;
// column was written by the operation that has a higher lts, which contradicts to the model, since otherwise we'd validate it by now
if (modelLts < rowLts)
throw new RuntimeException("Can't find a corresponding event id in the model for: " + rowLts + " " + modelLts);
// Compare values for columns that were supposed to be written with this lts
for (int col = 0; col < row.lts.length; col++) {
if (row.lts[col] != rowLts)
continue;
long m = descriptorSelector.modificationId(pd, row.cd, rowLts, row.vds[col], col);
long vd = descriptorSelector.vd(pd, row.cd, rowLts, m, col);
// If the value model predicts doesn't match the one received from the database, throw an exception
if (vd != row.vds[col])
throw new RuntimeException("Returned value doesn't match the model");
}
continue outer;
}
}
}
}
```
As you can see, all validation is done using deflated `ResultSetRows`, which contain enough data to say which logical
timestamp each value was written with, and which value descriptor each value has. This model can also validate data
concurrently to the ongoing data modification operations.
## Quiescent Checker
Let's consider one more checker. It'll be more powerful than the visible rows checker in one way since it can find any
inconsistency in data (incorrect timestamp, missing or additional row, rows coming in the wrong order, etc), but it'll
also have one limitation: it won't be able to run concurrently with data modification statements. This means that for
this model to be used, we should have no _in-flight_ queries, and all queries have to be in a deterministic state by the
time we're validating their results.
For this checker, we assume that we have a component that is called `Reconciler`, which can inflate partition state _up
to some_ `lts`. `Reconciler` works by simply applying each modification in the same order they were applied to the
cluster, and using standard Cassandra data reconciliation rules (last write wins / DELETE wins over INSERT in case of a
timestamp collision).
With this component, and knowing that there can be no in-fight queries, we can validate data in the following way:
```
public void validatePartitionState(Iterator<ResultSetRow> actual, Query query) {
// find out up the highest completed logical timestamp
long maxCompleteLts = tracker.maxComplete();
// get the expected state from reconciler
Iterator<Reconciler.RowState> expected = reconciler.inflatePartitionState(query.pd, maxCompleteLts, query).iterator(query.reverse);
// compare actual and expected rows one-by-one in-order
while (actual.hasNext() && expected.hasNext()) {
ResultSetRow actualRowState = actual.next();
Reconciler.RowState expectedRowState = expected.next();
if (actualRowState.cd != expectedRowState.cd)
throw new ValidationException("Found a row in the model that is not present in the resultset:\nExpected: %s\nActual: %s",
expectedRowState, actualRowState);
if (!Arrays.equals(actualRowState.vds, expectedRowState.vds))
throw new ValidationException("Returned row state doesn't match the one predicted by the model:\nExpected: %s (%s)\nActual: %s (%s).",
Arrays.toString(expectedRowState.vds), expectedRowState,
Arrays.toString(actualRowState.vds), actualRowState);
if (!Arrays.equals(actualRowState.lts, expectedRowState.lts))
throw new ValidationException("Timestamps in the row state don't match ones predicted by the model:\nExpected: %s (%s)\nActual: %s (%s).",
Arrays.toString(expectedRowState.lts), expectedRowState,
Arrays.toString(actualRowState.lts), actualRowState);
}
if (actual.hasNext() || expected.hasNext()) {
throw new ValidationException("Expected results to have the same number of results, but %s result iterator has more results",
actual.hasNext() ? "actual" : "expected");
}
}
```
If there's any mismatch, it'll be caught right away: if there's an extra row
(for example, there were issues in Cassandra that caused it to have duplicate
rows), or if some row or even value in the row is missing.
## Exhaustive Checker
To be able to both run validation concurrently to modifications and be able to
catch all kinds of inconsistencies, we need a more involved checker.
In this checker, we rely on inflating partition state. However, we're most
interested in `lts`, `opId`, and visibility (whether or not it is still
in-flight) of each modification operation. To be able to give a reliable result,
we need to make sure we follow these rules:
* every operation model _thinks_ should be visible, has to be visible
* every operation model _thinks_ should be invisible, has to be invisible
* every operation model doesn't know the state of (i.e., it is still
in-flight) can be _either_ visible _invisible_
* there can be no state in the database that model is not aware of (in other words,
we either can _explain_ how a row came to be, or we conclude that the row is
erroneous)
A naive way to do this would be to inflate every possible partition state, where
every in-flight operation would be either visible or invisible, but this gets
costly very quickly since the number of possible combinations grows
exponentially. A better (and simpler) way to do this is to iterate all
operations and keep the state of "explained" operations:
```
public class RowValidationState {
// every column starts in UNOBSERVED, and has to move to either REMOVED, or OBSERVED state
private final ColumnState[] columnStates;
// keep track of operations related to each column state
private final Operation[] causingOperations;
}
```
Now, we move through all operations for a given row, starting from the _newest_ ones, towards
the oldest ones:
```
public void validatePartitionState(long verificationLts, PeekingIterator<ResultSetRow> actual_, Query query) {
// get a list of operations for each cd
NavigableMap<Long, List<Operation>> operations = inflatePartitionState(query);
for (Map.Entry<Long, List<Operation>> entry : operations.entrySet()) {
long cd = entry.getKey();
List<Operation> ops = entry.getValue();
// Found a row that is present both in the model and in the resultset
if (actual.hasNext() && actual.peek().cd == cd) {
validateRow(new RowValidationState(actual.next), operations);
} else {
validateNoRow(cd, operations);
// Row is not present in the resultset, and we currently look at modifications with a clustering past it
if (actual.hasNext() && cmp.compare(actual.peek().cd, cd) < 0)
throw new ValidationException("Couldn't find a corresponding explanation for the row in the model");
}
}
// if there are more rows in the resultset, and we don't have model explanation for them, we've found an issue
if (actual.hasNext())
throw new ValidationException("Observed unvalidated rows");
}
```
Now, we have to implement `validateRow` and `validateNoRow`. `validateNoRow` is easy: we only need to make sure that a
set of operations results in an invisible row. Since we're iterating operations in reverse order, if we encounter a
delete not followed by any writes, we can conclude that the row is invisible and exit early. If there's a write that is
not followed by a delete, and the row isn't covered by a range tombstone, we know it's an error.
`validateRow` only has to iterate operations in reverse order until it can explain the value in every column. For
example, if a value is `UNOBSERVED`, and the first thing we encounter is a `DELETE` that removes this column, we only
need to make sure that the value is actually `null`, in which case we can conclude that the value can be explained
as `REMOVED`.
Similarly, if we encounter an operation that has written the expected value, we conclude that the value is `OBSERVED`.
If there are any seeming inconsistencies between the model and resultset, we have to check whether or not the operation
in question is still in flight. If it is, its results may still not be visible, so we can't reliably say it's an error.
To summarize, in order for us to implement an exhaustive checker, we have to iterate operations for each of the rows
present in the model in reverse order until we either detect inconsistency that can't be explained by an in-flight
operation or until we explain every value in the row.
## Conclusion
As you can see, all checkers up till now are almost entirely stateless. Exhaustive and quiescent models rely
on `DataTracker` component that is aware of the in-flight and completed `lts`, but don't need any other state apart from
that, since we can always inflate a complete partition from scratch every time we validate.
While not relying on the state is a useful feature, at least _some_ state is useful to have. For example, if we're
validating just a few rows in the partition, right now we have to iterate through each and every `lts` that has visited
this partition and filter out only modifications that have visited it. However, since the model is notified of each
_started_, and, later, finished modification via `recordEvent`, we can keep track of `pd -> (cd -> lts)` map. You can
check out `VisibleRowsChecker` as an example of that.

View File

@ -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)

View File

@ -0,0 +1,230 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.apache.cassandra.harry.checker;
import java.util.ArrayList;
import java.util.List;
import java.util.Random;
import java.util.function.Consumer;
import org.apache.cassandra.harry.gen.EntropySource;
import org.apache.cassandra.harry.gen.rng.JdkRandomEntropySource;
public class ModelChecker<STATE>
{
private final List<StepExecutor<STATE>> steps;
private final List<Precondition<STATE>> invariants;
private Precondition<STATE> exitCondition;
private Consumer<STATE> beforeAll;
private Consumer<STATE> afterAll;
private STATE init;
public ModelChecker()
{
steps = new ArrayList<>();
invariants = new ArrayList<>();
}
public void run() throws Throwable
{
run(Integer.MAX_VALUE, System.currentTimeMillis());
}
public void run(int maxSteps, long seed) throws Throwable
{
assert init != null : "Initial condition is not specified";
Ref<STATE> state = new Ref<>(init);
EntropySource entropySource = new JdkRandomEntropySource(new Random(seed));
if (beforeAll != null)
beforeAll.accept(state.get());
for (int i = 0; i < maxSteps; i++)
{
if (exitCondition != null && exitCondition.test(state.get()))
return;
// TODO: add randomisation / probability for triggering a specific step
steps.get(entropySource.nextInt(steps.size())).execute(state, entropySource.derive());
for (Precondition<STATE> invariant : invariants)
invariant.test(state.get());
}
if (afterAll != null)
afterAll.accept(state.get());
}
public ModelChecker<STATE> init(STATE state)
{
this.init = state;
return this;
}
public ModelChecker<STATE> beforeAll(Consumer<STATE> precondition)
{
this.beforeAll = precondition;
return this;
}
public ModelChecker<STATE> afterAll(Consumer<STATE> postcondition)
{
this.afterAll = postcondition;
return this;
}
public ModelChecker<STATE> exitCondition(Precondition<STATE> precondition)
{
this.exitCondition = precondition;
return this;
}
public ModelChecker<STATE> step(Precondition<STATE> precondition, Step<STATE> step)
{
steps.add((ref, entropySource) -> {
ref.map(state -> {
if (!precondition.test(state))
return state;
return step.next(state, entropySource);
});
});
return this;
}
public ModelChecker<STATE> invariant(Precondition<STATE> invariant)
{
invariants.add(invariant);
return this;
}
public ModelChecker<STATE> step(Step<STATE> step)
{
return step(Precondition.alwaysTrue(), step);
}
public ModelChecker<STATE> step(StatePrecondition<STATE> precondition, ThrowingFunction<STATE, STATE> step)
{
steps.add((ref, entropySource) -> {
ref.map(state -> {
if (!precondition.test(state))
return state;
return step.apply(state);
});
});
return this;
}
public ModelChecker<STATE> step(ThrowingConsumer<STATE> step)
{
return step((t, entropySource) -> {
step.consume(t);
return t;
});
}
public ModelChecker<STATE> step(ThrowingFunction<STATE, STATE> step)
{
return step((t, entropySource) -> {
return step.apply(t);
});
}
interface StepExecutor<STATE>
{
void execute(Ref<STATE> state, EntropySource entropySource) throws Throwable;
}
public interface StatePrecondition<STATE>
{
boolean test(STATE state) throws Throwable;
}
public interface Precondition<STATE>
{
boolean test(STATE state) throws Throwable;
static <STATE> Precondition<STATE> alwaysTrue()
{
return (a) -> true;
}
}
public interface Step<STATE>
{
STATE next(STATE t, EntropySource entropySource) throws Throwable;
}
public interface ThrowingConsumer<I>
{
void consume(I t) throws Throwable;
}
public interface ThrowingFunction<I, O>
{
O apply(I t) throws Throwable;
}
public interface ThrowingBiFunction<I1, I2, O>
{
O apply(I1 t1, I2 t2) throws Throwable;
}
public static class Ref<T>
{
public T ref;
public Ref(T init)
{
this.ref = init;
}
public T get()
{
return ref;
}
public void set(T v)
{
this.ref = v;
}
public void map(ThrowingFunction<T, T> fn) throws Throwable
{
this.ref = fn.apply(ref);
}
}
public static class State<MODEL, SUT>
{
public final MODEL model;
public final SUT sut;
public State(MODEL model, SUT sut)
{
this.model = model;
this.sut = sut;
}
public State<MODEL, SUT> next(MODEL state)
{
return new State<>(state, this.sut);
}
}
}

View File

@ -0,0 +1,279 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.apache.cassandra.harry.clock;
import java.util.concurrent.CopyOnWriteArrayList;
import java.util.concurrent.Executors;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicLong;
import java.util.concurrent.locks.LockSupport;
import com.google.common.annotations.VisibleForTesting;
import org.apache.cassandra.harry.core.Configuration;
import org.apache.cassandra.harry.model.OpSelectors;
/**
* Monotonic clock, that guarantees that any LTS can be converted to a unique RTS, given
* the number of LTS does not exceed the number of RTS (e.g., we do not draw LTS more frequently
* than once per microsecond).
* <p>
* This conversion works as follows:
* <p>
* * every `period`, we record the current timestamp and maximum seen LTS, and keep history of up to
* `historySize` LTS/timestamp combinations
* * when queried to retrieve the LTS, we find a timestamp, relative to which we can calculate RTS.
* After that, we calculate a difference between the largest LTS that is still smaller than the converted
* one, and add this difference to the timestamp.
* <p>
* This way, later LTS can only be mapped to later RTS, and any LTS that was drawn previously, will be
* uniquely mapped relative to some timestamp, with the order matching the LTS order.
*/
public class ApproximateClock implements OpSelectors.Clock
{
public static final long START_VALUE = 0;
public static final long DEFUNCT = Long.MIN_VALUE;
public static final long REBASE_IN_PROGRESS = Long.MIN_VALUE + 1;
// TODO: there's a theoretical possibility of a bug; when we have several consecutive epochs without
// change in LTS, current implementation will return the latest epoch instead of the earliest one.
// This is not a big deal in terms of monotonicity but can cause some problems when validating TTL.
// The simples fix would be to find the smallest matching epoch.
private final ScheduledExecutorService executor;
private final int historySize;
private final CopyOnWriteArrayList<Long> ltsHistory;
private final long startTimeMicros;
private volatile int idx;
private final AtomicLong lts;
private final long periodMicros;
private final long epoch;
private final TimeUnit epochTimeUnit;
public ApproximateClock(long period, TimeUnit timeUnit)
{
this(10000, period, timeUnit);
}
public ApproximateClock(int historySize, long epoch, TimeUnit epochTimeUnit)
{
this(TimeUnit.MILLISECONDS.toMicros(System.currentTimeMillis()),
historySize, new CopyOnWriteArrayList<>(), START_VALUE, 0, epoch, epochTimeUnit);
rebase();
}
ApproximateClock(long startTimeMicros,
int historySize,
CopyOnWriteArrayList<Long> history,
long lts,
int idx,
long epoch,
TimeUnit epochTimeUnit)
{
this.startTimeMicros = startTimeMicros;
this.historySize = historySize;
this.ltsHistory = history;
this.lts = new AtomicLong(lts);
this.idx = idx;
this.periodMicros = epochTimeUnit.toMicros(epoch);
this.executor = Executors.newSingleThreadScheduledExecutor(r -> {
Thread t = new Thread(r);
t.setName("ApproximateMonotonicClock-ScheduledTasks");
t.setDaemon(true);
return t;
});
this.executor.scheduleAtFixedRate(this::rebase, epoch, epoch, epochTimeUnit);
this.epoch = epoch;
this.epochTimeUnit = epochTimeUnit;
}
@VisibleForTesting
public static ApproximateClock forDebug(long startTimeMicros, int historySize, long lts, int idx, long period, TimeUnit timeUnit, long... values)
{
CopyOnWriteArrayList<Long> history = new CopyOnWriteArrayList<>();
for (int i = 0; i < values.length; i++)
history.set(i, values[i]);
assert values.length == idx; // sanity check
return new ApproximateClock(startTimeMicros, historySize, history, lts, idx, period, timeUnit);
}
public long get(long idx)
{
return ltsHistory.get((int) (idx % historySize));
}
private void rebase()
{
int arrayIdx = idx % historySize;
long rebaseLts = lts.get();
if (rebaseLts == DEFUNCT)
throw new IllegalStateException();
while (!lts.compareAndSet(rebaseLts, REBASE_IN_PROGRESS))
rebaseLts = lts.get();
ltsHistory.add(arrayIdx, rebaseLts == START_VALUE ? START_VALUE : (rebaseLts + 1));
// If we happen to exhaust counter, we just need to make operations "wider".
// It is unsafe to proceed, so we defunct the clock.
//
// We could make a clock implementation that would sleep on `get`, but it will
// be more expensive, since we'll have to check for overflow each time before
// returning anything.
if (idx > 1 && get(idx) - get(idx - 1) > periodMicros)
{
lts.set(DEFUNCT);
executor.shutdown();
throwCounterExhaustedException();
}
idx = idx + 1;
if (!lts.compareAndSet(REBASE_IN_PROGRESS, rebaseLts))
throw new IllegalStateException("No thread should have changed LTS during rebase. " + lts.get());
}
@Override
public long nextLts()
{
long current = lts.get();
while (true)
{
if (current >= 0)
{
if (lts.compareAndSet(current, current + 1))
return current;
current = lts.get();
continue;
}
if (current == REBASE_IN_PROGRESS)
{
LockSupport.parkNanos(1);
current = lts.get();
continue;
}
if (current == DEFUNCT)
throwCounterExhaustedException();
throw new IllegalStateException("This should have been unreachable: " + current);
}
}
public long peek()
{
while (true)
{
long ret = lts.get();
if (ret == REBASE_IN_PROGRESS)
{
LockSupport.parkNanos(1);
continue;
}
if (ret == DEFUNCT)
throwCounterExhaustedException();
return ret;
}
}
public Configuration.ClockConfiguration toConfig()
{
int idx = this.idx;
long[] history = new long[Math.min(idx, historySize)];
for (int i = 0; i < history.length; i++)
history[i] = ltsHistory.get(i);
return new Configuration.DebugApproximateClockConfiguration(startTimeMicros,
ltsHistory.size(),
history,
lts.get(),
idx,
epoch,
epochTimeUnit);
}
public long lts(final long rts)
{
final int historyIdx = idx - 1;
for (int i = 0; i < historySize - 1 && historyIdx - i >= 0; i++)
{
long periodStartRts = startTimeMicros + periodMicros * (historyIdx - i);
if (rts >= periodStartRts)
{
long periodStartLts = get(historyIdx - i);
return periodStartLts + rts - periodStartRts;
}
}
throw new IllegalStateException("RTS is too old to convert to LTS: " + rts + "\n " + ltsHistory);
}
public long rts(final long lts)
{
assert lts <= peek() : String.format("Queried for LTS we haven't yet issued %d. Max is %d.", lts, peek());
final int historyIdx = idx - 1;
for (int i = 0; i < historySize - 1 && historyIdx - i >= 0; i++)
{
long periodStartLts = get(historyIdx - i);
if (lts >= periodStartLts)
{
long periodStartRts = startTimeMicros + periodMicros * (historyIdx - i);
return periodStartRts + lts - periodStartLts;
}
}
throw new IllegalStateException("LTS is too old to convert to RTS: " + lts + "\n " + dumpHistory());
}
private String dumpHistory()
{
String s = "";
int idx = this.idx;
for (int i = 0; i < Math.min(idx, historySize); i++)
{
s += ltsHistory.get(i) + ",";
}
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));
}
}

View File

@ -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);
}
}
}

File diff suppressed because it is too large Load Diff

View File

@ -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(){}
}
}

View File

@ -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;
}
}

View File

@ -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<Long> 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;
}
}

View File

@ -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]));
}
}

View File

@ -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()));
}
}

View File

@ -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<Integer> 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()));
}
}

View File

@ -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<ResultSetRow> 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();
}
}
}

View File

@ -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);
}
}

View File

@ -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()));
}
}

View File

@ -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<Long> 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<Long> 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))) +
")";
}
}

View File

@ -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<T>
{
public final String name;
public final DataType<T> type;
public final Kind kind;
int columnIndex;
public ColumnSpec(String name,
DataType<T> 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<T> 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<T>
{
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<T> 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<Byte> int8Type = new DataType<Byte>("tinyint")
{
public Bijections.Bijection<Byte> generator()
{
return Bijections.INT8_GENERATOR;
}
};
public static final DataType<Short> int16Type = new DataType<Short>("smallint")
{
public Bijections.Bijection<Short> generator()
{
return Bijections.INT16_GENERATOR;
}
};
public static final DataType<Integer> int32Type = new DataType<Integer>("int")
{
public Bijections.Bijection<Integer> generator()
{
return Bijections.INT32_GENERATOR;
}
};
public static final DataType<Long> int64Type = new DataType<Long>("bigint")
{
public Bijections.Bijection<Long> generator()
{
return Bijections.INT64_GENERATOR;
}
};
public static final DataType<Boolean> booleanType = new DataType<Boolean>("boolean")
{
public Bijections.Bijection<Boolean> 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<Float> floatType = new DataType<Float>("float")
{
public Bijections.Bijection<Float> generator()
{
return Bijections.FLOAT_GENERATOR;
}
};
public static final DataType<Double> doubleType = new DataType<Double>("double")
{
public Bijections.Bijection<Double> generator()
{
return Bijections.DOUBLE_GENERATOR;
}
};
public static final DataType<String> asciiType = new DataType<String>("ascii")
{
private final Bijections.Bijection<String> gen = new StringBijection();
public Bijections.Bijection<String> generator()
{
return gen;
}
public int compareLexicographically(long l, long r)
{
return Long.compare(l, r);
}
};
public static final DataType<String> textType = new DataType<String>("text")
{
private final Bijections.Bijection<String> gen = new StringBijection();
public Bijections.Bijection<String> generator()
{
return gen;
}
public int compareLexicographically(long l, long r)
{
return Long.compare(l, r);
}
};
public static DataType<String> asciiType(int nibbleSize, int maxRandomBytes)
{
Bijections.Bijection<String> gen = new StringBijection(nibbleSize, maxRandomBytes);
return new DataType<String>("ascii")
{
public Bijections.Bijection<String> 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<UUID> uuidType = new DataType<UUID>("uuid")
{
public Bijections.Bijection<UUID> 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<UUID> timeUuidType = new DataType<UUID>("timeuuid")
{
public Bijections.Bijection<UUID> 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<Date> timestampType = new DataType<Date>("timestamp")
{
public Bijections.Bijection<Date> 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<DataType<?>> 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<T> extends DataType<T>
{
public static final Map<DataType<?>, 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<T> baseType;
private final Bijections.Bijection<T> generator;
public ReversedType(DataType<T> baseType)
{
super(baseType.cqlName);
this.baseType = baseType;
this.generator = new Bijections.ReverseBijection<>(baseType.generator());
}
public ReversedType(DataType<T> baseType, Bijections.Bijection<T> generator)
{
super(baseType.cqlName);
this.baseType = baseType;
this.generator = generator;
}
public boolean isReversed()
{
return true;
}
public Bijections.Bijection<T> generator()
{
return generator;
}
public int maxSize()
{
return baseType.maxSize();
}
public DataType<T> baseType()
{
return baseType;
}
public static <T> DataType<T> getInstance(DataType<T> type)
{
ReversedType<T> t = (ReversedType<T>) cache.get(type);
if (t == null)
t = new ReversedType<>(type);
assert t.baseType == type : "Type mismatch";
return t;
}
}
}

View File

@ -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<String, ColumnSpec.DataType<?>> nameToTypeMap;
public static final Collection<ColumnSpec.DataType<?>> columnTypes;
public static final Collection<ColumnSpec.DataType<?>> partitionKeyTypes;
public static final Collection<ColumnSpec.DataType<?>> 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<ColumnSpec.DataType<?>> builder = new ArrayList<>(partitionKeyTypes);
Map<String, ColumnSpec.DataType<?>> 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 <T> Generator<T> fromValues(Collection<T> allValues)
{
return fromValues((T[]) allValues.toArray());
}
public static <T> Generator<T> fromValues(T[] allValues)
{
return (rng) -> {
return allValues[rng.nextInt(allValues.length - 1)];
};
}
@SuppressWarnings("unchecked")
public static Generator<ColumnSpec<?>> columnSpecGenerator(String prefix, ColumnSpec.Kind kind)
{
return fromValues(columnTypes)
.map(new Function<ColumnSpec.DataType<?>, ColumnSpec<?>>()
{
private int counter = 0;
public ColumnSpec<?> apply(ColumnSpec.DataType<?> type)
{
return new ColumnSpec<>(prefix + (counter++),
type,
kind);
}
});
}
@SuppressWarnings("unchecked")
public static Generator<ColumnSpec<?>> columnSpecGenerator(Collection<ColumnSpec.DataType<?>> columnTypes, String prefix, ColumnSpec.Kind kind)
{
return fromValues(columnTypes)
.map(new Function<ColumnSpec.DataType<?>, 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<ColumnSpec<?>> clusteringColumnSpecGenerator(String prefix)
{
return fromValues(clusteringKeyTypes)
.map(new Function<ColumnSpec.DataType<?>, 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<ColumnSpec<?>> partitionColumnSpecGenerator(String prefix)
{
return fromValues(partitionKeyTypes)
.map(new Function<ColumnSpec.DataType<?>, 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<String> tableNameSupplier;
private Generator<ColumnSpec<?>> pkGenerator = partitionColumnSpecGenerator("pk");
private Generator<ColumnSpec<?>> ckGenerator = clusteringColumnSpecGenerator("ck");
private Generator<ColumnSpec<?>> regularGenerator = columnSpecGenerator("regular", ColumnSpec.Kind.REGULAR);
private Generator<ColumnSpec<?>> 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<String> 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<ColumnSpec.DataType<?>> 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<ColumnSpec.DataType<?>> 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<ColumnSpec.DataType<?>> 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<ColumnSpec.DataType<?>> 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<ColumnCounts> 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<SchemaSpec> generator()
{
Generator<ColumnCounts> columnCountsGenerator = columnCountsGenerator();
return columnCountsGenerator.flatMap(counts -> {
return rand -> {
List<ColumnSpec<?>> pk = pkGenerator.generate(rand, counts.pks);
List<ColumnSpec<?>> 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<SchemaSpec> surjection()
{
return generator().toSurjection(SCHEMAGEN_STREAM_ID);
}
}
public static Surjections.Surjection<SchemaSpec> 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<String> tableNameSupplier = () -> DEFAULT_PREFIX + counter.getAndIncrement();
// simplest schema gen, nothing can go wrong with it
public static final Surjections.Surjection<SchemaSpec> 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<String> simpleStringType = ColumnSpec.asciiType(4, 10);
private static final Surjections.Surjection<SchemaSpec> 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<SchemaSpec> 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<SchemaSpec> 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<SchemaSpec> 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<SchemaSpec> 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<SchemaSpec> 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<SchemaSpec>[] 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<SchemaSpec> progression(int switchAfter)
{
Supplier<SchemaSpec>[] generators = new Supplier[PROGRESSIVE_GENERATORS.length];
for (int i = 0; i < generators.length; i++)
generators[i] = PROGRESSIVE_GENERATORS[i].toSupplier();
return new Supplier<SchemaSpec>()
{
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<ColumnSpec<?>> toColumns(Map<String, String> config, ColumnSpec.Kind kind, boolean allowReverse)
{
if (config == null)
return Collections.EMPTY_LIST;
List<ColumnSpec<?>> columns = new ArrayList<>(config.size());
for (Map.Entry<String, String> 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<String, String> pks,
Map<String, String> cks,
Map<String, String> regulars,
Map<String, String> 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;
}

View File

@ -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<ColumnSpec<?>> partitionKeys;
public final List<ColumnSpec<?>> clusteringKeys;
public final List<ColumnSpec<?>> regularColumns;
public final List<ColumnSpec<?>> staticColumns;
public final List<ColumnSpec<?>> allColumns;
public final Set<ColumnSpec<?>> 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<ColumnSpec<?>> partitionKeys,
List<ColumnSpec<?>> clusteringKeys,
List<ColumnSpec<?>> regularColumns,
List<ColumnSpec<?>> 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<ColumnSpec<?>> partitionKeys,
List<ColumnSpec<?>> clusteringKeys,
List<ColumnSpec<?>> regularColumns,
List<ColumnSpec<?>> 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<ColumnSpec<?>> 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<Relation> 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<Relation> 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<bigint> 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<StringBuilder>
{
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 <T> Iterable<T> concat(Iterable<T>... iterables)
{
assert iterables != null && iterables.length > 0;
if (iterables.length == 1)
return iterables[0];
return () -> {
return new Iterator<T>()
{
int idx;
Iterator<T> 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;
}
};
};
}
}

View File

@ -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);
}

View File

@ -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<ReplayingVisitor.Visit> 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();
}
}

View File

@ -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<ReplayingVisitor.Visit>, 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<Long, ReplayingVisitor.Visit> log;
// TODO: primitive array with a custom/noncopying growth strat
protected final Map<Long, PartitionVisitState> 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<Long> visitedPds()
{
return new ArrayList<>(partitionStates.keySet());
}
@Override
public Iterator<ReplayingVisitor.Visit> 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<Long, Long> 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<Long> 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<Long> 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<Long> 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<Long> returned;
private final EntropySource entropySource;
private final List<Long> 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");
}
}
}

View File

@ -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<Long> visitedLts;
PartitionVisitState(long pd, long[] possibleCds, NavigableSet<Long> visitedLts)
{
this.pd = pd;
this.possibleCds = possibleCds;
this.visitedLts = visitedLts;
}
}

View File

@ -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);
}
}

View File

@ -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();
}

View File

@ -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<VisitExecutor.BaseOperation> 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<ReplayingVisitor.Visit> 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<ReplayingVisitor.Visit> 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<EntropySource> rng)
{
entropySource.seed(seed);
rng.accept(entropySource);
}
public EntropySource withSeed(long seed)
{
entropySource.seed(seed);
return entropySource;
}
}
}

View File

@ -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<Long>
{
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));
}
}

View File

@ -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<Byte> INT8_GENERATOR = new ByteGenerator();
public static final Bijection<Short> INT16_GENERATOR = new Int16Generator();
public static final Bijection<Integer> INT32_GENERATOR = new Int32Generator();
public static final Bijection<Long> INT64_GENERATOR = new LongGenerator();
public static final Bijection<Float> FLOAT_GENERATOR = new FloatGenerator();
public static final Bijection<Double> DOUBLE_GENERATOR = new DoubleGenerator();
public static final Bijection<Boolean> BOOLEAN_GENERATOR = new BooleanGenerator();
public static final Bijection<UUID> UUID_GENERATOR = new UUIDGenerator();
public static final TimeUUIDGenerator TIME_UUID_GENERATOR = new TimeUUIDGenerator();
public static final Bijection<Date> 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.
* <p>
* deflated has to be equal to adjustEntropyDomain value.
* <p>
* When inflating, we should inflate up to adjustEntropyDomain values. This way, deflated values will correspond to infoated ones.
*/
public interface Bijection<T>
{
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<T> implements Bijection<T>
{
private final Bijection<T> delegate;
public ReverseBijection(Bijection<T> 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<Long>
{
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<Integer>
{
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<Short>
{
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<Byte>
{
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<Boolean>
{
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<Float>
{
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<Double>
{
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<UUID>
{
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<UUID>
{
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<Date>
{
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;
}
}
}

View File

@ -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<Boolean>
{
public static BooleanGenerator INSTANCE = new BooleanGenerator();
public Boolean generate(EntropySource rng)
{
return rng.nextBoolean();
}
}

View File

@ -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);
}
}

View File

@ -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 <K, V> ColumnSpec.DataType<Map<K, V>> mapColumn(ColumnSpec.DataType<K> k,
ColumnSpec.DataType<V> v,
int maxSize)
{
return new ColumnSpec.DataType<Map<K, V>>(String.format("map<%s,%s>", k.toString(), v.toString()))
{
private final Bijections.Bijection<Map<K, V>> gen = mapGen(k.generator(), v.generator(), maxSize);
public Bijections.Bijection<Map<K, V>> generator()
{
return gen;
}
public int maxSize()
{
return Long.BYTES;
}
};
}
public static <V> ColumnSpec.DataType<List<V>> listColumn(ColumnSpec.DataType<V> v,
int maxSize)
{
return new ColumnSpec.DataType<List<V>>(String.format("set<%s>", v.toString()))
{
private final Bijections.Bijection<List<V>> gen = listGen(v.generator(), maxSize);
public Bijections.Bijection<List<V>> generator()
{
return gen;
}
public int maxSize()
{
return Long.BYTES;
}
};
}
public static <V> ColumnSpec.DataType<Set<V>> setColumn(ColumnSpec.DataType<V> v,
int maxSize)
{
return new ColumnSpec.DataType<Set<V>>(String.format("set<%s>", v.toString()))
{
private final Bijections.Bijection<Set<V>> gen = setGen(v.generator(), maxSize);
public Bijections.Bijection<Set<V>> generator()
{
return gen;
}
public int maxSize()
{
return Long.BYTES;
}
};
}
public static <K, V> Bijections.Bijection<Map<K, V>> mapGen(Bijections.Bijection<K> keyGen,
Bijections.Bijection<V> valueGen,
int maxSize)
{
return new MapGenerator<>(keyGen, valueGen, maxSize);
}
public static <V> Bijections.Bijection<List<V>> listGen(Bijections.Bijection<V> valueGen,
int maxSize)
{
return new ListGenerator<>(valueGen, maxSize);
}
public static <V> Bijections.Bijection<Set<V>> setGen(Bijections.Bijection<V> valueGen,
int maxSize)
{
return new SetGenerator<>(valueGen, maxSize);
}
public static class MapGenerator<K, V> implements Bijections.Bijection<Map<K, V>>
{
public final Bijections.Bijection<K> keyGen;
public final Bijections.Bijection<V> valueGen;
public int maxSize;
public MapGenerator(Bijections.Bijection<K> keyGen,
Bijections.Bijection<V> valueGen,
int maxSize)
{
this.keyGen = keyGen;
this.valueGen = valueGen;
this.maxSize = maxSize;
}
public Map<K, V> inflate(long descriptor)
{
long rnd = RngUtils.next(descriptor);
int count = RngUtils.asInt(rnd, 0, maxSize);
Map<K, V> 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<K, V> value)
{
throw new UnsupportedOperationException();
}
public int byteSize()
{
return Long.BYTES;
}
public int compare(long l, long r)
{
throw new UnsupportedOperationException();
}
}
public static class ListGenerator<V> implements Bijections.Bijection<List<V>>
{
public final Bijections.Bijection<V> valueGen;
public int maxSize;
public ListGenerator(Bijections.Bijection<V> valueGen,
int maxSize)
{
this.valueGen = valueGen;
this.maxSize = maxSize;
}
public List<V> inflate(long descriptor)
{
long rnd = RngUtils.next(descriptor);
int count = RngUtils.asInt(rnd, 0, maxSize);
List<V> 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<V> value)
{
throw new UnsupportedOperationException();
}
public int byteSize()
{
return Long.BYTES;
}
public int compare(long l, long r)
{
throw new UnsupportedOperationException();
}
}
public static class SetGenerator<V> implements Bijections.Bijection<Set<V>>
{
public final Bijections.Bijection<V> valueGen;
public int maxSize;
public SetGenerator(Bijections.Bijection<V> valueGen,
int maxSize)
{
this.valueGen = valueGen;
this.maxSize = maxSize;
}
public Set<V> inflate(long descriptor)
{
long rnd = RngUtils.next(descriptor);
int count = RngUtils.asInt(rnd, 0, maxSize);
Set<V> 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<V> value)
{
throw new UnsupportedOperationException();
}
public int byteSize()
{
return Long.BYTES;
}
public int compare(long l, long r)
{
throw new UnsupportedOperationException();
}
}
}

View File

@ -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<ColumnSpec<?>> 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<ColumnSpec<?>> 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<ColumnSpec<?>> 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<ColumnSpec<?>> 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<ColumnSpec<?>> 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<ColumnSpec<?>> 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<Object[]>
{
@VisibleForTesting
public final List<ColumnSpec<?>> columns;
KeyGenerator(List<ColumnSpec<?>> 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<ColumnSpec<?>> 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<ColumnSpec<?>> 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);
}
}
}

View File

@ -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);
}
}

View File

@ -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<T>
{
// 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<T> generate(EntropySource rng, int n)
{
List<T> res = new ArrayList<>(n);
for (int i = 0; i < n; i++)
res.add(generate(rng));
return res;
}
default Surjections.Surjection<T> 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<T>
{
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<T> bind(EntropySource rand)
{
return () -> generate(rand);
}
default <T1> Generator<T1> map(Function<T, T1> map)
{
return (rand) -> map.apply(generate(rand));
}
default <T1> Generator<T1> flatMap(Function<T, Generator<T1>> fmap)
{
return (rand) -> fmap.apply(generate(rand)).generate(rand);
}
default <T2, T3> Generator<T3> zip(Generator<T2> g1, BiFunction<T, T2, T3> map)
{
return (rand) -> map.apply(Generator.this.generate(rand), g1.generate(rand));
}
default <T2, T3, T4> Generator<T4> zip(Generator<T2> g1, Generator<T3> g2, TriFunction<T, T2, T3, T4> map)
{
return (rand) -> map.apply(Generator.this.generate(rand), g1.generate(rand), g2.generate(rand));
}
default <T2, T3, T4, T5> Generator<T5> zip(Generator<T2> g1, Generator<T3> g2, Generator<T4> g3, QuatFunction<T, T2, T3, T4, T5> map)
{
return (rand) -> map.apply(Generator.this.generate(rand), g1.generate(rand), g2.generate(rand), g3.generate(rand));
}
default <T2, T3, T4, T5, T6> Generator<T6> zip(Generator<T2> g1, Generator<T3> g2, Generator<T4> g3, Generator<T5> g4, QuinFunction<T, T2, T3, T4, T5, T6> map)
{
return (rand) -> map.apply(Generator.this.generate(rand), g1.generate(rand), g2.generate(rand), g3.generate(rand), g4.generate(rand));
}
default <T2, T3, T4, T5, T6, T7> Generator<T7> zip(Generator<T2> g1, Generator<T3> g2, Generator<T4> g3, Generator<T5> g4, Generator<T6> g5, SechFunction<T, T2, T3, T4, T5, T6, T7> 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<T1, T2, T3, RES>
{
RES apply(T1 t1, T2 t2, T3 t3);
}
public interface QuatFunction<T1, T2, T3, T4, RES>
{
RES apply(T1 t1, T2 t2, T3 t3, T4 t4);
}
public interface QuinFunction<T1, T2, T3, T4, T5, RES>
{
RES apply(T1 t1, T2 t2, T3 t3, T4 t4, T5 t5);
}
public interface SechFunction<T1, T2, T3, T4, T5, T6, RES>
{
RES apply(T1 t1, T2 t2, T3 t3, T4 t4, T5 t5, T6 t6);
}
}

View File

@ -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 <T> Generator<T> pick(List<T> ts)
{
return (rng) -> {
return ts.get(rng.nextInt(0, ts.size() - 1));
};
}
public static <T> Generator<T> pick(T... ts)
{
return pick(Arrays.asList(ts));
}
public static <T> Generator<List<T>> subsetGenerator(List<T> list)
{
return subsetGenerator(list, 0, list.size() - 1);
}
public static <T> Generator<List<T>> subsetGenerator(List<T> list, int minSize, int maxSize)
{
return (rng) -> {
int count = rng.nextInt(minSize, maxSize);
Set<T> set = new HashSet<>();
for (int i = 0; i < count; i++)
set.add(list.get(rng.nextInt(minSize, maxSize)));
return (List<T>) new ArrayList<>(set);
};
}
public static <T extends Enum<T>> Generator<T> enumValues(Class<T> e)
{
return pick(Arrays.asList(e.getEnumConstants()));
}
public static <T> Generator<List<T>> list(Generator<T> of, int maxSize)
{
return list(of, 0, maxSize);
}
public static <T> Generator<List<T>> list(Generator<T> of, int minSize, int maxSize)
{
return (rng) -> {
int count = rng.nextInt(minSize, maxSize);
return of.generate(rng, count);
};
}
public static <T> Generator<T> constant(T constant)
{
return (random) -> constant;
}
public static <T> Generator<T> constant(Supplier<T> constant)
{
return (random) -> constant.get();
}
}

View File

@ -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<String>
{
public static final int NIBBLES_SIZE = 256;
private final String[] nibbles;
private final Map<String, Integer> 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<String> strings = new TreeSet<String>();
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<String> iter = strings.iterator();
for (int i = 0; i < NIBBLES_SIZE; i++)
nibbles[i] = iter.next();
return nibbles;
}
}

View File

@ -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 <T> Surjection<T> constant(Supplier<T> constant)
{
return (current) -> constant.get();
}
public static <T> Surjection<T> constant(T constant)
{
return (current) -> constant;
}
public static <T> Surjection<T> pick(List<T> ts)
{
return new Surjection<T>()
{
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 <T> Surjection<T> weighted(int[] weights, T... items)
{
return weighted(weights(weights), items);
}
public static <T> Surjection<T> weighted(long[] weights, T... items)
{
assert weights.length == items.length;
Arrays.sort(weights);
TreeMap<Integer, T> weightMap = new TreeMap<Integer, T>();
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 <T> Surjection<T> weighted(Map<T, Integer> weights)
{
TreeMap<Integer, T> weightMap = new TreeMap<Integer, T>();
int sum = 0;
for (Map.Entry<T, Integer> 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 <T> Surjection<T> pick(T... ts)
{
return pick(Arrays.asList(ts));
}
public static <T extends Enum<T>> Surjection<T> enumValues(Class<T> e)
{
return enumValues(e, e.getEnumConstants());
}
public static <T extends Enum<T>> Surjection<T> enumValues(Class<T> klass, T... e)
{
return pick(Arrays.asList(e));
}
public interface Surjection<T>
{
T inflate(long descriptor);
default <T1> Surjection<T1> map(Function<T, T1> map)
{
return (current) -> map.apply(inflate(current));
}
default LongFunction<T> toFn()
{
return new LongFunction<T>()
{
public T apply(long value)
{
return inflate(value);
}
};
}
default Generator<T> toGenerator()
{
return new Generator<T>()
{
public T generate(EntropySource rng)
{
return inflate(rng.next());
}
};
}
@VisibleForTesting
default Supplier<T> toSupplier()
{
EntropySource rng = new PcgRSUFast(0, 0);
return () -> inflate(rng.next());
}
}
}

View File

@ -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);
}
}
}

View File

@ -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();
}
}

View File

@ -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.
* <p>
* Base algorithm translated from C/C++ code:
* https://github.com/imneme/pcg-c
* https://github.com/imneme/pcg-cpp
* <p>
* 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;
}
}

Some files were not shown because too many files have changed in this diff Show More