mirror of https://github.com/apache/cassandra
Bring Harry into C* Tree
Patch by Alex Petrov; reviewed by Caleb Rackliffe and Marcus Eriksson for CASSANDRA-19210.
This commit is contained in:
parent
b757dad43f
commit
439d1b122a
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
|
@ -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"),
|
||||
|
|
|
|||
|
|
@ -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: {}
|
||||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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));
|
||||
}
|
||||
}
|
||||
|
|
@ -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
|
||||
{
|
||||
|
||||
}
|
||||
}
|
||||
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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(() -> {
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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 })
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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() +
|
||||
'}';
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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));
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
{
|
||||
|
|
|
|||
|
|
@ -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());
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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;
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
@ -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);
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
@ -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
|
||||
{
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -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));
|
||||
}
|
||||
}
|
||||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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());
|
||||
}
|
||||
}
|
||||
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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())));
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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 }
|
||||
}
|
||||
|
|
@ -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));
|
||||
}
|
||||
}
|
||||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
@ -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)
|
||||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -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)
|
||||
|
|
@ -0,0 +1,647 @@
|
|||
# Harry, a fuzz testing tool for Apache Cassandra
|
||||
|
||||
The project aims to generate _reproducible_ workloads that are as close to real-life as possible, while being able to
|
||||
_efficiently_ verify the cluster state against the model without pausing the workload itself.
|
||||
|
||||
## Getting Started in under 5 minutes
|
||||
|
||||
Harry can operate as a straightforward read/write "correctness stress tool" that will check to ensure reads on a cluster
|
||||
are consistent with what it knows it wrote to the cluster. You have a couple options for this.
|
||||
|
||||
### Option 2: Running things manually lower in the stack:
|
||||
|
||||
To start a workload that performs a concurrent read/write workload, 2 read and 2 write threads for 60 seconds
|
||||
against a in-jvm cluster you can use the following code:
|
||||
|
||||
```
|
||||
try (Cluster cluster = builder().withNodes(3)
|
||||
.start())
|
||||
{
|
||||
SchemaSpec schema = new SchemaSpec("harry", "test_table",
|
||||
asList(pk("pk1", asciiType), pk("pk1", int64Type)),
|
||||
asList(ck("ck1", asciiType), ck("ck1", int64Type)),
|
||||
asList(regularColumn("regular1", asciiType), regularColumn("regular1", int64Type)),
|
||||
asList(staticColumn("static1", asciiType), staticColumn("static1", int64Type)));
|
||||
|
||||
Configuration config = HarryHelper.defaultConfiguration()
|
||||
.setKeyspaceDdl(String.format("CREATE KEYSPACE IF NOT EXISTS %s WITH replication = {'class': 'SimpleStrategy', 'replication_factor': %d};", schema.keyspace, 3))
|
||||
.setSUT(() -> new InJvmSut(cluster))
|
||||
.build();
|
||||
|
||||
Run run = config.createRun();
|
||||
|
||||
concurrent(run, config,
|
||||
asList(pool("Writer", 2, MutatingVisitor::new),
|
||||
pool("Reader", 2, RandomPartitionValidator::new)),
|
||||
2, TimeUnit.MINUTES)
|
||||
.run();
|
||||
}
|
||||
```
|
||||
|
||||
# I've found a falsification. What now?
|
||||
|
||||
There is no one-size-fits-all solution for debugging a falsification. We did try to create a shrinker, but unfortunately
|
||||
without Simulator, shrinker only works for issues that are non-concurrent in nature, since there's no way to create a
|
||||
stable repro otherwise. That said, there are several things that might get you started and inspire further ideas about
|
||||
how to debug the issue.
|
||||
|
||||
First of all, understand whether or not the issue is likely to be concurrent in nature. If you re-run your test with the
|
||||
same seed, but see no falsification, and it fails only sporadically, and often on different logical timestamp, it is
|
||||
likely that the issue is, in fact concurrent. Here, it is important to note that when you are running concurrent
|
||||
read/write workload, you will get different interleaving of reads and writes every time you do this.
|
||||
|
||||
If you can get a stable repro with a sequential runner, you're in luck. Now all you need to do is to add logs everywhere
|
||||
and understand what might be causing it. But even if you do not have a stable repro, you are still likely to follow the
|
||||
same steps:
|
||||
|
||||
* Inspect the error itself. Do Cassandra-returned results make sense? Is anything out of order? Are there any duplicates
|
||||
or gaps?
|
||||
* Switch to logging mutating visitor and closely inspect its output. Closely inspect the output of the model. Do the
|
||||
values make sense?
|
||||
* Check the output of data tracker. Does the model or Cassandra have missing columns or rows? Do these outputs contain
|
||||
latest logical timestamps for each of the operations from the log? How about in-flight operations?
|
||||
* Filter out relevant operation log entries and inspect them closely. Given these operations, does the output of the
|
||||
model, or output of the database make most sense?
|
||||
|
||||
Next, you might want to try to narrow down the scope of the problem. Depending on what the falsification looks like, use
|
||||
your Cassandra knowledge to see what might apply in your situation:
|
||||
|
||||
* Try checking if changing schema to use different column types does anything.
|
||||
* Try disabling range deletes, regular deletes, or column deletes.
|
||||
* Try changing the size of partition and see if the issue still reproduces.
|
||||
* Try disabling static columns.
|
||||
|
||||
To avoid listing every feature in Harry, it suffices to say you should try to enable/disable features that make sense
|
||||
in the given context, and try to find the combination that avoids the failure, or a minimal combination that still
|
||||
reproduces the issue. Your first goal should be to find a _stable repro_, even if it involves modifying Cassandra or
|
||||
Harry, or taking the operations, and composing the repro manually. Having a stable repro will make finding a cause much
|
||||
simpler. Sometimes you will find the cause before you have a stable repro, in which case, you _still_ have to produce a
|
||||
stable repro to make things simpler for the reviewer, and to include it into the test suite of your patch.
|
||||
|
||||
Lastly, *be patient*. Debugging falsifications is often a multi-hour endeavour, and things do not always jump out at you,
|
||||
so you might have to spend a significant amount of time tracking the problem down. Once you have found it, it is very
|
||||
rewarding.
|
||||
|
||||
## Further Reading
|
||||
* [Harry: An open-source fuzz testing and verification tool for Apache Cassandra](https://cassandra.apache.org/_/blog/Harry-an-Open-Source-Fuzz-Testing-and-Verification-Tool-for-Apache-Cassandra.html)
|
||||
|
||||
---
|
||||
# Technical and Implementation Details
|
||||
|
||||
## System Under Test implementations
|
||||
|
||||
* `in_jvm/InJvmSut` - simple in-JVM-dtest system under test.
|
||||
* `println/PrintlnSut` - system under test that prints to sdtout instead of executing queries on the cluster; useful for
|
||||
debugging.
|
||||
* `mixed_in_jvm/MixedVersionInJvmSut` - in-JVM-dtest system under test that works with mixed version clusters.
|
||||
* `external/ExternalClusterSut` - system under test that works with CCM, Docker, Kubernetes, or cluster you may. have
|
||||
deployed elsewhere
|
||||
|
||||
Both in-JVM SUTs have fault-injecting functionality available.
|
||||
|
||||
## Visitors
|
||||
|
||||
* `single/SingleValidator` - visitor that runs several different read queries against a single partition that is
|
||||
associated with current logical timestamp, and validates their results using given model.
|
||||
* `all_partitions/AllPartitionsValidator` - concurrently validates all partitions that were visited during this run.
|
||||
* `repair_and_validate_local_states/RepairingLocalStateValidator` - similar to `AllPartitionsValidator`, but performs
|
||||
repair before checking node states.
|
||||
* `mutating/MutatingVisitor` - visitor that performs all sorts of mutations.
|
||||
* `logging/LoggingVisitor` - similar to `MutatingVisitor`, but also logs all operations to a file; useful for debug
|
||||
purposes.
|
||||
* `corrupting/CorruptingVisitor` - visitor that will deliberately change data in the partition it visits. Useful for
|
||||
negative tests (i.e. to ensure that your model actually detects data inconsistencies).
|
||||
|
||||
And more.
|
||||
|
||||
## Models
|
||||
|
||||
* `querying_no_op/QueryingNoOpValidator` - a model that can be used to "simply" run random queries.
|
||||
* `quiescent_checker/QuiescentChecker` - a model that can be used to verify results of any read that has no writes to
|
||||
the same partition_ concurrent to it. Should be used in conjunction with locking data tracker.
|
||||
* `quiescent_local_state_checker/QuiescentLocalStateChecker` - a model that can check local states of each replica that
|
||||
has to own
|
||||
|
||||
## Runners
|
||||
|
||||
* `sequential/SequentialRunner` - runs all visitors sequentially, in the loop, for a specified amount of time; useful
|
||||
for simple tests that do not have to exercise concurrent read/write path.
|
||||
* `concurrent/ConcurrentRunner` - runs all visitors concurrently, each visitor in its own thread, looped, for a
|
||||
specified amount of time; useful for things like concurrent read/write workloads.
|
||||
* `chain/ChainRunner` - receives other runners as input, and runs them one after another once. Useful for both simple
|
||||
and complex scenarios that involve both read/write workloads, validating all partitions, exercising other node-local
|
||||
or cluster-wide operations.
|
||||
* `staged/StagedRunner` - receives other runners (stages) as input, and runs them one after another in a loop; useful
|
||||
for implementing complex scenarios, such as read/write workloads followed by some cluster changing operations.
|
||||
|
||||
## Clock
|
||||
|
||||
* `approximate_monotonic/ApproximateMonotonicClock` - a timestamp supplier implementation that tries to keep as close to
|
||||
real time as possible, while preserving mapping from real-time to logical timestamps.
|
||||
* `offset/OffsetClock` - a (monotonic) clock that supplies timestamps that do not have any relation to real time.
|
||||
|
||||
# Introduction
|
||||
|
||||
Harry has two primary modes of functionality:
|
||||
|
||||
* Unit test mode: in which you define specific sequences of
|
||||
operations and let Harry test these operations using different
|
||||
schemas and conditions.
|
||||
* Exploratory/fuzz mode: in which you define distributions of events
|
||||
rather rather than sequences themselves, and let Harry try out
|
||||
different things.
|
||||
|
||||
Usually, in unit-test mode, we’re applying several write operations to the cluster state and then run different read
|
||||
queries and validate their results. To learn more about writing unit tests, refer to the "Writing Unit Tests" section.
|
||||
|
||||
In exploratory mode, we continuously apply write operations to the cluster and validate their state, allowing data size
|
||||
to grow and simulating real-life behaviour. To learn more about implementing test cases using fuzz mode, refer to the "
|
||||
Implementing Tests" section of this guide, but it's likely you'll have to read the rest of this document to implement
|
||||
more complex scenarios.
|
||||
|
||||
# Writing Unit Tests
|
||||
|
||||
To write unit tests with Harry, there's no special knowledge required. Usually, unit tests are written by simply
|
||||
hardcoding the schema and then writing several modification statements one after the other, and then manually validating
|
||||
results of a `SELECT` query. This might work for simple scenarios, but there’s still a chance that for some other schema
|
||||
or some combination of values the tested feature may not work.
|
||||
|
||||
To improve the situation, we can express the test in more abstract terms and, instead of writing it using specific
|
||||
statements, we can describe which statement _types_ are to be used:
|
||||
|
||||
```
|
||||
test(new SchemaGenerators.Builder("harry")
|
||||
.partitionKeySpec(1, 5)
|
||||
.clusteringKeySpec(1, 5)
|
||||
.regularColumnSpec(1, 10)
|
||||
.generator(),
|
||||
historyBuilder -> {
|
||||
historyBuilder.insert();
|
||||
historyBuilder.deletePartition();
|
||||
historyBuilder.deleteRowSlice();
|
||||
});
|
||||
```
|
||||
|
||||
This spec can be used to generate clusters of different sizes, configured with different schemas, executing the given a
|
||||
sequence of actions both in isolation, and combined with other randomly generated ones, with failure-injection.
|
||||
|
||||
Best of all is that this test will _not only_ ensure that such a sequence of actions does not produce an exception, but
|
||||
also would ensure that cluster will respond with correct results to _any_ allowed read query.
|
||||
|
||||
To begin specifying operations for a new partition, either start calling methods on the `HistoryBuilder`, or, if you
|
||||
would like to specify the partition which Harry needs to visit use `#visitPartition` or `#beginBatch` have to be called,
|
||||
for starting a visit to a particular partition with a single or multiple actions.
|
||||
|
||||
After that, the actions are self-explanatory: `#insert`, `#update`, `#deleteRow`, `#deleteColumns`, `#deleteRowRange`, `#deleteRowSlice`
|
||||
`#deletePartition`.
|
||||
|
||||
After history generated by `HistoryBuilder` is replayed using `ReplayingVisitor` (or by using a `ReplayingHistoryBuilder`
|
||||
which combines the two for your convenience), you can use any model (`QuiescentChecker` by default) to validate queries.
|
||||
Queries can be provided manually or generated using `QueryGenerator` or `TypedQueryGenerator`.
|
||||
|
||||
# Basic Terminology
|
||||
|
||||
* Inflate / inflatable: a process of producing a value (for example, string, or a blob) from a `long` descriptor that
|
||||
uniquely identifies the value. See data generation section of this guide for more details.
|
||||
* Deflate / deflatable: a process of producing the descriptor the value was inflated from during verification.
|
||||
See model section of this guide for more details.
|
||||
|
||||
For definitions of logical timestamp, descriptor, and other entities used during inflation and deflation, refer
|
||||
to formal relationships section.
|
||||
|
||||
# Features
|
||||
|
||||
Currently, Harry can exercise the following Cassandra functionality:
|
||||
|
||||
* Supported data types: `int8`, `int16`, `int32`, `int64`, `boolean`, `float`, `double`, `ascii`, `uuid`, `timestamp`.
|
||||
Collections are only _inflatable_.
|
||||
* Random schema generation, with an arbitrary number of partition and clustering keys.
|
||||
* Schemas with arbitrary `CLUSTERING ORDER BY`
|
||||
* Randomly generated `INSERT` and `UPDATE` queries with all columns or arbitrary column subset
|
||||
* Randomly generated `DELETE` queries: for a single column, single row, or a range of rows
|
||||
* Inflating and validating entire partitions (with allowed in-flight queries)
|
||||
* Inflating and validating random `SELECT` queries: single row, slices (with single open end), and ranges (with both
|
||||
ends of clusterings specified)
|
||||
|
||||
Inflating partitions is done using `Reconciler`. Validating partitions and random queries can be done using `QuiescentChecker`.
|
||||
|
||||
## Outstanding Work
|
||||
|
||||
#### The following have not yet been implemented:
|
||||
|
||||
* Some types (such as collections) are not deflatable
|
||||
* 2i queries are not implemented
|
||||
* Fault injection is not implemented (available via Cassandra Simulator)
|
||||
* TTL is not supported
|
||||
* Some SELECT queries are not supported: `LIMIT`, `IN`, `GROUP BY`, token range queries
|
||||
|
||||
#### Some features can be improved upon or further optimized:
|
||||
|
||||
* Pagination is currently implemented but hard-coded to a page size of 1
|
||||
* RNG should be able to yield less than 64 bits of entropy per step
|
||||
* State tracking should be done in a compact off-heap data structure
|
||||
* Inflated partition state and per-row operation log should be done in a compact
|
||||
off-heap data structure
|
||||
* Decision-making about _when_ we visit partitions and/or rows should be improved
|
||||
|
||||
While this list of improvements is incomplete, t should give the reader a rough idea about the state of the project.
|
||||
The original goal of the project was to drive to stability after the significant storage engine rewrite in
|
||||
CASSANDRA-8099 and help remove data loss bugs from the codebase before they got out into the wild. Next steps are to
|
||||
integrate it into both CI and into regular daily dev workflows.
|
||||
|
||||
# Goals: Reproducibility and Efficiency
|
||||
|
||||
_Reproducibility_ is achieved by using the PCG family of random number generators and generating schema, configuration,
|
||||
and every step of the workload from the repeatable sequence of random numbers. Schema and configuration are generated
|
||||
from the _seed_. Each operation is assigned its own monotonically increasing _logical timestamp_, which preserves
|
||||
logical operation order between different runs.
|
||||
|
||||
_Efficiency_ is achieved by employing the features of the PCG random number generator (walking the sequence of random
|
||||
numbers back and forth), and writing value generators in a way that preserves properties of the descriptor it was
|
||||
generated from.
|
||||
|
||||
Given a `long` _descriptor_ can be _inflated_ into some value:
|
||||
|
||||
* value can be _deflated_ back to the descriptor it was generated from (in other words, generation is *invertible*)
|
||||
* two inflated values will sort the same way as two descriptors they were generated from (in other words, generation is
|
||||
*order-preserving*)
|
||||
|
||||
These properties are also preserved for the composite values, such as clustering and partition keys.
|
||||
|
||||
# Components
|
||||
|
||||
Every Harry run starts from Configuration. You can find an example configuration under `conf/example.yml`.
|
||||
|
||||
*Clock* is a component responsible for mapping _logical_ timestamps to _real-time_ ones. When reproducing test failures,
|
||||
and for validation purposes, a snapshot of such be taken to map a real-time timestamp from the value retrieved from the
|
||||
database to map it back to the logical timestamp of the operation that wrote this value. Given a real-time timestamp,
|
||||
the clock can return a logical timestamp, and vice versa.
|
||||
|
||||
*Runner* is a component that schedules operations that change the cluster (system under test) and model state.
|
||||
|
||||
*System under test*: a Cassandra node or cluster. Default implementation is in_jvm (in-JVM DTest cluster). Harry also
|
||||
supports external clusters.
|
||||
|
||||
*Model* is responsible for tracking logical timestamps that system under test was notified about.
|
||||
|
||||
*Partition descriptor selector* controls how partitions are selected based on the current logical timestamp. The default
|
||||
implementation is a sliding window of partition descriptors that will visit one partition after the other in the
|
||||
window `slide_after_repeats` times. After that, it will retire one partition descriptor, and pick a new one.
|
||||
|
||||
*Clustering descriptor selector* controls how clustering keys are picked within the partition: how many rows there can
|
||||
be in a partition, how many rows are visited for a logical timestamp, how many operations there will be in batch, what
|
||||
kind of operations there will and how often each kind of operation is going to occur.
|
||||
|
||||
# Implementing Tests
|
||||
|
||||
All Harry components are pluggable and can be redefined. However, many of the default implementations will cover most of
|
||||
the use-cases, so in this guide we’ll focus on ones that are most often used to implement different use cases:
|
||||
|
||||
* System Under Test: defines how Harry can communicate with Cassandra instances and issue common queries. Examples of a
|
||||
system under test can be a CCM cluster, a “real” Cassandra cluster, or an in-JVM dtest cluster.
|
||||
* Visitor: defines behaviour that gets triggered at a specific logical timestamp. One of the default implementations is
|
||||
MutatingVisitor, which executes write workload against SystemUnderTest. Examples of a visitor, besides a mutating
|
||||
visitor, could be a validator that uses the model to validate results of different queries, a repair runner, or a
|
||||
fault injector.
|
||||
* Model: validates results of read queries by comparing its own internal representation against the results returned by
|
||||
system under test. You can find three simplified implementations of model in this document: Visible Rows Checker,
|
||||
Quiescent Checker, and an Exhaustive Checker.
|
||||
* Runner: defines how operations defined by visitors are executed. Harry includes two default implementations: a
|
||||
sequential and a concurrent runner. Sequential runner allows no overlap between different visitors or logical
|
||||
timestamps. Concurrent runner allows visitors for different timestamps to overlap.
|
||||
|
||||
System under test is the simplest one to implement: you only need a way to execute Cassandra queries. At the moment of
|
||||
writing, all custom things, such as nodetool commands, failure injection, etc, are implemented using a SUT / visitor
|
||||
combo: visitor knows about internals of the cluster it is dealing with.
|
||||
|
||||
Generally, visitor has to follow the rules specified by DescriptorSelector and PdSelector: (it can only visit issue
|
||||
mutations against the partition that PdSelector has picked for this LTS), and DescriptorSelector (it can visit exactly
|
||||
DescriptorSelector#numberOfModifications rows within this partition, operations have to have a type specified by
|
||||
#operationKind, clustering and value descriptors have to be in accordance with DescriptorSelector#cd and
|
||||
DescriptorSelector#vds). The reason for these limitations is because model has to be able to reproduce the exact
|
||||
sequence of events that was applied to system under test.
|
||||
|
||||
Default implementations of partition and clustering descriptors, used in fuzz mode allow to optimise verification. For
|
||||
example, it is possible to go find logical timestamps that are visiting the same partition as any given logical
|
||||
timestamp. When running Harry in unit-test mode, we use a special generating visitor that keeps an entire given sequence
|
||||
of events in memory rather than producing it on the fly. For reasons of efficiency, we do not use generating visitors
|
||||
for generating and verifying large datasets.
|
||||
|
||||
# Formal Relations Between Entities
|
||||
|
||||
To be able to implement efficient models, we had to reduce the amount of state required for validation to a minimum and
|
||||
try to operate on primitive data values most of the time. Any Harry run starts with a `seed`. Given the same
|
||||
configuration, and the same seed, we're able to make runs deterministic (in other words, records in two clusters created
|
||||
from the same seed are going to have different real-time timestamps, but will be otherwise identical; logical time
|
||||
stamps will also be identical).
|
||||
|
||||
Since it's clear how to generate things like random schemas, cluster configurations, etc., let's discuss how we're
|
||||
generating data, and why this type of generation makes validation efficient.
|
||||
|
||||
First, we're using PCG family of random number generators, which, besides having nice characteristics that any RNG
|
||||
should have, have two important features:
|
||||
|
||||
* Streams: for single seed, we can have several independent _different_ streams of random numbers.
|
||||
* Walkability: PCG generators generate a stream of numbers you can walk _back_ and _forth_. That is, for any number _n_
|
||||
that represents a _position_ of the random number in the stream of random numbers, we can get the random number at
|
||||
this position. Conversely, given a random number, we can determine what is its position in the stream. Moreover,
|
||||
knowing a random number, we can determine which number precedes it in the stream of random numbers, and, finally, we
|
||||
can determine how many numbers there are in a stream between the two random numbers.
|
||||
|
||||
Out of these operations, determining the _next_ random number in the sequence can be done in constant time, `O(1)`.
|
||||
Advancing generator by _n_ steps can be done in `O(log(n))` steps. Since generation is cyclical, advancing the iterator
|
||||
backward is equivalent to advancing it by `cardinality - 1` steps. If we're generating 64 bits of entropy, advancing
|
||||
by `-1` can be done in 64 steps.
|
||||
|
||||
Let's introduce some definitions:
|
||||
|
||||
* `lts` is a *logical timestamp*, an entity (number in our case), given by the clock, on which some action occurs
|
||||
* `m` is a *modification id*, a sequential number of the modification that occurs on `lts`
|
||||
* `rts` is an approximate real-time as of clock for this run
|
||||
* `pid` is a partition position, a number between `0` and N, for `N` unique generated partitions
|
||||
* `pd` is a partition descriptor, a unique descriptor identifying the partition
|
||||
* `cd` is a clustering descriptor, a unique descriptor identifying row within some partition
|
||||
|
||||
A small introduction that can help to understand the relation between these
|
||||
entities. Hierarchically, the generation process looks as follows:
|
||||
|
||||
* `lts` is an entry point, from which the decision process starts
|
||||
* `pd` is picked from `lts`, and determines which partition is going to be visited
|
||||
* for `(pd, lts)` combination, `#mods` (the number of modification batches) and `#rows` (the number of rows per
|
||||
modification batch) is determined. `m` is an index of the modification batch, and `i` is an index of the operation in
|
||||
the modification batch.
|
||||
* `cd` is picked based on `(pd, lts)`, and `n`, a sequential number of the operation among all modification batches
|
||||
* operation type (whether we're going to perform a write, delete, range delete, etc), columns involved in this
|
||||
operation, and values for the modification are picked depending on the `pd`, `lts`, `m`, and `i`
|
||||
|
||||
Most of this formalization is implemented in `OpSelectors`, and is relied upon in`PartitionVisitor` and any
|
||||
implementation of a `Model`.
|
||||
|
||||
Random number generation (see `OpSelectors#Rng`):
|
||||
|
||||
* `rng(i, stream[, e])`: returns i'th number drawn from random sequence `stream` producing values with `e` bits of
|
||||
entropy (64 bits for now).
|
||||
* `rng'(s, stream[, e])`: returns `i` of the random number `s` drawn from the random sequence `stream`. This function is
|
||||
an inverse of `rng`.
|
||||
* `next(rnd, stream[, e])` and `prev(rnd, stream[, e])`: the next/previous number relative to `rnd` drawn from random
|
||||
sequence `stream`.
|
||||
|
||||
A simple example of a partition descriptor selector is one that is based on a sliding window of a size `s`, that slides
|
||||
every `n` iterations. First, we determine _where_ the window should start for a given `lts` (in other words, how many
|
||||
times it has already slid). After that, we determine which `pd` we pick out of `s` available ones. After picking each
|
||||
one of the `s` descriptors `n` times, we retire the oldest descriptor and pick a new one to the window. Window start and
|
||||
offset are then used as input for the `rng(start + offset, stream)` to make sure descriptors are uniformly distributed.
|
||||
|
||||
We can build a clustering descriptor selector in a similar manner. Each partition will use its `pd` as a stream id, and
|
||||
pick `cd` from a universe of possible `cds` of size `#cds`. On each `lts`, we pick a random `offset`, and start
|
||||
picking `#ops` clusterings from this `offset < #cds`, and wrap around to index 0 after that. This way, each operation
|
||||
maps to a unique `cd`, and `#op` can be determined from `cd` deterministically.
|
||||
|
||||
# Data Generation
|
||||
|
||||
So far, we have established how to generate partition, clustering, and value _descriptors_. Now, we need to understand
|
||||
how we can generate data modification statements out of these descriptors in a way that helps us to validate data later.
|
||||
|
||||
Since every run has a predefined schema, and by the time we visit a partition we have a logical timestamp, we can make
|
||||
the rest of the decisions: pick a number of batches we're about to perform, determine what kind of operations each one
|
||||
of the batches is going to contain, which rows we're going to visit (clustering for each modification operation).
|
||||
|
||||
To generate a write, we need to know _which partition_ we're going to visit (in other words, partition descriptor),
|
||||
_which row_ we'd like to modify (in other words, clustering descriptor), _which columns_ we're modifying (in other
|
||||
words, a column mask), and, for each modified column - its value. By the time we're ready to make an actual query to the
|
||||
database, we already know `pd`, `cd`, `rts`, and `vds[]`, which is all we need to "inflate" a write.
|
||||
|
||||
To inflate each value descriptor, we take a generator for its datatype, and turn its descriptor into the object. This
|
||||
generation process has the following important properties:
|
||||
|
||||
* it is invertible: for every `inflate(vd) -> value`, there's `deflate(value) -> vd`
|
||||
* it is order-preserving: `compare(vd1, vd2) == compare(inflate(vd1), inflate(vd2))`
|
||||
|
||||
Inflating `pd` and `cd` is slightly more involved than inflating `vds`, since partition and clustering keys are often
|
||||
composite. This means that `inflate(pd)` returns an array of objects, rather just a single
|
||||
object: `inflate(pd) -> value[]`, and `deflate(value[]) -> pd`. Just like inflating value descriptors, inflating keys
|
||||
preserves order.
|
||||
|
||||
It is easy to see that, given two modifications: `Update(pd1, cd1, [vd1_1, vd2_1, vd3_1], lts1)`
|
||||
and `Update(pd1, cd1, [vd1_2, vd3_2], lts2)`, we will end up with a resultset that contains effects of both
|
||||
operations: `ResultSetRow(pd1, cd1, [vd1_2@rts2, vd2_1@rts1, vd3_2@rts2])`.
|
||||
|
||||
# Model
|
||||
|
||||
`Model` in Harry ties the rest of the components together and allows us to check whether or not data returned by the
|
||||
cluster actually makes sense. The model relies on the clock, since we have to convert real-time timestamps of the
|
||||
returned values back to logical timestamps, and on descriptor selectors to pick the right partition and rows.
|
||||
|
||||
## Visible Rows Checker
|
||||
|
||||
Let's try to put it all together and build a simple model. The simplest one is a visible row checker. It can check if
|
||||
any row in the response returned from the database could have been produced by one of the operations. However, it won't
|
||||
be able to find errors related to missing rows, and will only notice some cases of erroneously overwritten rows.
|
||||
|
||||
In the model, we can see a response from the database in its deflated state. In other words, instead of the actual
|
||||
values returned, we see their descriptors. Every resultset row consists of `pd`, `cd`, `vds[]` (value descriptors),
|
||||
and `lts[]` (logical timestamps at which these values were written).
|
||||
|
||||
To validate, we need to iterate through all operations for this partition, starting with the latest one the model is
|
||||
aware of. This model has no internal state, and validates entire partitions:
|
||||
|
||||
```
|
||||
void validatePartitionState(long validationLts, List<ResultSetRow> rows) {
|
||||
long pd = pdSelector.pd(validationLts, schema);
|
||||
|
||||
for (ResultSetRow row : rows) {
|
||||
// iterator that gives us unique lts from the row in descending order
|
||||
LongIterator rowLtsIter = descendingIterator(row.lts);
|
||||
// iterator that gives us unique lts from the model in descending order
|
||||
LongIterator modelLtsIter = descendingIterator(pdSelector, validationLts);
|
||||
|
||||
outer:
|
||||
while (rowLtsIter.hasNext()) {
|
||||
long rowLts = rowLtsIter.nextLong();
|
||||
|
||||
// this model can not check columns whose values were never written or were deleted
|
||||
if (rowLts == NO_TIMESTAMP)
|
||||
continue outer;
|
||||
|
||||
if (!modelLtsIter.hasNext())
|
||||
throw new ValidationException(String.format("Model iterator is exhausted, could not verify %d lts for the row: \n%s %s",
|
||||
rowLts, row));
|
||||
|
||||
while (modelLtsIter.hasNext()) {
|
||||
long modelLts = modelLtsIter.nextLong();
|
||||
// column was written by the operation that has a lower lts than the current one from the model
|
||||
if (modelLts > rowLts)
|
||||
continue;
|
||||
// column was written by the operation that has a higher lts, which contradicts to the model, since otherwise we'd validate it by now
|
||||
if (modelLts < rowLts)
|
||||
throw new RuntimeException("Can't find a corresponding event id in the model for: " + rowLts + " " + modelLts);
|
||||
|
||||
// Compare values for columns that were supposed to be written with this lts
|
||||
for (int col = 0; col < row.lts.length; col++) {
|
||||
if (row.lts[col] != rowLts)
|
||||
continue;
|
||||
|
||||
long m = descriptorSelector.modificationId(pd, row.cd, rowLts, row.vds[col], col);
|
||||
long vd = descriptorSelector.vd(pd, row.cd, rowLts, m, col);
|
||||
|
||||
// If the value model predicts doesn't match the one received from the database, throw an exception
|
||||
if (vd != row.vds[col])
|
||||
throw new RuntimeException("Returned value doesn't match the model");
|
||||
}
|
||||
continue outer;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
As you can see, all validation is done using deflated `ResultSetRows`, which contain enough data to say which logical
|
||||
timestamp each value was written with, and which value descriptor each value has. This model can also validate data
|
||||
concurrently to the ongoing data modification operations.
|
||||
|
||||
## Quiescent Checker
|
||||
|
||||
Let's consider one more checker. It'll be more powerful than the visible rows checker in one way since it can find any
|
||||
inconsistency in data (incorrect timestamp, missing or additional row, rows coming in the wrong order, etc), but it'll
|
||||
also have one limitation: it won't be able to run concurrently with data modification statements. This means that for
|
||||
this model to be used, we should have no _in-flight_ queries, and all queries have to be in a deterministic state by the
|
||||
time we're validating their results.
|
||||
|
||||
For this checker, we assume that we have a component that is called `Reconciler`, which can inflate partition state _up
|
||||
to some_ `lts`. `Reconciler` works by simply applying each modification in the same order they were applied to the
|
||||
cluster, and using standard Cassandra data reconciliation rules (last write wins / DELETE wins over INSERT in case of a
|
||||
timestamp collision).
|
||||
|
||||
With this component, and knowing that there can be no in-fight queries, we can validate data in the following way:
|
||||
|
||||
```
|
||||
public void validatePartitionState(Iterator<ResultSetRow> actual, Query query) {
|
||||
// find out up the highest completed logical timestamp
|
||||
long maxCompleteLts = tracker.maxComplete();
|
||||
|
||||
// get the expected state from reconciler
|
||||
Iterator<Reconciler.RowState> expected = reconciler.inflatePartitionState(query.pd, maxCompleteLts, query).iterator(query.reverse);
|
||||
|
||||
// compare actual and expected rows one-by-one in-order
|
||||
while (actual.hasNext() && expected.hasNext()) {
|
||||
ResultSetRow actualRowState = actual.next();
|
||||
Reconciler.RowState expectedRowState = expected.next();
|
||||
|
||||
if (actualRowState.cd != expectedRowState.cd)
|
||||
throw new ValidationException("Found a row in the model that is not present in the resultset:\nExpected: %s\nActual: %s",
|
||||
expectedRowState, actualRowState);
|
||||
|
||||
if (!Arrays.equals(actualRowState.vds, expectedRowState.vds))
|
||||
throw new ValidationException("Returned row state doesn't match the one predicted by the model:\nExpected: %s (%s)\nActual: %s (%s).",
|
||||
Arrays.toString(expectedRowState.vds), expectedRowState,
|
||||
Arrays.toString(actualRowState.vds), actualRowState);
|
||||
|
||||
if (!Arrays.equals(actualRowState.lts, expectedRowState.lts))
|
||||
throw new ValidationException("Timestamps in the row state don't match ones predicted by the model:\nExpected: %s (%s)\nActual: %s (%s).",
|
||||
Arrays.toString(expectedRowState.lts), expectedRowState,
|
||||
Arrays.toString(actualRowState.lts), actualRowState);
|
||||
}
|
||||
|
||||
if (actual.hasNext() || expected.hasNext()) {
|
||||
throw new ValidationException("Expected results to have the same number of results, but %s result iterator has more results",
|
||||
actual.hasNext() ? "actual" : "expected");
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
If there's any mismatch, it'll be caught right away: if there's an extra row
|
||||
(for example, there were issues in Cassandra that caused it to have duplicate
|
||||
rows), or if some row or even value in the row is missing.
|
||||
|
||||
## Exhaustive Checker
|
||||
|
||||
To be able to both run validation concurrently to modifications and be able to
|
||||
catch all kinds of inconsistencies, we need a more involved checker.
|
||||
|
||||
In this checker, we rely on inflating partition state. However, we're most
|
||||
interested in `lts`, `opId`, and visibility (whether or not it is still
|
||||
in-flight) of each modification operation. To be able to give a reliable result,
|
||||
we need to make sure we follow these rules:
|
||||
|
||||
* every operation model _thinks_ should be visible, has to be visible
|
||||
* every operation model _thinks_ should be invisible, has to be invisible
|
||||
* every operation model doesn't know the state of (i.e., it is still
|
||||
in-flight) can be _either_ visible _invisible_
|
||||
* there can be no state in the database that model is not aware of (in other words,
|
||||
we either can _explain_ how a row came to be, or we conclude that the row is
|
||||
erroneous)
|
||||
|
||||
A naive way to do this would be to inflate every possible partition state, where
|
||||
every in-flight operation would be either visible or invisible, but this gets
|
||||
costly very quickly since the number of possible combinations grows
|
||||
exponentially. A better (and simpler) way to do this is to iterate all
|
||||
operations and keep the state of "explained" operations:
|
||||
|
||||
```
|
||||
public class RowValidationState {
|
||||
// every column starts in UNOBSERVED, and has to move to either REMOVED, or OBSERVED state
|
||||
private final ColumnState[] columnStates;
|
||||
// keep track of operations related to each column state
|
||||
private final Operation[] causingOperations;
|
||||
}
|
||||
```
|
||||
|
||||
Now, we move through all operations for a given row, starting from the _newest_ ones, towards
|
||||
the oldest ones:
|
||||
|
||||
```
|
||||
public void validatePartitionState(long verificationLts, PeekingIterator<ResultSetRow> actual_, Query query) {
|
||||
// get a list of operations for each cd
|
||||
NavigableMap<Long, List<Operation>> operations = inflatePartitionState(query);
|
||||
|
||||
for (Map.Entry<Long, List<Operation>> entry : operations.entrySet()) {
|
||||
long cd = entry.getKey();
|
||||
List<Operation> ops = entry.getValue();
|
||||
|
||||
// Found a row that is present both in the model and in the resultset
|
||||
if (actual.hasNext() && actual.peek().cd == cd) {
|
||||
validateRow(new RowValidationState(actual.next), operations);
|
||||
} else {
|
||||
validateNoRow(cd, operations);
|
||||
|
||||
// Row is not present in the resultset, and we currently look at modifications with a clustering past it
|
||||
if (actual.hasNext() && cmp.compare(actual.peek().cd, cd) < 0)
|
||||
throw new ValidationException("Couldn't find a corresponding explanation for the row in the model");
|
||||
}
|
||||
}
|
||||
|
||||
// if there are more rows in the resultset, and we don't have model explanation for them, we've found an issue
|
||||
if (actual.hasNext())
|
||||
throw new ValidationException("Observed unvalidated rows");
|
||||
}
|
||||
```
|
||||
|
||||
Now, we have to implement `validateRow` and `validateNoRow`. `validateNoRow` is easy: we only need to make sure that a
|
||||
set of operations results in an invisible row. Since we're iterating operations in reverse order, if we encounter a
|
||||
delete not followed by any writes, we can conclude that the row is invisible and exit early. If there's a write that is
|
||||
not followed by a delete, and the row isn't covered by a range tombstone, we know it's an error.
|
||||
|
||||
`validateRow` only has to iterate operations in reverse order until it can explain the value in every column. For
|
||||
example, if a value is `UNOBSERVED`, and the first thing we encounter is a `DELETE` that removes this column, we only
|
||||
need to make sure that the value is actually `null`, in which case we can conclude that the value can be explained
|
||||
as `REMOVED`.
|
||||
|
||||
Similarly, if we encounter an operation that has written the expected value, we conclude that the value is `OBSERVED`.
|
||||
If there are any seeming inconsistencies between the model and resultset, we have to check whether or not the operation
|
||||
in question is still in flight. If it is, its results may still not be visible, so we can't reliably say it's an error.
|
||||
|
||||
To summarize, in order for us to implement an exhaustive checker, we have to iterate operations for each of the rows
|
||||
present in the model in reverse order until we either detect inconsistency that can't be explained by an in-flight
|
||||
operation or until we explain every value in the row.
|
||||
|
||||
## Conclusion
|
||||
|
||||
As you can see, all checkers up till now are almost entirely stateless. Exhaustive and quiescent models rely
|
||||
on `DataTracker` component that is aware of the in-flight and completed `lts`, but don't need any other state apart from
|
||||
that, since we can always inflate a complete partition from scratch every time we validate.
|
||||
|
||||
While not relying on the state is a useful feature, at least _some_ state is useful to have. For example, if we're
|
||||
validating just a few rows in the partition, right now we have to iterate through each and every `lts` that has visited
|
||||
this partition and filter out only modifications that have visited it. However, since the model is notified of each
|
||||
_started_, and, later, finished modification via `recordEvent`, we can keep track of `pd -> (cd -> lts)` map. You can
|
||||
check out `VisibleRowsChecker` as an example of that.
|
||||
|
|
@ -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)
|
||||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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));
|
||||
}
|
||||
}
|
||||
|
|
@ -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
|
|
@ -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(){}
|
||||
}
|
||||
}
|
||||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
@ -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]));
|
||||
}
|
||||
}
|
||||
|
|
@ -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()));
|
||||
}
|
||||
}
|
||||
|
|
@ -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()));
|
||||
}
|
||||
}
|
||||
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
@ -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()));
|
||||
}
|
||||
}
|
||||
|
|
@ -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))) +
|
||||
")";
|
||||
}
|
||||
}
|
||||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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;
|
||||
}
|
||||
|
|
@ -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;
|
||||
}
|
||||
};
|
||||
};
|
||||
}
|
||||
}
|
||||
|
|
@ -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);
|
||||
}
|
||||
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
|
|
@ -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");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
@ -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);
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -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();
|
||||
}
|
||||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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));
|
||||
}
|
||||
}
|
||||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
@ -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());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
|
|
@ -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
Loading…
Reference in New Issue